A practitioner's reference for designing scalable, maintainable, and battle-tested APIs — the way interviewers expect.
Choosing the right API style is the first signal your interviewer looks for. Each paradigm has a distinct sweet spot — knowing the tradeoffs cold is non-negotiable.
Resource-oriented architecture over HTTP. Leverages standard methods and status codes. Stateless by design.
Query language for your API. Clients declare exactly what data they need in a single request.
High-performance RPC using Protocol Buffers over HTTP/2. First-class streaming support.
GET /v1/users/{id} # Get a user
POST /v1/users # Create user
PUT /v1/users/{id} # Full replace
PATCH /v1/users/{id} # Partial update
DELETE /v1/users/{id} # Delete
# Nested resources — keep depth ≤ 2
GET /v1/users/{id}/orders # OK
GET /v1/orders?userId={id} # Better for deep nesting
Breaking changes are inevitable. How you version determines how much pain you cause your consumers and yourself.
| Strategy | Example | Pros | Cons | Best for |
|---|---|---|---|---|
| URL Path | /v1/users |
Explicit, cacheable, easy routing | URL proliferation | Public REST APIs |
| Header | API-Version: 2024-01 |
Clean URLs, date-based | Harder to test in browser | Stripe-style APIs |
| Query Param | ?version=2 |
Easy to test | Cache pollution, easily missed | Internal / beta APIs |
| Content Type | Accept: application/vnd.api.v2+json |
Semantically correct | Very verbose, low adoption | Rarely used in practice |
Stripe uses date-based header versioning and pins each API key to the version active at creation. Users upgrade explicitly. This is widely considered best-in-class — mention it when discussing public APIs.
Every list endpoint needs pagination. The strategy you pick has deep implications for performance, consistency, and client complexity.
| Strategy | Mechanism | Scale | Consistency | When to use |
|---|---|---|---|---|
| Offset | ?offset=20&limit=10 |
Poor | Unstable | Admin dashboards, small datasets |
| Cursor | ?after=eyJpZCI6MTIzfQ |
Excellent | Stable | Feeds, infinite scroll, large datasets |
| Page Token | ?pageToken=Abc123XY |
Good | Stable | Google APIs pattern, opaque tokens |
| Time-based | ?since=2024-01-01T00:00Z |
Excellent | Depends | Event logs, audit trails, webhooks |
{
"data": [...],
"pagination": {
"cursor": "eyJpZCI6MTAwfQ==", // base64({"id":100})
"has_more": true,
"total": null // omit for perf — avoid COUNT(*)
}
}
// Next page request
GET /v1/messages?after=eyJpZCI6MTAwfQ==&limit=20
Offset pagination is O(offset) in most databases — OFFSET 10000 still scans 10,000 rows. For any high-traffic or large dataset, always recommend cursor-based. The cursor is typically an encoded primary key or (timestamp, id) composite.
// Header
{ "alg": "RS256", "typ": "JWT" }
// Payload — keep small (network overhead)
{
"sub": "user_abc123",
"iss": "auth.example.com",
"aud": "api.example.com",
"exp": 1735689600, // 15 min from now
"scope": "read:users write:orders"
}
// Never put PII or secrets in JWT payload — it's only base64 encoded
| Algorithm | How it Works | Burst? | Complexity |
|---|---|---|---|
| Fixed Window | Count resets each window (e.g., per minute) | Edge bursts possible | O(1) Redis INCR |
| Sliding Window | Log timestamps in a sorted set, count within window | Smooth | O(N) memory |
| Token Bucket | Tokens refill at constant rate, consumed per request | Allows bursts | O(1) state |
| Leaky Bucket | Requests queue and drain at fixed rate | No bursts | O(1) state |
# Always return these headers so clients can back off gracefully
X-RateLimit-Limit: 1000 # requests per window
X-RateLimit-Remaining: 847 # remaining this window
X-RateLimit-Reset: 1735689600 # epoch when window resets
Retry-After: 60 # seconds (on 429)
# When limit exceeded:
HTTP/1.1 429 Too Many Requests
{
"error": {
"code": "VALIDATION_ERROR", // machine-readable
"message": "Invalid email format", // human-readable
"field": "email", // optional: field context
"request_id": "req_7Xk3mN9pL", // for tracing/support
"docs_url": "https://api.example.com/errors/VALIDATION_ERROR"
}
}
// HTTP Status Codes — use them correctly
200 OK | 201 Created | 204 No Content
400 Bad Request | 401 Unauthorized | 403 Forbidden
404 Not Found | 409 Conflict | 422 Unprocessable
429 Rate Limited| 500 Server Error | 503 Unavailable
Network failures are inevitable. Clients retry. Your API must handle duplicate requests safely — especially for writes and financial operations.
// Client generates a unique key per "logical operation"
POST /v1/payments
Idempotency-Key: k1_2Xm9pL8nQ7rT4vF // client-generated UUID
{
"amount": 5000,
"currency": "usd"
}
// Server behavior:
// 1. Hash the key, check cache/DB
// 2. If found → return stored response (no re-execution)
// 3. If not found → process, store result with key (TTL: 24h)
// 4. Return same response for all retries
// GET, HEAD, OPTIONS = naturally idempotent
// PUT, DELETE = idempotent by spec
// POST, PATCH = require explicit handling
Signals that separate mid-level answers from staff-level answers in system design.
Ask who the consumers are (browser, mobile, microservices?), expected scale (QPS), and SLA requirements. Never jump to REST vs gRPC without this.
Use /payments, not /createPayment. Actions are HTTP methods. Exception: long-running operations like /payments/{id}/cancel are acceptable.
Proactively mention idempotency keys, retry headers, exponential backoff guidance, and circuit breaker patterns. Shows operational maturity.
An API is a contract. Distinguish breaking vs additive changes. Propose a deprecation strategy (sunset headers, migration guides, dual-running versions).
Always include request IDs in responses. Discuss distributed tracing (trace-id propagation), structured logs, and rate limit telemetry dashboards.
HTTPS everywhere, input validation at the edge, short-lived JWTs, CORS policies, and API key rotation. Name these unprompted — most candidates don't.