API Design Principles
API Design Principles
Tested in: System Design rounds, backend interviews, "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. Use Cache-Control headers. |
| Layered system | Client doesn't know if it's hitting server or load balancer. |
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 = same request N times = same result as 1 time.
Why POST isn't idempotent: calling POST /orders twice creates two orders.
Idempotency key for POST (fintech critical):
POST /payments
Idempotency-Key: uuid-here
Server stores key + result. Duplicate request returns same result, doesn't charge 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 — verb in URL (RPC style, not REST)
GET /getUserOrders
POST /createOrder
DELETE /deleteOrder
# Query params for filtering/sorting (not 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 (Pydantic)
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 auth). 403 = "I know who you are, you can't do this."
Pagination
Offset pagination (simple, but slow at scale)
GET /orders?offset=1000&limit=20
→ SQL: SELECT * FROM orders LIMIT 20 OFFSET 1000
Problem: OFFSET 1000 scans and discards 1000 rows. Gets slower with large offsets.
Cursor pagination (senior answer — O(1) per page)
GET /orders?cursor=eyJpZCI6MTAwfQ&limit=20
→ SQL: SELECT * FROM orders WHERE id > {decoded_cursor} LIMIT 20
Response: { "data": [...], "next_cursor": "eyJpZCI6MTIwfQ" }
Pros: consistent, fast, no duplicates if data inserts between pages
Cons: can't jump to arbitrary page
Use cursor for: feeds, activity logs, large datasets. Use offset for: admin UIs where user jumps to page 50.
Versioning Strategies
# URL path versioning (most common, easy to route)
GET /v1/users
GET /v2/users
# Header versioning (cleaner URLs, harder to test in browser)
GET /users
Accept-Version: v2
# Query param (avoid — caching issues)
GET /users?version=2
URL path versioning is the standard. Use it unless you have a reason not to.
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 debugging in production — correlates logs, traces, and user reports.
Authentication Patterns
JWT (most common in interviews)
Header: Authorization: Bearer <token>
Token = base64(header) + "." + base64(payload) + "." + signature
Payload: { "user_id": 123, "exp": 1716000000, "roles": ["admin"] }
Stateless — server verifies signature, no DB lookup needed.
Expiry: short-lived (15 min access token) + long-lived refresh token (7 days).
API Key (service-to-service)
Header: X-API-Key: your-key-here
Or query param: ?api_key=... (avoid — appears in logs)
OAuth 2.0 (third-party auth — "Login with Google")
Authorization Code flow:
1. User redirected to Google
2. Google redirects back with code
3. Backend exchanges code for access token
4. Backend gets user info from Google
Rate Limiting Headers (standard)
X-RateLimit-Limit: 1000 # max requests per window
X-RateLimit-Remaining: 950 # remaining in current window
X-RateLimit-Reset: 1716000000 # UTC epoch when window resets
Retry-After: 30 # seconds until retry (on 429)
Interview Questions to Prepare
-
"How would you design a paginated API for a feed?" → Cursor-based, return next_cursor, client sends cursor on next call
-
"How do you make a payment API idempotent?" → Idempotency-Key header, store key+result in Redis with 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 versioning. Keep v1 alive. Deprecate with sunset header + migration docs.
-
"How would you design rate limiting for a public API?" → Token bucket per API key in Redis. Return 429 with Retry-After. See [[System Design/Problem Designs/Rate Limiter]]
Related
- [[System Design/Problem Designs/Rate Limiter]] — rate limiting implementation
- [[API Gateway]] — gateway-level concerns
- [[Python/Libraries/FastAPI]] — FastAPI implements these patterns