01. REST API Design — build the menu around nouns, not kitchen actions¶
~16 min read. Clean REST feels boring in the best possible way.
Built on the ELI5 in 00-eli5.md. The menu — the list of what customers can ask for — becomes your map of resources, verbs, and predictable outcomes.
1) Start with resources and stable URIs¶
REST starts with nouns. Not controller names. Not database table verbs. See. A client wants an order, a cart, a payment, or a user profile. So the URI should name that thing clearly.
Good URI examples:
GET /v1/orders/4812GET /v1/customers/88/ordersPOST /v1/carts/91/itemsDELETE /v1/carts/91/items/3
Weak URI examples:
POST /createOrderGET /fetchCustomerOrdersPOST /deleteCartItemGET /orderService/getById?id=4812
The menu should read like categories in a restaurant. Simple, no? You scan it and know what exists. You do not decode backend class names.
A quick shape helps:
┌──────── Client wants data ────────┐
│ user profile │ cart │ order │ bill │
└───────────────────────────────────┘
│
▼
URI names the resource clearly
Plural nouns are common because collections matter. /orders means the collection. /orders/4812
means one member. Nested paths show ownership carefully. /customers/88/orders/4812 means order
4812 under customer 88. Do not nest six levels deep. That usually signals confused boundaries.
A practical URI rule set:
- Use lowercase letters.
- Use hyphens for readability when needed.
- Put version at one consistent place.
- Keep filters in query parameters, not in path names.
- Keep identifiers opaque.
Worked example. Say your food delivery system has 12 million orders. A support tool wants one order
fast. Use GET /v1/orders/4812. If support wants delayed orders from Bengaluru, use GET
/v1/orders?city=bengaluru&status=delayed&limit=50. The path still names the resource. The query
shapes the slice.
2) Pick HTTP verbs by meaning, not by convenience¶
REST uses HTTP verbs as business signals. That signal matters for retries, caches, and client expectations. So what to do? Match the verb to the real intent.
GET reads data. It should not create side effects. If GET /orders/4812/confirm confirms an
order, you are hiding a write behind a read. That is dangerous.
POST creates a subordinate resource, or triggers a non-idempotent action. Example:
If that succeeds, you usually return 201 Created. You may include Location: /v1/orders/4812.
PUT replaces the full resource state. If the client sends the same body twice, the end state
should stay the same. That is idempotency.
PATCH changes part of the resource. Example:
DELETE removes the resource, or marks it deleted. Calling the same delete twice should not create
two cancellations. It should still land in the same end state.
See this retry table:
Verb Safe to retry? Typical use
GET Yes Read one or many resources
POST Usually no Create order, start payment
PUT Yes Replace cart snapshot
PATCH Depends Partial update with clear rules
DELETE Yes Remove or cancel resource
Worked example with numbers. A courier app times out after 800 ms. It retries PUT
/v1/drivers/22/location with the same latitude and longitude. Good design updates the same location
once more, not create duplicate driver records. If it retries POST /v1/payments, you may
accidentally charge twice. This is why idempotency keys matter around POST.
3) Status codes are contracts, not decoration¶
The receipt of HTTP is the status code plus body. If you misuse status codes, clients build
wrong fallback logic. See. A mobile app cannot guess your secret meaning for 200.
Common codes you should use deliberately:
200 OKfor a successful read or update response body.201 Createdfor successful creation.202 Acceptedwhen work is queued, not finished.204 No Contentwhen success needs no body.400 Bad Requestfor malformed input shape.401 Unauthorizedwhen identity is missing or invalid.403 Forbiddenwhen identity exists but access is denied.404 Not Foundwhen the resource is absent.409 Conflictwhen the state clashes, like version mismatch.422 Unprocessable Entitywhen shape is valid but business rule fails.429 Too Many Requestswhen rate limits fire.500and503for server-side trouble.
Worked example. A rider can cancel an order only before pickup. Request body is valid, but the order
is already picked up. 422 or 409 explains the state problem better than 400. If order 4812
does not exist, that is 404. If auth token expired 12 minutes ago, that is 401. Different causes
need different receipt signals.
A compact flow:
Client ──▶ POST /v1/orders
│
├── valid and created ──▶ 201
├── bad JSON shape ──▶ 400
├── stock conflict ──▶ 409
└── kitchen on fire ──▶ 503
Keep response bodies consistent too. One error shape for all teams beats five custom surprises. Include code, message, and machine-usable details.
4) HATEOAS is optional in practice, but discoverability still matters¶
HATEOAS means the server gives links for the next legal moves. Pure REST fans love it. Most real APIs use it lightly. That is fine. But discoverability still matters.
Suppose GET /v1/orders/4812 returns this:
{
"id": 4812,
"status": "PLACED",
"totalPaise": 149900,
"links": {
"self": "/v1/orders/4812",
"cancel": "/v1/orders/4812/cancel",
"payment": "/v1/orders/4812/payment"
}
}
Those links tell the client what comes next. That is a small HATEOAS win. You do not need to build a hypermedia religion. You do need a predictable menu and clear next steps.
Concrete endpoint set for an ecommerce checkout:
GET /v1/products/302POST /v1/cartsPOST /v1/carts/91/itemsPOST /v1/ordersGET /v1/orders/4812PATCH /v1/orders/4812DELETE /v1/orders/4812/cancel
Now check the sequence. Product is a resource. Cart is a resource. Order is a resource. Each URI says what thing is being handled. The verbs say what change is requested. The status codes say what happened. That is REST doing useful work.
Mini worked example. A search page needs 20 products out of 87 total matches. Use GET
/v1/products?query=tea&page=2&pageSize=20. Return 200 with items, total, page, and
pageSize. Do not invent /searchProductsPage2.
Another check. If two clients update order note text concurrently, a version field can prevent
silent overwrite. Client sends If-Match: 17 or body version 17. Server returns 409 when
current version is already 18. That protects the latest state clearly.
One more URI smell test. If a path contains implementation words like controller or service, the
menu is leaking kitchen internals. Clients should see business nouns, not your package
structure. Simple, no?
Where this lives in the wild¶
- A Stripe API engineer designs payment intent endpoints so merchants can create, confirm, and retrieve payments predictably.
- A Swiggy backend engineer exposes order, cart, and delivery resources so mobile apps can retry safely during weak network moments.
- A GitHub platform engineer shapes repository and issue URIs so millions of clients can read and automate actions consistently.
- A Shopify product API engineer keeps product, cart, and checkout endpoints stable for app developers building storefront flows.
- A Notion integrations engineer designs block and database resources so third-party clients navigate object relationships cleanly.
Pause and recall¶
- Why is
/orders/4812better than/getOrderById? - When is
PUTa better choice thanPOST? - Why should
401and403not be merged casually? - What practical value do response links give in a REST API?
Interview Q&A¶
How do you choose between PUT and PATCH?¶
Use PUT for full replacement semantics, and PATCH for partial updates with explicit patch rules.
Idempotency and client expectations should stay obvious. Common wrong answer to avoid: "PATCH is
always better because it sends fewer bytes."
Why avoid verbs in REST URIs?¶
Because the URI should identify the resource, while the HTTP method carries the action meaning. That keeps the surface predictable across many endpoints. Common wrong answer to avoid: "Verbs in paths are fine because HTTP verbs are optional decoration."
When would you return 202 Accepted?¶
Return it when the request is valid, but the work continues asynchronously after the response. The
client should not assume the job already finished. Common wrong answer to avoid: "Use 202 for any
slow request, even when the update is complete."
Is HATEOAS mandatory for a good REST API?¶
No. Lightweight links can help, but consistency of resources, verbs, and responses matters more in most teams. Common wrong answer to avoid: "If there are no hypermedia links, the API is not REST at all and is useless."
Apply now (5 min)¶
Exercise. Design the checkout surface for a grocery app. Write five endpoints for products, carts, orders, and order cancellation. For each one, choose the method, path, and expected success status.
Sketch from memory. Draw one box for client, one for API, and three resource paths under it. Then say aloud which operations are safe to retry, and which ones need special care.
Bridge. REST gives one fixed menu to everyone. GraphQL asks each client to pick exactly the fields it wants from that menu. → 02-graphql-design.md