Sections
Related Guides
Design Patterns for LLD Interviews
Low-Level Design
SOLID Principles in Practice
Low-Level Design
Structural Patterns: Proxy, Adapter, Facade, Composite & Decorator
Low-Level Design
API Gateway Design: Auth, Rate Limiting, Routing, and BFF Patterns
High-Level Design
Rate Limiter Design: Token Bucket, Sliding Window, and Distributed Enforcement
High-Level Design
API Design Principles: REST, Idempotency, Versioning, and Error Contracts
Well-designed APIs are force multipliers — enabling teams to work independently and systems to evolve safely. Master REST's UNIFORM INTERFACE constraint, idempotency keys, API versioning strategies, and error contracts — the LLD interview topics that separate senior from junior engineers at FAANG.
Why API Design Is an LLD Topic
API design lives at the intersection of LLD and systems thinking. It's not just "write REST endpoints" — it's modeling your domain as a service contract that multiple teams or systems will depend on for years. A poorly designed API is harder to change than poorly structured internal code, because your clients depend on it.
The three laws of API design (Hyrum's Law, Postel's Law, and the Principle of Least Astonishment) define why this is hard: once an API is published, you effectively promise never to break clients even in ways you didn't document. Every naming choice, every field, every error code is a permanent commitment.
Hyrum's Law: "With a sufficient number of users of an API, it does not matter what you promise in the contract: all observable behaviors of your system will be depended on by somebody." This is why you can't remove fields, change error codes, or alter response structure without breaking someone.
What Earns Each Level
6/10: Knows HTTP methods and status codes. Designs basic CRUD endpoints. Knows GET is idempotent.
8/10: Designs idempotent PUT and DELETE. Implements idempotency keys for non-idempotent POST (payment, order). Versions APIs correctly (URI versioning vs header). Designs a complete error response schema with error codes, messages, and request IDs.
10/10: Applies Hyrum's Law when designing (build only what you commit to). Designs for backward compatibility from day one (required fields must have defaults, never remove fields). Explains consumer-driven contract testing. Designs the full API surface: pagination (cursor vs offset), rate limiting headers, partial update (PATCH vs PUT), and idempotency key mechanics with deduplication window.
The API Design Checklist
REST resource modeling: nouns, not verbs
Resources should be nouns: /orders, /users, /products. Actions are expressed via HTTP methods: GET (read), POST (create), PUT (replace), PATCH (partial update), DELETE (remove). Anti-pattern: /createOrder, /getUser, /deleteProduct (verbs in URLs). Exception: complex operations that don't map to CRUD (e.g., /orders/{id}/cancel) — use POST to a sub-resource action noun.
Idempotency: design for safe retries
GET, PUT, DELETE are inherently idempotent (calling them N times has the same effect as calling once). POST creates a new resource — not inherently idempotent (two POSTs create two orders). For idempotency on POST: require clients to supply an idempotency_key header (UUID). Store (idempotency_key, response) on first call. On duplicate request: return stored response without processing. Deduplication window: 24 hours typical. This enables clients to safely retry on network failure.
Versioning: plan before you ship v1
Three strategies: (1) URI versioning: /v1/users, /v2/users — most visible, easiest to route, cached independently. (2) Header versioning: Accept: application/vnd.api+json;version=2 — cleaner URIs, harder to curl/debug. (3) Query param: /users?version=2 — simplest but easily omitted. URI versioning is the most practical. Rule: v1 is forever. Don't break v1 when shipping v2 — maintain in parallel until v1 deprecation window (6-24 months) passes.
Error contracts: every error has a contract
Standard error response: { error_code: 'ORDER_NOT_FOUND', message: 'Order abc123 does not exist.', request_id: 'req_xyz789', timestamp: '2024-01-15T10:23:45Z', details: {} }. error_code is machine-readable (clients can switch on it). message is human-readable (developers can read it in logs). request_id links to distributed traces. HTTP status code aligns: 404 for ORDER_NOT_FOUND, 422 for validation errors, 429 for rate limit exceeded, 503 for dependency unavailable.
Backward compatibility: never break clients
Safe changes (backward compatible): add new optional fields, add new endpoints, add new enum values (clients must handle unknown values), relax validation. Breaking changes: remove or rename fields, change field types, change error codes, remove enum values, make optional fields required. Strategy: deprecate first (add Deprecation header), run deprecated version for 6+ months, remove only after all clients have migrated. Additive-only changes to the data contract.
Idempotency Key Flow: Safe Payment Retry
Pagination, Rate Limiting & Error Handling Templates
Cursor-based Pagination (preferred over offset):
GET /orders?cursor=eyJpZCI6MTIzfQ==&limit=20
Response:
{
"orders": [...],
"pagination": {
"next_cursor": "eyJpZCI6MTQzfQ==",
"has_more": true,
"total_count": null // optional; expensive to compute
}
}
Cursor encodes the last seen row's sort key (Base64-encoded {"id": 123}). Stable under concurrent inserts — offset pagination shows duplicates/skips when rows are inserted between pages. Cursor pagination is O(1) per page; offset is O(offset) per page.
Rate Limiting Response Headers:
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1705334625 (Unix timestamp when quota resets)
Retry-After: 60 (seconds until retry)
Content-Type: application/json
{
"error_code": "RATE_LIMIT_EXCEEDED",
"message": "Rate limit of 1000 requests per minute exceeded.",
"request_id": "req_abc123"
}
Idempotent PUT for replace, PATCH for partial update:
// PUT replaces the entire resource (all fields required):
PUT /users/123
{"name": "Alice", "email": "alice@example.com", "role": "admin"}
// PATCH updates only provided fields (partial update):
PATCH /users/123
{"email": "alice@newdomain.com"} // only email changes
// JSON Patch (RFC 6902) — explicit operations:
PATCH /users/123
[{"op": "replace", "path": "/email", "value": "alice@newdomain.com"}]
Versioned Error Codes (machine-readable, stable):
// Good — stable machine-readable code + human message:
{"error_code": "PAYMENT_INSUFFICIENT_FUNDS", "message": "..."}
// Bad — HTTP status only; 422 means different things in different APIs:
HTTP 422 Unprocessable Entity
{"error": "invalid input"} // client can't distinguish between types
// Bad — string message as the contract (Hyrum's Law will bite you):
{"error": "The order was not found"} // clients will parse this string
HTTP Method Semantics and Properties
| Method | Idempotent? | Safe? | Typical use | Response code |
|---|---|---|---|---|
| GET | Yes | Yes (no side effects) | Read resource or collection | 200 OK, 404 Not Found |
| POST | No | No | Create resource, trigger action | 201 Created, 202 Accepted (async) |
| PUT | Yes | No | Replace entire resource | 200 OK, 204 No Content |
| PATCH | Not guaranteed | No | Partial update of resource | 200 OK, 204 No Content |
| DELETE | Yes | No | Remove resource (2nd DELETE = 404, same outcome) | 204 No Content, 404 if already deleted |
| HEAD | Yes | Yes | Same as GET but no body (check existence, headers) | 200 or 404 |
| OPTIONS | Yes | Yes | Discover allowed methods, CORS preflight | 200 with Allow header |
Common API Design Mistakes
1. Chatty APIs. Client makes 10 API calls to render one screen. Fix: composite endpoints that return all data needed for a screen in one request. Or GraphQL for flexible client-driven queries.
2. Returning internal IDs directly. Exposing auto-increment database IDs (/users/42) reveals row count and enables enumeration attacks. Prefer UUIDs (/users/usr_f47ac10b-58cc-4372-a567). UUIDs also enable database migration without changing external IDs.
3. Missing idempotency on writes. A mobile app retries a failed payment → double charge. Any mutating operation that's exposed to unreliable networks (mobile, microservices) needs idempotency.
4. Unstable sort order in paginated results. Paginating by ORDER BY created_at fails when multiple records have the same timestamp (ties are broken non-deterministically). Always add a tiebreaker: ORDER BY created_at DESC, id DESC. Cursor-based pagination requires a stable, unique sort key.
5. Returning 200 for errors. {"status": "error", "code": 404} with HTTP 200. Clients can't use standard HTTP error handling, monitoring tools miss errors, and retry logic breaks. Use proper HTTP status codes.
6. Adding required fields to existing endpoints. Making a previously optional field required in PUT is a breaking change. Existing clients without that field will suddenly fail. Deprecate the old shape, add the new required field as optional first, then make it required only in v2.
Interview Delivery Summary
When asked to "design the API" for a system: first model the resources (nouns), then define the methods per resource (CRUD + action sub-resources for complex operations). Address idempotency on POST endpoints explicitly: "For the payment endpoint, I'd require an Idempotency-Key header — the server stores the result keyed on this, so retries are safe."
Version from day one: "I'd version the API as /v1/ from the start, even if there's no v2 yet — it costs nothing now and saves a painful migration later."
Staff signal: bring up Hyrum's Law: "Once we expose a field, clients will depend on it. We should only include what we're committing to maintain. I'd start with a minimal surface and expand rather than expose everything and try to remove later."