The API is the contract between your clients and your server. It's the list of things a client is allowed to ask for, what each request looks like, and what comes back.
It's worth designing early and carefully, for a simple reason: once a version of the app is on a million phones, you can't take the API back. You can change your database, your server code and your whole architecture behind it, but the API is a promise to code you no longer control.
In this lesson we'll design Tiffin's API in the most common style, REST over HTTP. The next lesson covers the small details (pagination, errors, versioning) that are hard to change later.
Start from the screens, not the tables
A common mistake is to design the API by looking at the database: one endpoint per table. That gives the client building blocks it then has to assemble with many calls, and we saw in the last lesson that round trips are what make a phone app slow.
Start instead from what each client actually does. For Tiffin:
| Client | What it needs to do |
|---|---|
| Customer app | List nearby restaurants, see a menu, place an order, pay, see order status, see past orders |
| Kitchen tablet | See new orders, accept or reject one, mark food ready, mark a dish sold out |
| Rider app | See assigned pickups, mark picked up, mark delivered |
Each of these becomes one or two API calls.
Resources and verbs
REST organises an API around resources: the nouns of your system. For Tiffin those are restaurants, menus, orders, payments and deliveries.
Each resource has a URL. HTTP methods say what you want to do with it:
| Method | Meaning | Changes data? | Same effect if repeated? |
|---|---|---|---|
GET |
Read it | No | Yes |
POST |
Create a new one, or trigger an action | Yes | No |
PUT |
Replace it entirely with what I send | Yes | Yes |
PATCH |
Change some fields | Yes | Depends |
DELETE |
Remove it | Yes | Yes |
"Same effect if repeated" has a name: idempotent. It's about what happens to the data, not the reply: a second DELETE may answer "not found", but the order is gone either way. Sending the same PUT twice leaves things exactly as sending it once. Sending the same POST /orders twice creates two orders. PATCH depends on the change: "set the address to X" is idempotent, but "add 1 to the quantity" isn't. That difference matters a lot on flaky mobile networks, where the app often can't tell whether a request got through. We'll fix the POST case properly with idempotency keys in Module 12. For now, just notice which calls are dangerous to retry.
Tiffin's endpoints
Here's a first version.
# Customer app
GET /restaurants?lat=..&lng=.. nearby open restaurants
GET /restaurants/{id} one restaurant's details
GET /restaurants/{id}/menu its menu, grouped by category
POST /orders place an order
GET /orders/{id} one order and its status
GET /me/orders my past orders
POST /orders/{id}/payment pay for an order
# Kitchen tablet
GET /restaurants/{id}/orders?status=placed new orders for my restaurant
POST /orders/{id}/accept accept an order
POST /orders/{id}/reject reject it, with a reason
POST /orders/{id}/ready food is ready for pickup
PATCH /menu-items/{id} e.g. mark a dish sold out
# Rider app
GET /me/deliveries my current pickups
POST /deliveries/{id}/picked-up
POST /deliveries/{id}/delivered
A few choices to notice.
URLs are nouns, plural, and nested only one level. /restaurants/42/menu reads naturally. /restaurants/42/categories/3/items/7 is too deep; a dish has its own id, so /menu-items/7 is enough.
Status changes are actions, not field edits. We could let the kitchen send PATCH /orders/{id} with {"status": "accepted"}. But accepting an order isn't just setting a field. The server has to check that the order is still placed, that this tablet belongs to this restaurant, and then notify the customer. A named action like POST /orders/{id}/accept makes that rule obvious, and makes it impossible for a client to set some status the server didn't expect.
/me means "the logged-in user". The server works out who "me" is from the login token. The client never sends its own customer id, so it can't ask for someone else's orders by changing a number.
What goes in, what comes out
Let's look at the most important call: placing an order.
POST /orders
Authorization: Bearer <customer token>
Content-Type: application/json
{
"restaurant_id": 42,
"items": [
{ "menu_item_id": 7, "quantity": 2 },
{ "menu_item_id": 19, "quantity": 1 }
],
"delivery_address_id": 5,
"coupon_code": "WELCOME100"
}
What's not in that request matters as much as what is. There's no price, no total, no customer id and no status. All of those are decided by the server. The client only says what it wants.
The response:
HTTP/1.1 201 Created
Location: /orders/9481
{
"id": 9481,
"status": "placed",
"items": [
{ "menu_item_id": 7, "name": "Chicken dum biryani", "quantity": 2, "price_paise": 28000 },
{ "menu_item_id": 19, "name": "Raita", "quantity": 1, "price_paise": 4000 }
],
"subtotal_paise": 60000,
"discount_paise": 10000,
"delivery_fee_paise": 3000,
"total_paise": 53000,
"created_at": "2026-10-18T20:05:13Z"
}
Two habits worth copying:
- Money is an integer in the smallest unit (paise, cents). Never a floating-point number.
0.1 + 0.2is not exactly0.3in floating point, and you don't want rounding errors in what people pay. - Times are in UTC, in ISO 8601 format. Let the client convert to local time for display.
The response also echoes back the names and prices the server used. If a price changed between the customer opening the menu and placing the order, the app can see it and tell them.
Status codes
Every response has a status code. Clients, proxies and monitoring tools all read it, so use the standard meanings rather than returning 200 with "error": true inside.
| Code | Meaning | Tiffin example |
|---|---|---|
200 OK |
It worked, here's the result | Menu returned |
201 Created |
A new resource was made | Order placed |
204 No Content |
It worked, nothing to return | A dish marked sold out |
400 Bad Request |
The request itself is malformed | quantity is "two" |
401 Unauthorized |
We don't know who you are | Missing or expired token |
403 Forbidden |
We know who you are, and you can't do this | A tablet tries to accept another restaurant's order |
404 Not Found |
No such resource | /orders/999999 |
409 Conflict |
The request clashes with the current state | Accepting an order that was already cancelled |
422 Unprocessable |
Well-formed, but breaks a business rule | The restaurant closed a minute ago |
429 Too Many Requests |
Slow down | A buggy app polling 50 times a second (Module 8) |
500 Internal Server Error |
Our bug | An unhandled exception |
503 Service Unavailable |
We're overloaded or down, try later | The database is unreachable |
The big split to remember: 4xx means the client should change something, 5xx means the server failed. That decides whether retrying makes sense. Retrying a 400 gives the same 400 forever. Retrying a 503 after a short wait might work.
401 vs 403 trips everyone up. Despite the name, 401 Unauthorized really means "unauthenticated": log in. 403 Forbidden means "logged in, but not allowed". A customer trying to view someone else's order gets 403, or 404 if you'd rather not even confirm the order exists.
Why REST, and not something else?
REST isn't the only style. You'll meet two others:
- GraphQL lets the client send a query describing exactly which fields it wants, across several resources, in one request. Great when many different clients need different shapes of data.
- gRPC uses compact binary messages and generated code. Mostly used for service-to-service calls inside a company, which we'll cover in Module 11.
For Tiffin version one, REST is a good default. Every language, tool and developer understands it, it works with browser and CDN caching out of the box (useful in Module 3), and it's easy to debug with nothing more than curl.
Design the rider's endpoints
Riders get offered deliveries. A rider can accept or decline an offer, and once accepted, the customer should be able to see the rider's name and phone number on their order. Sketch the endpoints, and for each say what status code you'd return if two riders try to accept the same delivery at the same moment.
Show one answer
GET /me/delivery-offers offers waiting for me
POST /delivery-offers/{id}/accept take it
POST /delivery-offers/{id}/decline pass on it
GET /orders/{id} (customer) now includes a "rider" object
Accept is an action, so POST. The first rider gets 200 OK with the delivery details. The second gets 409 Conflict: their request was fine, but the state changed under them, so the app should show "this delivery was taken" and move on.
On the server, making sure only one wins is a database job: something like UPDATE deliveries SET rider_id = :me WHERE id = :id AND rider_id IS NULL, and checking whether a row was updated.
What to remember
- The API is a contract with code you don't control. Design it carefully, because you can't take it back.
- Design from what each screen and client needs, not from your database tables.
- URLs name resources (nouns). HTTP methods say what to do.
GET,PUTandDELETEare idempotent,POSTis not. - Model important state changes as named actions (
POST /orders/{id}/accept) so the server enforces the rules. - Clients say what they want. The server decides prices, totals, ids and statuses. Money is an integer in paise or cents.
- Use standard status codes. 4xx means the client must change something, 5xx means the server failed.