How it works
Start with resources and their lifecycles, not database tables. For a booking service, expose resources such as /holds and /reservations, with stable identifiers and explicit states. Use query parameters for filtering and sorting, such as GET /reservations?status=confirmed, and check authorization for every requested resource.
Choose methods by their semantics. GET reads without requesting a state change; POST commonly creates a resource or submits an operation; PUT replaces the state at a known URI; PATCH applies a partial update; DELETE removes a resource or its association. PUT and DELETE are idempotent in intended effect, although repeated requests can return different responses. PATCH is not automatically idempotent.
Make outcomes understandable to clients. A successful creation can return 201 Created with a Location header; 204 indicates success without a response body. Use meaningful failures such as 400 for malformed requests, 401 for missing or invalid authentication, 403 for forbidden access, and 409 for a state conflict. Return a consistent error body with a machine-readable code, useful details, and a request identifier, without leaking secrets.
Design explicitly for retries and concurrent changes. A POST /payments request can carry an application-defined Idempotency-Key so a timeout does not cause a second charge. For document edits, return an ETag and require If-Match on updates; reject a stale version with 412 Precondition Failed. These contracts need durable deduplication or atomic version checks behind the API.
Bound collection reads and define their ordering. For example, GET /photos?limit=50&cursor=... returns a page and an opaque continuation cursor. A cursor can encode the last item's creation time and ID, but it does not automatically provide a consistent snapshot. For cacheable reads, define Cache-Control and validators while preventing private responses from entering shared caches.
Represent long-running work honestly. POST /exports can return 202 Accepted with a link to a durable operation resource that reports pending, running, succeeded, or failed. Acceptance is not completion. Document schemas and errors, commonly with OpenAPI, and establish compatibility rules so older clients continue working as the API evolves.
A worked example
A payment API that handles an ambiguous timeout
A client sends POST /payments with amount_minor=2500, currency=USD, and Idempotency-Key=k123. The server atomically records the request fingerprint and creates payment p42 in a pending state before scheduling external processing. It returns 201 Created with Location: /payments/p42; creation does not mean the charge succeeded. If the response is lost, the client retries with k123 and receives the same payment identity rather than creating another payment. Reusing k123 with amount_minor=3000 returns a documented conflict. GET /payments/p42 reports the current outcome. The contract specifies a 24-hour key retention window; beyond it, clients must not assume that reusing the key remains safe. External charging still needs provider-side deduplication or reconciliation.
Trade-offs to say out loud
- Uniform resources versus specialized operations
- Resource-oriented contracts are predictable, but workflows such as cancellation often need an explicit operation or subresource rather than unrestricted field updates.
- Small resources versus request overhead
- Fine-grained endpoints are reusable, but a screen that needs many related objects may require bounded expansions, batching, or an aggregated response.
- Retry safety versus server-side coordination
- Idempotency keys simplify recovery from timeouts but require defined key scope, payload validation, retention, and atomic handling of concurrent requests.
- Freshness versus caching efficiency
- Caching reduces read latency and backend load, but each endpoint needs an explicit staleness policy and safe treatment of authenticated data.
- Pagination flexibility versus stable traversal
- Offset pagination supports direct page numbers, while cursor pagination usually handles large changing collections better but limits random access and does not inherently freeze results.
- Compatibility versus faster contract changes
- Keeping old clients working constrains schema evolution, while introducing a new API version creates migration and maintenance work.
When to reach for it
- The problem exposes a request-response interface to mobile apps, browsers, partners, or independently deployed services.
- Clients create or modify durable resources, and you need to define what success means after a timeout or retry.
- Collections can grow large enough to require bounded responses, filtering, stable ordering, and pagination.
- Multiple clients can update the same resource, or operations take too long to complete in one HTTP request.
- The interviewer asks for an API contract before moving into storage and service architecture.
How to bring it up in the interview
- Start with two or three critical user flows and show their requests, responses, identifiers, and failure cases; do not spend the interview naming every endpoint.
- Explain the guarantee behind each successful response: whether the resource is durable, work is merely accepted, or an external action has actually completed.
- Walk through a lost response and a retry. Expect follow-up questions about duplicate requests, concurrent retries, key expiration, and crashes between recording the request and performing its effect.
- Tie the contract to implementation: If-Match requires an atomic conditional write, cursor pagination needs a matching query plan, and asynchronous acceptance needs durable work tracking.
- Defend REST as a fit for resource-oriented request-response traffic, not as a universal solution; expect comparisons with GraphQL, RPC, and streaming interfaces when those better match the workload.
Common mistakes
- Treating REST as JSON over HTTP or a naming convention, without defining method semantics, resource states, and failure behavior.
- Using GET for state-changing actions, or assuming every PUT, DELETE, or PATCH handler is retry-safe merely because of its method name.
- Returning 200 for every outcome or calling 202 Accepted proof that background work has completed.
- Accepting an idempotency key without defining its scope, retention, payload mismatch behavior, and atomic relationship to the operation.
- Exposing unbounded lists, allowing arbitrary writes to server-controlled fields, or assuming possession of a resource ID grants access.
Where it comes up
Interview questions where rest api design is part of a strong answer, and why.
- Design a payment systemThe payment creation contract must distinguish durable acceptance from successful charging and define retry behavior after ambiguous failures.
- Design Wikipedia or NotionDocument reads and saves need stable resource identities, revision semantics, and conditional updates that prevent silent overwrites.
- Design a control plane for a distributed databaseProvisioning, scaling, and upgrade APIs need durable operation resources that expose progress and remain safe across retries and uncertain outcomes.
Questions people ask
What are the most important REST API design best practices?
Model resources around user-visible concepts, use HTTP methods and status codes consistently, and define validation and error responses. Add bounded pagination, per-resource authorization, retry safety, and compatibility rules where needed. In an interview, explain the behavior under failure rather than only listing endpoint names.
What is the difference between REST and an HTTP JSON API?
REST is an architectural style with constraints including stateless interactions, a uniform interface, cacheability, and hypermedia-driven application state. An HTTP API that exchanges JSON is not automatically RESTful. Many APIs called REST in practice are resource-oriented HTTP APIs that adopt only some of these constraints.
Does a stateless REST API mean the server cannot store sessions or data?
No: servers still store resources, credentials, operation records, and other application data. Stateless interaction means each request supplies the context needed to interpret it rather than depending on an implicit conversational sequence held by a particular server. Authentication and authorization still need explicit design.