API design: REST, HTTP, GraphQL and gRPC cheat sheet
The 22 API design: REST, HTTP, GraphQL and gRPC interview answers in 30 seconds each, plus quick facts. Printable, free.
One page per topic: print it or save it as PDF from your browser's print menu. Practise these questions
Rules of thumb
- Nouns in paths, verbs in methods; 201 + Location for creates.
- POST isn't idempotent: require an Idempotency-Key for creates clients may retry.
- If-None-Match → 304; If-Match → 412.
- Cursor pagination with an opaque token and a max page size.
- Add, don't change: new fields are safe, renames and type changes aren't.
- One error format (Problem Details) with stable types and a trace id.
- 429 + Retry-After; clients wait and add jitter.
- Sign webhooks over timestamp + raw body; compare in constant time.
Designing REST resources
Model nouns as resources and let HTTP methods carry the verbs: GET /orders, POST /orders, GET /orders/{id}, PATCH /orders/{id}, and nested items at /orders/{id}/items when they only exist inside an order. Use plural, lowercase, hyphenated path segments, put filters and sorting in query parameters, and return the created resource's URL in a Location header with 201.
Safe and idempotent HTTP methods
Safe methods (GET, HEAD, OPTIONS) shouldn't change state. Idempotent methods give the same server state whether a request runs once or several times: GET, HEAD, PUT, DELETE and OPTIONS. POST isn't idempotent, and PATCH isn't guaranteed to be. That matters because clients, proxies and load balancers retry after timeouts; a retried idempotent request is harmless, while a retried POST can create a second order unless you add an idempotency key.
Choosing HTTP status codes
Use the code clients can act on. 400 for a malformed request, 422 when it's well-formed but fails validation, 401 when credentials are missing or invalid (the client should authenticate), 403 when the caller is known but not allowed, 404 when it doesn't exist (or to hide that it does), 409 for a conflict with current state, 412 when an If-Match precondition fails, 429 for rate limits with Retry-After, and 503 when the server is temporarily unavailable.
Idempotency keys for POST
Have the client generate a unique Idempotency-Key per logical operation and send it with every retry. The server stores the key with the request fingerprint and, once done, the response. A retry with the same key gets the stored response instead of running again; a concurrent duplicate waits or gets 409; the same key with a different body is rejected with 422. Keep keys for at least as long as clients retry, often 24 hours.
CREATE TABLE idem (
key text PRIMARY KEY,
request_hash text NOT NULL,
status int,
response jsonb
);
-- first request claims the key, does the work, stores the answer
INSERT INTO idem (key, request_hash) VALUES ('order-7f3a', 'sha256:ab');
UPDATE idem SET status = 201, response = '{"order_id": 42}'
WHERE key = 'order-7f3a';
-- the retry finds the key and replays
INSERT INTO idem (key, request_hash) VALUES ('order-7f3a', 'sha256:ab')
ON CONFLICT (key) DO NOTHING;
SELECT status, response->>'order_id' FROM idem WHERE key = 'order-7f3a';ETags and conditional requests
An ETag is a version identifier for a representation. For caching, the client sends If-None-Match: <etag> and the server answers 304 Not Modified with no body if nothing changed, saving bandwidth. For safe updates, the client sends If-Match: <etag> with a PUT or PATCH; if someone else changed the resource since, the server answers 412 Precondition Failed instead of overwriting. That's optimistic locking over HTTP.
Paginating API responses
Use cursor pagination: return a page of items plus an opaque next cursor that encodes where the page ended (for example the last sort key and id). The client passes it back to get the next page. Unlike offset pages, cursors don't skip or repeat items when rows are inserted, and each page costs the same to fetch. Put a server-enforced maximum on the page size.
Versioning an API
Prefer compatible changes so you rarely need a version: add optional fields and new endpoints, never remove or rename fields or change their meaning, and make clients ignore fields they don't know. When a breaking change is unavoidable, publish a new major version (in the path like /v2/, or by media type), run both in parallel, announce a deprecation date with Deprecation and Sunset headers, and track who still calls the old one.
Which API changes are breaking
A change breaks clients if a correct old client could now fail. For responses: removing or renaming a field, changing its type or meaning, or making it nullable are breaking; adding a field usually isn't. For requests: adding a required parameter, tightening validation, or removing an accepted value are breaking; adding an optional parameter isn't. Status code and error format changes are breaking too.
A consistent error format
Return the right status code plus a machine-readable body in one format across the whole API. RFC 9457 Problem Details (application/problem+json) is a good standard: type (a URI identifying the error kind), title, status, detail and instance, plus your own fields such as a list of invalid fields. Clients branch on type or a stable error code, never on the human-readable text, and the body never leaks stack traces or SQL.
Rate limiting with a token bucket
A token bucket holds up to N tokens and refills at a steady rate; each request takes a token or is rejected when none are left. Unused tokens build up to the capacity, so a client that was quiet can send a short burst, while the long-run rate stays capped by the refill. A fixed window counts requests per clock minute, which lets a client send double the limit across a window boundary and resets abruptly.
429, Retry-After and rate-limit headers
Return 429 Too Many Requests with a Retry-After header (seconds or a date) and a Problem Details body. Advertise the limits so well-behaved clients can pace themselves: many APIs send RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset style headers (an IETF draft standardises RateLimit and RateLimit-Policy). Clients should wait at least Retry-After, add jitter, and slow down before hitting the limit.
When GraphQL fits and what it costs
GraphQL fits when many clients need different shapes of related data: one endpoint, a typed schema, and each client asks for exactly the fields it needs, avoiding over-fetching and chains of requests. The costs: N+1 database queries from naive resolvers, harder HTTP caching because everything is a POST to one URL, and expensive queries clients can write unless you limit depth and complexity.
gRPC vs REST
gRPC uses Protocol Buffers over HTTP/2: compact binary messages, generated typed clients in many languages, deadlines and cancellation built in, and streaming in either direction. It suits internal service-to-service calls where both sides are yours and latency matters. JSON over HTTP is easier to debug with curl, works in every browser and through every proxy, and is the better default for public APIs.
Designing reliable webhooks
Sign every delivery: an HMAC of the timestamp and raw body with a per-customer secret, sent in a header, so receivers can verify origin and reject replays outside a few minutes. Deliver at least once: retry with exponential backoff for a day or more, and include an event id so receivers can deduplicate. Receivers should respond 2xx quickly and process asynchronously; events may arrive out of order, so include a timestamp or version.
Long-running operations: 202 and polling
Don't hold the request open. Accept it with 202 Accepted and a Location header pointing to an operation resource (/exports/{id}) that reports status (pending, running, succeeded, failed) and, when done, a link to the result. Clients poll that URL with backoff, or you notify them by webhook. The job runs in a worker from a queue, and creating it should take an idempotency key so retries don't start two exports.
CORS: what it protects and how to configure it
Browsers block a page from reading responses from another origin unless that server opts in. CORS is the opt-in: the API sends Access-Control-Allow-Origin for the allowed origin; for non-simple requests (custom headers, JSON bodies, PUT/DELETE) the browser first sends an OPTIONS preflight and checks Allow-Methods and Allow-Headers. Allow your known front-end origins explicitly. CORS doesn't protect the API from non-browser clients.
JSON pitfalls: money, big ids and dates
JSON numbers are often parsed as 64-bit floats. JavaScript can't represent integers above 2^53 − 1 exactly, so large ids get silently rounded; send them as strings. Floats can't hold most decimal fractions exactly, so send money as integer minor units or a decimal string plus a currency code. Send timestamps as RFC 3339 strings in UTC with an explicit offset, not local times or ambiguous formats.
PATCH: merge patch vs JSON Patch
Two standards: JSON Merge Patch (RFC 7396, application/merge-patch+json) sends a partial document; fields present are replaced, and null means delete the field. It's simple but can't set a field to null as a value or edit array elements. JSON Patch (RFC 6902) sends a list of operations (add, remove, replace, test) with paths, which handles arrays and conditional updates at the cost of complexity. Pair either with If-Match.
Uploading large files
Don't stream large files through your API servers. Have the API authorise the upload and return a presigned URL to object storage (S3, GCS) with a short expiry, size limit and content type; the client uploads directly, in parts for big files so a dropped connection resumes instead of restarting. Storage then notifies your backend (event or callback) to validate, scan and process the file in a worker.
Polling, SSE and WebSockets
Polling is simplest and works everywhere; fine when updates every few seconds are enough. Server-sent events keep one HTTP response open and stream text events from server to client, with automatic reconnect and Last-Event-ID resume, which suits one-way feeds. WebSockets give a full two-way channel, for chat or collaborative editing, but need their own auth, heartbeats and load-balancer support.
Batch and bulk endpoints
Accept many items in one request with a firm maximum (for example 500), validate each, and return a per-item result: success with the new id or an error, in the same order or keyed by a client-supplied reference. Decide and document whether the batch is all-or-nothing (one transaction) or partial success; partial is more useful for imports, and 207 Multi-Status or 200 with per-item statuses communicates it.
What an API gateway does
A gateway is the single entry point that handles cross-cutting edge concerns: TLS termination, routing to services, authentication (validating tokens), coarse rate limits, request size limits, CORS, and request logging with trace ids. Business rules and fine-grained authorization stay in the services, because only they know whether this user may edit this order. A backend-for-frontend is a gateway variant shaped for one client type.
Quick facts
- Idempotent methods
- GET, HEAD, PUT, DELETE and OPTIONS are idempotent in HTTP semantics; POST isn't, and PATCH isn't guaranteed.
- RFC 9457
- Problem Details for HTTP APIs (2023), replacing RFC 7807: type, title, status, detail, instance.
- Largest safe JavaScript integer
- 2^53 − 1 = 9,007,199,254,740,991. Send bigger ids as strings.
- 412 vs 428
- 412 Precondition Failed: If-Match didn't match. 428 Precondition Required: the server insists on a condition.
- JSON Merge Patch
- RFC 7396: null deletes a field and arrays are replaced wholesale.
- S3 multipart limits
- Parts of 5 MiB to 5 GiB (the last can be smaller), up to 10,000 parts per upload.