API Design Essential

REST API Design

Principles and conventions for designing RESTful HTTP APIs that are consistent, evolvable, and easy for clients to consume — covering resource naming, HTTP semantics, status codes, versioning, and pagination.

⏱ 14 min read

What it is

REST (Representational State Transfer) is an architectural style for distributed hypermedia systems described by Roy Fielding in his 2000 dissertation. A RESTful API exposes resources — conceptual entities — via a uniform interface built on standard HTTP methods, URIs, and status codes. Clients interact with resources by exchanging representations (typically JSON) without the server needing to store client session state.

The six constraints that define REST are: client-server separation, statelessness, cacheability, a uniform interface, a layered system, and (optionally) code-on-demand. In practice, most "REST" APIs are actually RESTish — they use HTTP verbs and JSON but stop short of full HATEOAS hypermedia links.

Key design dimensions include: how you name and structure resource URIs, which HTTP methods you expose, what status codes you return, how clients request partial data or page through collections, and how you evolve the API over time without breaking existing consumers.

Why it exists

Before REST became dominant, web services used SOAP/WSDL (heavy XML envelopes, strict contracts) or ad-hoc RPC styles (?action=getUser). REST leveraged the existing HTTP infrastructure — caches, CDNs, proxies, browsers — and gave developers a predictable mental model: every noun is a URL, and the verb is always an HTTP method.

The uniform interface reduces the learning curve for API consumers. When a developer encounters a new REST API, they can reasonably guess that GET /orders lists orders and DELETE /orders/42 removes one. This predictability lowers integration cost significantly compared to custom protocols.

When to use

  • Public-facing APIs consumed by external developers who expect standard HTTP conventions.
  • Browser-based clients and mobile apps that benefit from HTTP caching and standard tooling.
  • CRUD-heavy domains where resources map naturally to database entities.
  • Systems that need broad interoperability — REST works in any language with an HTTP library.
  • When discoverability and self-documentation via OpenAPI are priorities.

When not to use

  • Real-time streaming — WebSockets or SSE are more appropriate than repeated polling.
  • High-throughput internal service-to-service calls — gRPC with Protocol Buffers offers lower overhead and generated clients.
  • Complex, nested queries — GraphQL gives clients precise field-level control that REST's flat resource model struggles with.
  • Event-driven workflows — async messaging patterns (Kafka, SQS) decouple producers from consumers more effectively.

Typical architecture

Client                API Gateway          Service
  |                       |                    |
  |-- GET /v1/orders/42 -->|                    |
  |                       |-- forward + auth -->|
  |                       |                    |-- DB query
  |                       |<-- 200 JSON --------|
  |<-- 200 JSON -----------|                    |

Resource hierarchy:
  /users                         → collection
  /users/{id}                    → item
  /users/{id}/addresses          → sub-collection
  /users/{id}/addresses/{addrId} → sub-item

HTTP method semantics:
  GET     /users         → list (safe, idempotent)
  POST    /users         → create (neither)
  GET     /users/42      → fetch single (safe, idempotent)
  PUT     /users/42      → full replace (idempotent)
  PATCH   /users/42      → partial update (idempotent if designed so)
  DELETE  /users/42      → delete (idempotent)

ETag conditional request:
  GET /products/7
  ← ETag: "abc123"
  GET /products/7 + If-None-Match: "abc123"
  ← 304 Not Modified  (no body transmitted)

Pros and cons

Pros

  • Leverages existing HTTP infrastructure — CDNs, proxies, caching layers work natively.
  • Universal tooling: every language, every platform has an HTTP client.
  • Stateless design simplifies horizontal scaling — any server can handle any request.
  • Well-understood by developers; vast documentation and community support.
  • OpenAPI spec ecosystem enables generated docs, SDKs, and mocks.
  • Uniform interface reduces cognitive load for API consumers.

Cons

  • Over-fetching and under-fetching: clients often receive too much data or must make multiple round trips.
  • No built-in query language — complex filters require custom conventions.
  • Versioning is painful; breaking changes require coordinated migration.
  • Chatty for deeply nested resources (N+1 round trips).
  • JSON is verbose compared to binary protocols like Protobuf.
  • HTTP/1.1 head-of-line blocking; mitigated by HTTP/2 but not always deployed.

Implementation notes

Resource naming

Use plural nouns for collections: /orders, /users, /products. Never use verbs in URIs (/getOrder, /createUser — those are RPC, not REST). Keep URIs lowercase, hyphenated for multi-word segments (/line-items), and hierarchical only when ownership is genuinely modelled (limit nesting to two levels).

Status codes

Return semantically correct status codes — clients and infrastructure depend on them:

  • 200 OK — successful GET, PUT, PATCH.
  • 201 Created — successful POST; include Location header pointing to the new resource.
  • 204 No Content — successful DELETE or PATCH with no response body.
  • 400 Bad Request — malformed input; include a structured error body.
  • 401 Unauthorized — missing or invalid credentials.
  • 403 Forbidden — authenticated but lacking permission.
  • 404 Not Found — resource does not exist.
  • 409 Conflict — optimistic concurrency collision or duplicate creation.
  • 422 Unprocessable Entity — syntactically valid but semantically invalid input.
  • 429 Too Many Requests — rate limit exceeded.

