Designing the API
In one line
A handful of well-chosen endpoints, with clear resources, pagination, idempotency and the right protocol, fixes the contract before you design the internals.
What it is
In a design interview the API step is a short list of the main operations clients call: method, path, key inputs and outputs. It turns functional requirements into a contract. Three to six endpoints are usually enough; you are not writing a full OpenAPI spec.
The interesting decisions are not the URL names. They are: which protocol (REST, gRPC, WebSocket, server-sent events, webhooks), how lists are paginated, how retries are made safe (idempotency), what is synchronous versus accepted for later processing, and how errors and limits are reported.
Public and mobile clients usually get REST or GraphQL over HTTPS. Internal service-to-service calls often use gRPC for typed contracts and lower overhead. Real-time pushes use WebSockets or SSE. Events to third parties use webhooks.
Why interviewers ask
The API shows whether you understand how clients actually use the system. Interviewers look for pagination on lists, idempotency on writes that move money or create orders, and the right protocol for real-time features. Missing idempotency on a payment API or returning an unbounded list are classic red flags.
How it works
- 1Map each functional requirement to one endpoint. Use nouns for resources and HTTP methods for actions: POST /orders, GET /orders/{id}, GET /users/{id}/orders.
- 2Identify the caller from the auth token (for example a JWT in the Authorization header), not from a user_id in the body, so a user cannot act as someone else.
- 3Paginate every list with a cursor: GET /feed?limit=20&cursor=abc returns items plus next_cursor. Cursors (an encoded last-seen ID or timestamp) stay stable while new items arrive; OFFSET pagination gets slow and skips or repeats items.
- 4Make unsafe writes idempotent: the client sends an Idempotency-Key header (a UUID per logical attempt); the server stores key to result for 24 hours and returns the same result on retry instead of creating a second order or charge.
- 5Decide synchronous vs asynchronous: if work takes more than a second or two (video transcoding, report generation, large AI jobs), return 202 Accepted with a job ID, then let clients poll GET /jobs/{id} or receive a webhook or push.
- 6Choose real-time transport where needed: WebSocket for two-way chat, SSE for one-way streams such as live scores or LLM token streaming, long polling as a fallback for restrictive networks.
- 7Define errors and limits: 400 for bad input, 401 or 403 for auth, 404, 409 for conflicts (seat already taken), 429 with Retry-After for rate limits, 5xx only for server faults.
- 8Version the API (/v1/ in the path or a header) so mobile apps that users never update keep working.
Worked example · API for a food ordering service
- GET /v1/restaurants?lat=12.97&lng=77.59&cursor=... returns nearby restaurants, 20 per page, with next_cursor.
- GET /v1/restaurants/{id}/menu returns the menu; cacheable at the CDN for 60 seconds because menus change rarely.
- POST /v1/orders with header Idempotency-Key: 7f3c... and body {restaurant_id, items, address_id, payment_method}. Returns 201 with order_id and status PAYMENT_PENDING.
- If the phone loses network after sending and retries, the server finds the same idempotency key and returns the same order_id instead of creating a second order.
- Payment confirmation does not come back in the same call. The payment gateway calls our webhook POST /v1/payments/webhook with a signature header; we verify the signature and update the order.
- GET /v1/orders/{id} returns current status; on the tracking screen the app opens a WebSocket (or SSE) to receive status and courier location updates every 5 seconds.
- If the restaurant is closed when the order arrives, return 409 Conflict with an error code RESTAURANT_CLOSED so the app can show a clear message.
Picture it
An API is like a restaurant menu. The kitchen can reorganise its stations any time, but the menu tells customers exactly what they can order and what they will get. A good menu also says what happens if you order the same dish twice by mistake.
Key patterns
Idempotency key
When: Any POST that creates something or moves money
How: Client generates a UUID per logical action and reuses it on retries. Server stores the key with the result under a unique constraint and returns the stored result for repeats.
Cursor pagination
When: Any list that can grow, especially feeds and histories
How: Return next_cursor encoding the last item's sort key (for example created_at plus id). Query with WHERE (created_at, id) < (cursor values) ORDER BY created_at DESC, id DESC LIMIT n.
Async job API
When: Work that takes seconds to minutes
How: POST returns 202 with a job_id; GET /jobs/{id} returns status and result URL; optionally notify by webhook or push when done.
Backend for frontend
When: Mobile screens need data from many services
How: One endpoint per screen aggregates calls on the server, reducing round trips on slow mobile networks.
Real-world use
Stripe idempotency keys
Stripe's API accepts an Idempotency-Key header on POST requests and returns the saved result for a repeated key, so network retries never create duplicate charges. Many payment APIs follow the same pattern.
Payment gateway webhooks in India
Gateways such as Razorpay notify merchants of payment events through signed webhooks. Merchants must verify the signature and handle duplicate or out-of-order deliveries, because webhooks are retried.
gRPC inside microservice platforms
Many large companies, including Google internally and many Indian product companies, use gRPC with Protocol Buffers between services for typed contracts, HTTP/2 multiplexing and generated clients, while keeping REST or GraphQL at the public edge.
Streaming LLM responses
Chat-style AI APIs, such as OpenAI's and Anthropic's, stream tokens to clients with server-sent events, so users see the answer start in under a second instead of waiting for the full response.
In a project
Your company exposes an API for merchants to create payment links. Merchants report occasional duplicate links and their servers time out when listing all links.
- Add an Idempotency-Key header to POST /v1/payment_links; store key, request hash and response in a table with a unique index on (merchant_id, key) and 24-hour expiry.
- If the same key arrives with a different request body, return 422 so bugs on the merchant side are visible.
- Replace GET /v1/payment_links (which returned everything) with cursor pagination: limit up to 100, ordered by (created_at, id), next_cursor in the response.
- Add filters merchants actually need (status, created_from, created_to) backed by a composite index.
- Add rate limits per merchant with 429 and Retry-After, and document them.
- Release as /v2 and keep /v1 working with a deprecation date.
Impact: Duplicate links fall to zero, list calls return in under 100 ms instead of timing out, and the database stops seeing full-table scans from a few large merchants.
Trade-offs
Use it when
- REST: public and mobile APIs, cacheable reads, broad tool support
- gRPC: internal service calls needing typed contracts and low overhead
- GraphQL: many client types needing different shapes of the same data
- WebSocket: two-way, low-latency messaging such as chat
- SSE: one-way server streams such as live scores, notifications or LLM tokens
- Webhooks: notifying external systems about events
Avoid it when
- gRPC directly from browsers without a proxy layer such as gRPC-Web
- WebSockets for data that changes every few minutes; polling or push notifications are simpler
- GraphQL when you cannot control query cost; add depth and complexity limits first
- Offset pagination on large or fast-changing tables
Common mistakes
- No pagination on list endpoints.
- No idempotency on order or payment creation, so retries create duplicates.
- Passing user_id in the request body and trusting it instead of using the auth token.
- Making long-running work synchronous so clients time out.
- Using WebSockets for everything, including simple reads.
- Spending 10 minutes on URL naming and JSON fields instead of the decisions that matter.
- Returning 200 with an error message in the body instead of proper status codes.
Interview questions
Quick revision
- 3 to 6 endpoints mapped to functional requirements.
- Identity comes from the auth token, never from the body.
- Every list: cursor pagination with limit and next_cursor.
- Every create or money write: Idempotency-Key.
- Long work: 202 Accepted plus job ID plus poll or webhook.
- Two-way real time: WebSocket. One-way stream: SSE.
- Internal: gRPC. Public and mobile: REST or GraphQL.
- Status codes: 400, 401/403, 404, 409 conflict, 429 rate limited.
- Large uploads: pre-signed URL straight to object storage.
- Version APIs; old mobile apps live for years.
Check yourself
1. What prevents a retried POST /payments from charging the customer twice?
2. Which transport fits streaming LLM tokens from server to browser?
3. Why is OFFSET pagination a problem for a large, busy feed?
4. A report takes 3 minutes to generate. What should POST /reports return?
Saved on this device. Create a free account to keep it on every device.
Practise this with Sam
A real mock interview, out loud, with an honest scorecard. Free to start.