Back to Notes

API Design Principles

Tested in system design rounds, backend interviews, and "design this endpoint" questions.

REST Constraints (verbal must-know)

ConstraintWhat it means
StatelessServer holds no client session; every request carries all context (JWT in header)
Uniform interfaceResources identified by URL, standard HTTP verbs, consistent response format
Client-serverUI and backend evolve independently
CacheableGET responses should be cacheable via Cache-Control headers
Layered systemClient doesn't know if it's hitting the origin server or a load balancer/proxy in front of it

HTTP Verbs + Idempotency

VerbUseIdempotentSafe
GETFetch resource✅✅
POSTCreate resource❌❌
PUTReplace entire resource✅❌
PATCHPartial update❌ (usually)❌
DELETERemove 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

  1. "How would you design a paginated API for a feed?" — Cursor-based; return next_cursor; client sends it back on the next call.
  2. "How do you make a payment API idempotent?" — Idempotency-Key header, store key+result in a fast store (Redis) with a TTL.
  3. "What's the difference between 401 and 403?" — 401: not authenticated. 403: authenticated, not authorized.
  4. "How do you version an API without breaking clients?" — URL path versioning; keep v1 alive, deprecate with a sunset header and migration docs.

Cross-links

[[API Gateway]] · [[Rate Limiter]] · [[Communication Protocols]]