API Design
Good API design is the difference between a system developers love and one they tolerate. The principles: RESTful resource modeling, clear versioning, sensible pagination, consistent error handling, and idempotency for safety. A good API is consistent (same conventions everywhere), predictable (you can guess endpoints), and forgiving (idempotent so retries are safe). A bad API is the opposite — surprising, inconsistent, and dangerous to retry.
How it works
Good API design rests on five pillars:
- Resource modeling: model the API around nouns (resources), not verbs (actions).
/users,/orders,/orders/{id}/items— not/createOrderor/getUserData. - Versioning: APIs change. Version them from day one (
/v1/...) so you can introduce breaking changes without breaking existing clients. - Pagination: list endpoints must paginate. Returning 100k records in one response is broken.
- Error handling: consistent, structured error responses with meaningful codes and messages.
- Idempotency: write operations should be safe to retry. POST without idempotency is a bug waiting to happen.
REST (Representational State Transfer) is the dominant style. It maps HTTP verbs to actions: GET (read), POST (create), PUT/PATCH (update), DELETE (delete). The URL identifies the resource; the verb identifies the action.
API versioning is non-negotiable for public APIs. Three approaches:
- URI versioning (
/v1/users): the most common and the most explicit. Clients see the version in every request; routing is trivial. Downside: version in URL is "ugly" to some and breaks the principle that URLs are resource identifiers. - Header versioning (
Accept: application/vnd.api+json; version=1): cleaner URLs, version is a content negotiation. Downside: invisible in logs and tests; harder to route. - Query parameter (
/users?version=1): easy but mixes version with filtering. Generally discouraged.
The Stripe / GitHub / Twitter pattern: URI versioning. The cost of "ugly" URLs is far less than the cost of invisible versions. Most teams pick URI versioning and never look back.
When to bump the version: any breaking change. New endpoints, new fields, new optional parameters — no version bump needed. Removing or renaming fields, changing types, changing semantics — version bump. Maintain old versions for a deprecation window (6-24 months) so clients can migrate.
List endpoints must paginate. Three styles:
- Offset/limit (
?offset=100&limit=20): the simplest. SQL-friendly. Problem: skipping is O(N) in many databases, and items shift if data changes between pages. - Cursor-based (
?cursor=abc123&limit=20): the cursor encodes the position (typically the last seen ID + sort key). Stable across inserts/deletes; O(1) skip in databases. The right choice for most APIs. - Page-based (
?page=5&per_page=20): user-friendly for UI but has the same problems as offset. Use only for low-volume, stable data.
Return pagination metadata in the response: next_cursor, has_more, total (if cheap to compute). Don't make clients guess whether there's another page.
Rate-limit list endpoints to reasonable page sizes (e.g., max 100 per page). A ?limit=1000000 request is a denial-of-service vector.
Bad: 500 Internal Server Error with body "something went wrong." Good: a structured response with a stable error code, a human-readable message, and a request ID for support. Use HTTP status codes correctly (200 success, 4xx client error, 5xx server error). Don't return 200 with an error body — that breaks every HTTP client and framework. Include a request ID in every response so support can correlate. Stripe's error format is the gold standard: {type, code, message, param, request_id}. Pick a format and apply it everywhere — inconsistency is the #1 API complaint.
Idempotency makes APIs safe to retry — critical because networks fail and clients retry. HTTP spec defines GET, PUT, DELETE as idempotent; POST is not. But many POSTs should be idempotent (creating a charge, sending a message).
The fix is the Idempotency-Key header (Stripe's pattern, now widely adopted):
- Client generates a UUID per logical operation.
- Sends
Idempotency-Key: <uuid>on the POST. - Server stores the key + response for 24-48 hours.
- If the same key is sent again, server returns the stored response (not a new operation).
This makes POST safe to retry. Without it, a network timeout after the server created the charge but before the client received the response means a retry creates a duplicate. With it, the retry returns the original charge.
For non-idempotent operations (truly unique actions like "spin a slot machine"), don't accept an idempotency key — but document this clearly so clients know not to retry.
A client calls `POST /v1/charges` to charge $100. The server charges the card but the response is lost due to a network timeout. The client retries. What happens without idempotency, and how does an idempotency key fix it?
Pick one answer.
Which is the better RESTful design for an API that lists a user's orders?
Pick one answer.
Why is cursor-based pagination better than offset/limit for a list endpoint that receives frequent inserts?
Pick one answer.
Engineering mental model
Mental model. Think of API Design as a deliberate boundary in a system. The boundary exists because something becomes harder to manage when everything is done in one place: latency, scale, failure isolation, consistency, cost, or team ownership. The useful question is not “what does API Design mean?” but “what pressure makes this boundary worth introducing, and what new failure mode does it create?”
Before choosing API Design, name the workload, the critical user path, the dominant bottleneck, the failure you are trying to absorb, and the trade-off you are willing to accept. If you cannot name those five things, the design is probably premature.
curl -i https://api.example.com/v1/api-design
# Look for:
# - status code
# - latency
# - retryability
# - response sizeBack-of-the-envelope reasoning
Numerical lens: write down traffic, payload size, read/write ratio, peak multiplier and durability target before choosing a component. The numbers should justify the architecture.
Interactive thought experiment: API Design
Change the variables below and predict what breaks first in API Design. The production lab can later reuse these same inputs.
Change one variable at a time. Predict the failure mode first, then move the slider and see whether your mental model matches the simplified system response.
If you are stuck on API Design, start by drawing the request path and marking every network hop, stateful component, queue, cache and failure boundary. Then estimate where the system will saturate.
You increase traffic by 10× in a system using API Design. What should you inspect first?
Pick one answer.
Which statement is the safest engineering habit when using API Design?
Pick one answer.
You have dashboards for traffic, latency, errors and saturation. You can change the architecture, but every change has operational cost.
Production scenario: your system uses API Design, traffic suddenly spikes, and p99 latency doubles. What is your first move?
Interview drill
Answer this without notes: When would you choose API Design, and when would you intentionally avoid it? Mention at least one bottleneck it addresses, one failure mode it introduces, and one alternative. Then quantify the workload you are designing for.
A useful engineering lens for API Design: define the problem it solves, the simpler design that fails first, the constraint that forces you to introduce this concept, and the new failure modes the concept creates.
Numerical sanity check
Back-of-the-envelope reasoning beats fake precision. State your traffic, payload, concurrency and growth assumptions explicitly, then calculate enough to know whether the current architecture is orders of magnitude away from the target.
Do not optimize for a memorized definition. Reason from the workload and failure mode.
Imagine the simplest version of a system using API Design. What breaks first as traffic grows by 10×, and what would you change before reaching 100×?
Pick one answer.
What you gain, what you pay
- +Consistent, predictable APIs are easy to integrate and maintain.
- +Versioning lets APIs evolve without breaking clients.
- +Idempotency makes APIs safe to retry — essential for reliability.
- +Structured errors make debugging fast.
- −REST doesn't fit every domain (complex operations need RPC or GraphQL).
- −Versioning adds operational complexity (multiple versions to support).
- −Cursor pagination is harder for clients than offset/limit.
- −Idempotency requires server-side state and key management.
How this breaks in production
- Non-idempotent POSTs that double-execute on retry.
- No versioning — breaking changes break every client.
- Returning 200 with error bodies — breaks HTTP clients and frameworks.
- Unpaginated list endpoints that return 100k records in one response.
Don't fall into these traps
- •Using verbs in URLs (RPC style) instead of resources (REST style).
- •Not versioning from day one — retroactive versioning is painful.
- •Returning 200 with an error body instead of the correct 4xx/5xx status.
- •Forgetting idempotency on POST — leading to duplicate charges/sends on retry.
Real systems using this
How real systems implement this
- Stripe API — The reference for good API design: URI versioning (`/v1/charges`), Idempotency-Key header for safe retries, structured errors with request IDs, cursor-based pagination. Their docs are also a model — every endpoint has examples and edge cases.
- GitHub REST API — RESTful resource modeling (`/users/{user}/repos`), consistent pagination via Link headers, ETag-based caching. The pattern most public APIs now follow.
Practice saying it out loud
- Q1Design a RESTful API for an e-commerce system. What endpoints, verbs, and resources?
- Q2How do you make POST requests safe to retry?
- Q3Compare offset/limit and cursor pagination. When would you use each?
- Q4When should you bump the API version?
Further reading & references
Core explanations are original NO CAP material. External references are provided for deeper study and standards.
What next?
Mark as understood once the mental model clicks.
Next recommended
REST — Representational State Transfer