RESTful Resource Modeling
Resources are nouns, not verbs. POST /orders (create), GET /orders/{id} (read), PUT /orders/{id} (replace), PATCH /orders/{id} (partial update), DELETE /orders/{id}. Nest resources only one level deep: GET /orders/{id}/items (items under an order). Avoid: GET /getOrder, POST /createUser. Status codes: 200 OK, 201 Created (POST), 204 No Content (DELETE), 400 Bad Request (validation error), 401 Unauthorized, 403 Forbidden, 404 Not Found, 409 Conflict (duplicate), 422 Unprocessable Entity, 429 Too Many Requests, 500 Internal Server Error. Use plural nouns consistently: /users, /orders, /products.
Versioning
API versioning strategies: (1) URL path: /api/v1/users, /api/v2/users. Simple, explicit, easy to route. Most common. (2) Header: Accept: application/vnd.api+json;version=2. Cleaner URLs but harder to test in browsers. (3) Query parameter: GET /users?version=2. Easy to add but pollutes query strings. Recommendation: use URL versioning for public APIs. Increment major version on breaking changes (removed fields, changed types, new required parameters). Minor changes (added optional fields, new endpoints) are backward-compatible — no version bump. Maintain at least two major versions simultaneously; deprecate the old with a Sunset header and a 6-12 month timeline.
Pagination
Offset-based: GET /orders?offset=100&limit=20. Simple but inconsistent with concurrent inserts/deletes (item can be skipped or duplicated). Cursor-based: GET /orders?cursor=eyJpZCI6MTAwfQ&limit=20. The cursor encodes the position (e.g., base64-encoded last_id). Consistent under concurrent modifications. Used by Twitter, Facebook, Stripe. Keyset pagination: GET /orders?after_id=100&limit=20 — simpler cursor using a natural key. For large datasets, always prefer cursor-based. Response envelope: {data: […], next_cursor: “…”, has_more: true}. Return total count only when cheap (avoid COUNT(*) on large tables — use approximate counts from statistics).
Rate Limiting
Algorithms: (1) Token bucket: bucket holds N tokens, refilled at R tokens/second. Each request consumes one token. Allows bursts up to N. Implemented with Redis: DECR token_count per request, periodic refill job. (2) Fixed window: allow N requests per minute window. Simple but allows 2N requests at window boundaries. (3) Sliding window log: log all request timestamps, count requests in the last 60 seconds. Accurate but memory-intensive. (4) Sliding window counter: hybrid — combine fixed window counts with a weighted proportion of the previous window. Good balance. Return 429 with Retry-After header. Headers: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset. Tier rate limits by API key (free: 100/min, pro: 1000/min, enterprise: custom).
Request and Response Design
Request validation: validate types, required fields, string lengths, enum values at the API gateway or controller layer. Return 400 with structured error: {error: “VALIDATION_FAILED”, details: [{field: “email”, message: “Invalid format”}]}. Never expose stack traces in production responses. Idempotency: support Idempotency-Key header for POST requests that create resources. Store (key, response) and return the cached response on duplicate requests. Response envelope: {data: {…}, meta: {request_id: “…”}}. Always include a request_id for distributed tracing. Use consistent timestamp format: ISO 8601 (2024-01-15T10:30:00Z). Use cents for money (never floats).
GraphQL vs REST
REST: resource-oriented, multiple endpoints, over-fetching (fixed response shape), under-fetching (multiple requests for related data). Simple, cacheable (GET is cacheable by URL), easy to document with OpenAPI. GraphQL: single endpoint, client specifies exact fields needed (no over-fetching), fetch related data in one query. Complex query validation, harder to cache (POST queries), requires query depth limiting to prevent expensive queries. Use GraphQL when: clients have diverse data needs (mobile vs web vs partner), frequent UI changes require different data shapes, avoiding multiple round trips matters. Use REST when: simplicity, cacheability, and tooling maturity matter most. Most public APIs use REST; internal/product APIs often benefit from GraphQL.
Interview Tips
- Always discuss authentication (OAuth 2.0, API keys, JWT) and authorization (scopes, RBAC) for the API.
- Idempotency keys for write operations prevent double-charges and duplicate records on retry.
- Discuss backwards compatibility — adding optional fields is safe; removing or renaming fields is breaking.
- OpenAPI/Swagger spec as a contract enables auto-generated SDKs and mock servers.
{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “What is the difference between cursor-based and offset-based pagination?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Offset-based pagination (LIMIT/OFFSET) is simple but has correctness issues under concurrent writes: if a new item is inserted on page 1 while the user is on page 2, they will see a duplicate item shifted from page 1. If an item is deleted, they will skip an item. Cursor-based pagination uses a pointer to the last-seen item (usually the ID or a composite key). GET /items?after_id=100&limit=20 returns items with id > 100. Stable under concurrent modifications — the cursor encodes an absolute position. Limitation: cannot jump to page 5; can only go forward (or backward with a before_id cursor). Used by Stripe, Twitter, and Facebook for feeds and transaction lists. Keyset pagination is the same concept with natural keys.”
}
},
{
“@type”: “Question”,
“name”: “How does the token bucket rate limiting algorithm work?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “A token bucket holds up to N tokens (bucket capacity). Tokens are added at a rate of R tokens/second. Each request consumes one token. If the bucket is empty, the request is rejected (429 Too Many Requests). The bucket allows bursts up to N requests, then enforces the steady-state rate R. Implementation with Redis: store (token_count, last_refill_timestamp) per API key. On each request: compute tokens to add since last refill (min(N, current_tokens + elapsed_seconds * R)). If token_count >= 1: decrement and allow. Else: reject. Use a Lua script in Redis for atomic read-modify-write. Compare to fixed window (simpler but allows 2x burst at boundaries) and sliding window log (accurate but memory-intensive).”
}
},
{
“@type”: “Question”,
“name”: “What are the key considerations for backward-compatible API changes?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Backward-compatible (non-breaking) changes: adding new optional fields to responses, adding new optional request parameters, adding new endpoints, adding new enum values (risky if clients switch on all enum values). Breaking changes: removing fields from responses, renaming fields, changing field types, making optional fields required, removing enum values, changing HTTP methods for an endpoint. Strategy: treat your API as a contract. Once a field is in a response, clients may depend on it — never remove it without a version bump and deprecation period. For deprecations: add Deprecation and Sunset headers (per RFC 8594) to deprecated endpoints. Log usage of deprecated endpoints to identify clients still using them before removal.”
}
},
{
“@type”: “Question”,
“name”: “How do you design idempotent API endpoints?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Idempotent endpoints can be called multiple times with the same parameters and produce the same result. GET, PUT, DELETE are inherently idempotent by HTTP spec. POST is not — calling POST /orders twice creates two orders. Make POST idempotent by requiring an Idempotency-Key header (a UUID generated by the client per logical operation). On receipt: check if a response for this key exists in the store (Redis or DB). If yes: return the stored response. If no: process the request, store the (key, response), return the response. Storage TTL: 24-48 hours (long enough for client retry windows). Include the idempotency_key in the response so clients can verify. Stripe uses this pattern for all payment endpoints.”
}
},
{
“@type”: “Question”,
“name”: “When should you use GraphQL instead of REST?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “GraphQL is preferred when: (1) Clients have heterogeneous data needs — mobile needs less data than web, partner APIs need different fields. REST returns fixed shapes; GraphQL lets each client request exactly what it needs. (2) The API has a complex, interconnected graph of resources requiring multiple REST round trips. GraphQL fetches related data in one query. (3) Rapid frontend iteration — adding new fields does not require backend changes, just querying them. REST is preferred when: (1) Simplicity matters — REST is easier for third-party developers to understand. (2) HTTP caching is important — GraphQL POST requests are not cacheable by URL. (3) File uploads are needed (REST handles multipart better). (4) The API has a small, stable set of well-defined resources. Many companies use both: REST for public APIs, GraphQL for internal product APIs.”
}
}
]
}
Asked at: Stripe Interview Guide
Asked at: Cloudflare Interview Guide
Asked at: Atlassian Interview Guide
Asked at: Shopify Interview Guide