API Design Principles
Tested in system design rounds, backend interviews, and "design this endpoint" questions.
REST Constraints (verbal must-know)
| Constraint | What it means |
|---|---|
| Stateless | Server holds no client session; every request carries all context (JWT in header) |
| Uniform interface | Resources identified by URL, standard HTTP verbs, consistent response format |
| Client-server | UI and backend evolve independently |
| Cacheable | GET responses should be cacheable via Cache-Control headers |
| Layered system | Client doesn't know if it's hitting the origin server or a load balancer/proxy in front of it |
HTTP Verbs + Idempotency
| Verb | Use | Idempotent | Safe |
|---|---|---|---|
| GET | Fetch resource | ✅ | ✅ |
| POST | Create resource | ❌ | ❌ |
| PUT | Replace entire resource | ✅ | ❌ |
| PATCH | Partial update | ❌ (usually) | ❌ |
| DELETE | Remove resource | ✅ | ❌ |
Idempotent = calling it N times has the same effect as calling it once. Why POST isn't idempotent: POST /orders twice creates two orders.
Idempotency key for POST (critical in fintech):
POST /payments
Idempotency-Key: uuid-here
Server stores the key + result; a duplicate request with the same key returns the stored result instead of charging twice.
URL Design
# Good — noun-based resources, hierarchy
GET /users/{id}
GET /users/{id}/orders
POST /users/{id}/orders
DELETE /users/{id}/orders/{orderId}
# Bad — verbs in the URL (RPC style, not REST)
GET /getUserOrders
POST /createOrder
# Query params for filtering/sorting, never in the path
GET /orders?status=active&sort=created_at&order=desc
GET /orders?page=2&limit=20
HTTP Status Codes (must know cold)
2xx Success:
200 OK — GET, PUT, PATCH success
201 Created — POST success (include Location header)
204 No Content — DELETE success, no body
4xx Client errors:
400 Bad Request — malformed request, invalid params
401 Unauthorized — not authenticated (no/invalid token)
403 Forbidden — authenticated but no permission
404 Not Found — resource doesn't exist
409 Conflict — duplicate (email already registered)
422 Unprocessable — valid JSON but fails validation
429 Too Many Req — rate limited
5xx Server errors:
500 Internal Error — unexpected server error
502 Bad Gateway — upstream service failed
503 Unavailable — server overloaded or down
504 Gateway Timeout — upstream timed out
401 vs 403: 401 = "who are you?" (no/invalid auth). 403 = "I know who you are, you can't do this."
Pagination
Offset pagination (simple, slow at scale):
GET /orders?offset=1000&limit=20
→ SELECT * FROM orders LIMIT 20 OFFSET 1000
OFFSET 1000 scans and discards 1000 rows — gets slower with larger offsets.
Cursor pagination (senior answer — O(1) per page):
GET /orders?cursor=eyJpZCI6MTAwfQ&limit=20
→ SELECT * FROM orders WHERE id > {decoded_cursor} LIMIT 20
Response: { "data": [...], "next_cursor": "eyJpZCI6MTIwfQ" }
Consistent, fast, no duplicates even if rows are inserted between page fetches — but can't jump to an arbitrary page number. Use cursor pagination for feeds/activity logs/large datasets; offset pagination for admin UIs where a user needs to jump to page 50.
Versioning Strategies
GET /v1/users # URL path versioning — standard, easy to route, use unless you have a reason not to
GET /users Accept-Version: v2 # header versioning — cleaner URLs, harder to test in a browser
GET /users?version=2 # query param — avoid, breaks caching
Error Response Format
Always consistent — never return HTML for errors in an API.
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid email format",
"field": "email",
"request_id": "req_abc123"
}
}
request_id is critical for production debugging — it correlates logs, traces, and user bug reports.
Authentication Patterns
JWT (most common in interviews):
Authorization: Bearer <token>
Token = base64(header) + "." + base64(payload) + "." + signature
Payload: { "user_id": 123, "exp": 1716000000, "roles": ["admin"] }
Stateless — server verifies the signature, no DB lookup needed. Typical expiry: short-lived access token (15 min) + long-lived refresh token (7 days).
API Key (service-to-service): X-API-Key: your-key-here in a header (never as a query param — it ends up in logs).
OAuth 2.0 (third-party auth, "Login with Google"): authorization-code flow — user redirected to provider, provider redirects back with a code, backend exchanges the code for an access token, backend fetches user info with that token.
Rate Limiting Headers (standard)
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 950
X-RateLimit-Reset: 1716000000
Retry-After: 30
Full mechanism in [[Rate Limiter]].
Interview Drills
- "How would you design a paginated API for a feed?" — Cursor-based; return
next_cursor; client sends it back on the next call. - "How do you make a payment API idempotent?" —
Idempotency-Keyheader, store key+result in a fast store (Redis) with a TTL. - "What's the difference between 401 and 403?" — 401: not authenticated. 403: authenticated, not authorized.
- "How do you version an API without breaking clients?" — URL path versioning; keep
v1alive, deprecate with a sunset header and migration docs.
Cross-links
[[API Gateway]] · [[Rate Limiter]] · [[Communication Protocols]]