System DesignIntermediate 15 min Lesson 33 of 42

REST API Design

Design an API other people can use without asking you questions: resource naming, correct status codes, pagination, errors and versioning.

System Design · Lesson 33 of 42
0/42 done(0%)

What is it? #

REST is a style for designing APIs around resources — nouns — manipulated with standard HTTP methods.

Its value is predictability. If your API follows the conventions, a developer can guess most of it correctly after seeing two endpoints.

The core ideas are small: resources have URLs, methods describe the action, status codes describe the outcome, and responses are consistent in shape.

Most of the difficulty is in the details that show up later: pagination, filtering, error formats, versioning and how you change things without breaking clients.

Think of it like this #

A well-organised shop. Aisles are labelled by product type, prices are on the shelf, and the checkout works the same everywhere.

You do not need a guided tour because the layout follows conventions you already know. An API that invents its own conventions requires a tour for every visitor.

Simple example #

An orders API. Listing, filtering, paginating, creating, updating and cancelling orders, with predictable URLs, status codes and error shapes.

Code #

TEXT
Resources and methods

GET    /orders                  list, with filters and pagination
POST   /orders                  create               → 201 + Location header
GET    /orders/A-1              fetch one            → 200 or 404
PATCH  /orders/A-1              partial update       → 200
DELETE /orders/A-1              delete               → 204 (no body)
GET    /orders/A-1/lines        a sub-resource
POST   /orders/A-1/cancel       an action that is not CRUD — acceptable

Nouns, plural, lowercase, hyphenated. Never verbs in the path:
  BAD   /getOrder?id=A-1   /createNewOrder   /order_delete
TEXT
GET /orders?status=paid&created_after=2026-09-01&limit=20&cursor=eyJpZCI6...

200 OK
{
  "data": [
    { "id": "A-1", "status": "paid", "total": 2499.0, "currency": "INR" }
  ],
  "pagination": {
    "next_cursor": "eyJpZCI6IkEtMjEifQ",
    "has_more": true
  }
}
TEXT
A consistent error shape, used everywhere

400 Bad Request
{
  "error": {
    "code": "validation_failed",
    "message": "The request could not be processed",
    "details": [
      { "field": "quantity", "issue": "must be greater than 0" },
      { "field": "sku",      "issue": "unknown product" }
    ],
    "request_id": "req_01H9..."
  }
}

Machine-readable code, human-readable message, per-field details,
and a request ID so support can find it in the logs.
TEXT
Status codes, used honestly

200 OK               read or update succeeded
201 Created          new resource; include a Location header
202 Accepted         accepted for async processing, not yet done
204 No Content       success with nothing to return, e.g. delete
400 Bad Request      malformed or invalid input
401 Unauthorized     no or invalid credentials
403 Forbidden        authenticated but not permitted
404 Not Found        no such resource (also used to hide existence)
409 Conflict         clashes with current state, e.g. duplicate
422 Unprocessable    syntactically valid but semantically wrong
429 Too Many Requests  rate limited; include Retry-After
500 Internal Error   your fault; never leak a stack trace

How it works #

Resource naming is the foundation. Plural nouns with the method carrying the verb means DELETE /orders/A-1 needs no explanation. Verb-based paths force the reader to learn each one.

Cursor pagination is used here rather than page=2. Offset pagination breaks when rows are inserted between requests — items shift and get skipped or duplicated. A cursor encodes a position, so results stay stable.

Wrapping the list in a data key leaves room to add pagination and metadata later without changing the response type. Returning a bare array paints you into a corner.

One error shape everywhere is what makes client code simple. A machine-readable code lets clients branch; the message is for humans; details supports form validation; request_id connects a user's complaint to your logs in seconds.

Status codes must be honest. Returning 200 with an error body forces every client to parse the body to know what happened, and defeats caches, proxies and monitoring that count 5xx responses.

For actions that are not CRUD — cancel, refund, publish — a sub-resource POST such as /orders/A-1/cancel is a pragmatic and widely used compromise. Forcing everything into pure resource semantics usually produces worse APIs.

Versioning belongs in the plan from the start: /v1/orders, or a version header. Within a version, only make additive changes — new optional fields are safe, removing or renaming a field is not.

Real-world use #

Public APIs live or die by their documentation and consistency. OpenAPI specifications generate documentation, client libraries and request validation from one definition, which is why most teams maintain one.

Rate limiting, idempotency keys and correlation IDs, covered in earlier lessons, are all part of a well-behaved API surface.

Breaking changes are the recurring operational problem. Once external clients exist, you cannot rename a field; you add the new one, support both, announce a deprecation timeline and remove the old one later.

Alternatives exist and have their place. GraphQL suits clients that need widely varying data shapes; gRPC suits high-volume internal service calls. REST remains the default for public HTTP APIs because it is universally understood.

The most practical test of an API design is whether a competent developer can use it correctly without asking you a question.

Common mistakes #

  • Verbs in URLs, such as /getOrders or /createUser.
  • Returning 200 with an error inside the body.
  • Offset pagination on data that changes, causing skipped or duplicated rows.
  • Different error shapes on different endpoints.
  • Breaking changes without versioning or a deprecation period.

Practice #

Design the endpoints for a task management API: list with filters and pagination, create, fetch, update, delete, and mark complete. Write the URL, method, status code and response shape for each, plus one error example. Then list three changes that would break existing clients.

Quick quiz

  1. 1. How should resources be named?

  2. 2. Why prefer cursor pagination over offset?

  3. 3. What status code should a successful creation return?

  4. 4. Why include a request_id in error responses?

  5. 5. Which change is safe within an API version?

Summary

  • Model resources as plural nouns and let HTTP methods carry the verb.
  • Use honest status codes; never return 200 with an error body.
  • Prefer cursor pagination and wrap lists in a data object.
  • Use one consistent error shape with a code, message, details and request ID.
  • Version from the start and only make additive changes within a version.