Skip to content

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/4812
  • GET /v1/customers/88/orders
  • POST /v1/carts/91/items
  • DELETE /v1/carts/91/items/3

Weak URI examples:

  • POST /createOrder
  • GET /fetchCustomerOrders
  • POST /deleteCartItem
  • GET /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:

POST /v1/orders
Content-Type: application/json

{"customerId":88,"itemCount":3,"totalPaise":149900}

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:

PATCH /v1/orders/4812
Content-Type: application/json

{"deliverySlot":"7PM-9PM"}

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 OK for a successful read or update response body.
  • 201 Created for successful creation.
  • 202 Accepted when work is queued, not finished.
  • 204 No Content when success needs no body.
  • 400 Bad Request for malformed input shape.
  • 401 Unauthorized when identity is missing or invalid.
  • 403 Forbidden when identity exists but access is denied.
  • 404 Not Found when the resource is absent.
  • 409 Conflict when the state clashes, like version mismatch.
  • 422 Unprocessable Entity when shape is valid but business rule fails.
  • 429 Too Many Requests when rate limits fire.
  • 500 and 503 for 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/302
  • POST /v1/carts
  • POST /v1/carts/91/items
  • POST /v1/orders
  • GET /v1/orders/4812
  • PATCH /v1/orders/4812
  • DELETE /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

  1. Why is /orders/4812 better than /getOrderById?
  2. When is PUT a better choice than POST?
  3. Why should 401 and 403 not be merged casually?
  4. 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