Versioning strategies

URL path versioning (/v1/orders) is the most visible approach and the easiest to route. Use it for major breaking changes. Accept header versioning (Accept: application/vnd.myapi.v2+json) keeps URLs clean but is harder to test in a browser. Query parameter versioning (?version=2) is simple but pollutes URLs. Most teams default to URL versioning for public APIs.

Pagination

Offset/limit (?offset=100&limit=25) is simple but suffers from the "moving window" problem — if rows are inserted during pagination, clients see duplicates or skip rows. Cursor-based pagination uses an opaque token encoding the position of the last seen item, making it stable against inserts and deletes. Return cursors in a next or Link header (RFC 5988). Use cursor pagination for any collection that mutates frequently.

Partial responses

Support a fields query parameter to allow clients to request sparse fieldsets: GET /users/42?fields=id,name,email. This reduces payload size and over-fetching without requiring GraphQL. Document the supported fields in your OpenAPI schema.

Conditional requests

Return an ETag (a hash or version number of the resource) with every GET. Clients can send If-None-Match: "abc123" on subsequent requests; if the resource has not changed, return 304 Not Modified with no body, saving bandwidth. Use If-Match for optimistic concurrency on PUT/PATCH — if the ETag no longer matches, return 412 Precondition Failed.

Error bodies

Adopt a consistent error schema. The RFC 9457 (Problem Details) standard provides a good baseline:

{
  "type": "https://api.example.com/errors/validation",
  "title": "Validation failed",
  "status": 422,
  "detail": "The 'email' field must be a valid address.",
  "instance": "/orders/attempt-7e2a",
  "errors": [
    { "field": "email", "message": "Invalid format" }
  ]
}

Common failure modes

  • Verb tunnelling through POST — using POST /users/42/delete instead of DELETE /users/42 bypasses HTTP method semantics and breaks caches.
  • Returning 200 for errors — wrapping errors in a 200 body ({"success": false}) defeats HTTP-aware middleware and monitoring.
  • Inconsistent naming — mixing userId, user_id, and UserID in the same API forces clients to handle multiple styles.
  • Unbounded collections — returning an entire table without pagination causes out-of-memory issues as data grows.
  • Ignoring idempotency — a non-idempotent POST for resource creation means network retries can duplicate records; use idempotency keys.
  • Breaking changes without versioning — removing a field or changing a field type in an existing response silently breaks consumers.
  • Chatty sub-resource APIs — requiring 10 API calls to render a single screen degrades mobile performance.

Decision checklist

  • Are all URIs plural nouns with no verbs?
  • Are HTTP methods used according to their defined semantics (GET safe, PUT/DELETE idempotent)?
  • Are status codes semantically correct, not just 200 or 500?
  • Do error responses include a structured body with field-level details?
  • Is there a versioning strategy documented for breaking changes?
  • Are all collections paginated with a stable cursor mechanism?
  • Are ETags returned for cacheable resources?
  • Is an OpenAPI spec generated and kept in sync with the implementation?
  • Are idempotency keys supported for non-idempotent POST operations?
  • Is a fields parameter available for large resource types?

Example use cases

  • E-commerce platformGET /v1/products?category=shoes&sort=-price&cursor=eyJpZCI6MTAwfQ returns a stable, sorted page of products with a cursor for the next page.
  • Payment APIPOST /v1/payments with an Idempotency-Key: uuid header ensures a network retry does not charge a customer twice.
  • User management servicePATCH /v1/users/42 with a JSON Merge Patch body updates only the supplied fields; If-Match prevents concurrent overwrites.
  • Content delivery API — ETags on GET /v1/articles/slug let CDN edges and browser caches serve stale content and only revalidate when the article changes.
  • GraphQL — query language alternative that eliminates over/under-fetching.
  • gRPC — binary RPC alternative for internal, high-throughput services.
  • API Versioning — strategies for evolving APIs without breaking consumers.
  • API Gateway — cross-cutting concerns (auth, rate limiting) at the edge.
  • API Pagination — deep dive into cursor, offset, and keyset pagination.
  • HATEOAS — adding hypermedia links to REST responses for discoverability.
  • OpenAPI Specification — contract-first tooling for REST APIs.

Further reading

  • Fielding, R. T. (2000). Architectural Styles and the Design of Network-based Software Architectures — the original REST dissertation.
  • RFC 9457 — Problem Details for HTTP APIs.
  • RFC 5988 — Web Linking (Link header for pagination).
  • Microsoft REST API Guidelines — comprehensive naming and versioning conventions.
  • Zalando RESTful API Guidelines — battle-tested rules from a large e-commerce platform.
  • Google API Design Guide — resource-oriented design from a hyper-scale API team.