The API step is the few minutes of a system design interview where you write down the contract between your clients and your system: one endpoint per requirement, the fields that matter in each request and response, and the handful of details that decide whether the design survives real traffic. Done well, it proves you covered every requirement, gives your architecture diagram its request paths, and plants the hooks (idempotency keys, cursors, 202s) that your deep dives will use later. This page is a recipe for doing it in about five minutes, plus the follow-ups that come next.
Picture it first: the reservation slip
Before online booking, a train ticket started with a paper requisition slip at the station counter. That slip is a well-designed API.
- It asks only for what the clerk needs: train number, date, class, stations, and each passenger’s name and age. It never asks which ledger page or quota the berth comes from. That’s the clerk’s business.
- Your identity is checked at the window, against the ID card in your hand, not taken from whatever name you wrote at the top.
- You get back a PNR, a short number that means nothing to you but names your booking. Every later question, “is it confirmed?” or “cancel it”, starts with that number.
- If the booking can’t be settled on the spot, say you’re waitlisted, you don’t stand at the window for a week. You leave and check the PNR status later.
- A careful clerk handed the same slip twice, because you weren’t sure the first one went through, gives you the same PNR instead of a second berth.
Map it back: the slip is the request body (only the fields that matter), the ID check is the auth token, the PNR is the resource ID returned with 201 Created, checking status later is 202 plus a status resource, and the careful clerk is an idempotency key. Each line of the slip takes seconds to fill in. That is the right pace for this step of the interview, too.
Where the API step fits
Most design rounds follow the same arc: clarify requirements, name the core entities, define the API, draw the high-level design, then go into deep dives. The delivery framework walks through the whole arc. The API sits in the middle for a reason: it turns requirements into concrete operations, and every operation becomes a request path you draw next.
While you write endpoints, the interviewer is checking five things:
- Coverage. Every functional requirement maps to at least one endpoint, and nothing out of scope sneaks in.
- The client’s view. What does the app send, and what does it need back to draw the screen?
- Security sense. Who is calling, and may they touch this object?
- Scale awareness. Do lists page? Are writes safe to retry? Does slow work block a request?
- Time discipline. Five minutes, not fifteen. Listing every CRUD route for every entity is the classic way to burn time here.
The recipe, step by step
We’ll run the recipe on an IRCTC-style tatkal booking system: a quota of berths opens at a fixed time, say 10:00 IST, and a crowd many times the size of the quota tries to book in the first few minutes.
Step 1: list the functional requirements
You wrote these already; read them back. Users should be able to:
- Check availability for a train, date and class.
- Hold berths for their passengers for a few minutes.
- Book and pay for the held berths.
- See their bookings.
- Cancel a booking.
Step 2: name the resources
Pull out the nouns: Train, Availability, Hold, Booking (with its passengers) and Payment. Notice what’s missing: User. The caller is whoever the token says, so the user rarely needs to appear as a field.
API resources are not your tables. Google’s resource-oriented design guide (AIP-121) calls an API that is identical to its database schema an anti-pattern, because it couples every caller to your storage. A hold might live in Redis and a booking in PostgreSQL; the client never needs to know.
Step 3: one endpoint per requirement
Each requirement becomes a verb on a resource:
GET /v1/trains/{train_no}/availability?date=2026-11-02&class=3A
POST /v1/holds
POST /v1/bookings
GET /v1/bookings?limit=20&cursor=<opaque>
POST /v1/bookings/{booking_id}:cancel
Five requirements, five endpoints. If a requirement needs no endpoint, or an endpoint serves no requirement, say why.
Step 4: sketch the tricky requests and responses
Don’t write out all five. Pick the one or two where the shape matters, and show only the fields that drive the design:
POST /v1/holds
Authorization: Bearer <token>
{ "train_no": "12951", "date": "2026-11-02", "class": "3A",
"passengers": [{ "name": "Asha", "age": 34, "berth_pref": "lower" }] }
201 Created
Location: /v1/holds/hld_8Kq2
{ "hold_id": "hld_8Kq2", "berths": ["B4-23"],
"fare": { "amount": 162050, "currency": "INR" },
"expires_at": "2026-11-01T04:40:00Z" }
Three small choices carry weight. The fare is an integer in paise (162050 is ₹1,620.50), so there is no floating-point rounding. The expiry is an RFC 3339 timestamp in UTC: 04:40Z is 10:10 IST, ten minutes after the window opened. And the response is the hold itself, so the app can show a countdown without a second call.
POST /v1/bookings
Authorization: Bearer <token>
Idempotency-Key: 3f6c2a9e-7b1d-4e8a-9c55-0d2e8f4b7a13
{ "hold_id": "hld_8Kq2", "payment_method": "upi" }
201 Created
Location: /v1/bookings/bk_5Tz9
{ "booking_id": "bk_5Tz9", "status": "awaiting_payment",
"payment": { "redirect_url": "<psp-url>", "expires_at": "2026-11-01T04:40:00Z" } }
The booking exists at once, in an awaiting_payment state, because the money moves in the user’s UPI app, not inside this request.
Step 5: add the scale details
This is where strong answers pull ahead. Run down the list and say each one out loud:
- Identity from the token. No
user_idin the body or path. If the user must appear in a path, use an alias such as/v1/users/me, which Google’s naming guide (AIP-122) allows for the authenticated caller. - Pagination on every list. A
limitwith a documented default and maximum (say 20 and 100) plus an opaquecursor; the response carriesnext_cursor. Google’s pagination guide (AIP-158) requires paging from the first version, because adding it later breaks clients. See pagination. - An idempotency key on every non-idempotent write that matters. Here that is both
POST /v1/holdsandPOST /v1/bookings: a retried hold could grab a second set of berths, and a retried booking could charge twice. See idempotency. - Async for slow or external work. Payment confirmation arrives later as a webhook from the payment gateway. Anything that takes more than a couple of seconds returns
202with a status resource (next section). - A push channel only if needed. Booking status can be polled or sent as a mobile notification; a live seat map would justify SSE.
- Rate limits. Per user and per IP, especially when bots join the 10 am rush. Over the limit:
429withRetry-After. See rate limiting.
In the interview
One sentence buys a lot of credit: “The user comes from the auth token, never the request body, and every endpoint checks that the caller owns the object it names.” It shows you know the most common API vulnerability before anyone asks.
Put together, the booking flow looks like this. The interesting moment is step 2, where the response is lost and the retry must not book twice.
Step 6: errors in one line
Don’t list every code. One line covers it: 400 invalid input, 401 no valid token, 404 not found (also when the booking belongs to someone else, so you don’t confirm it exists), 409 berths gone or hold expired, 429 slow down, 5xx retry with backoff if the call is idempotent. Bodies use the standard problem-details format described in What is an API?.
409 Conflict suits a sold-out hold: RFC 9110 defines it as a clash with the resource’s current state that the user might resolve and resubmit, for example by choosing another class.
Step 7: name the protocol and why
One sentence is enough: “REST and JSON over HTTPS for the apps, because every client and CDN speaks it; gRPC from the booking service to inventory, for typed, low-latency internal calls; the payment gateway calls us back with a webhook.”
Slow work: 202, a status resource and a webhook
Some work can’t finish while a client waits: a Flipkart-style seller importing a 50,000-row catalogue, a video transcode, a monthly GST report. Holding the request open is fragile. Gateways and mobile networks drop long connections, and a client that times out and retries starts the job twice.
Instead, accept the job and hand back a place to watch it. RFC 9110 calls 202 Accepted intentionally noncommittal: the request was accepted but not completed, and the response ought to point to a status monitor. Microsoft’s Azure API guidelines spell the pattern out: return 202 with an Operation-Location header (plain Location works too), let clients poll a status resource whose status moves through values such as NotStarted, Running, Succeeded and Failed, send Retry-After while it is running, and keep the status for at least 24 hours after it finishes.
POST /v1/catalog-imports Idempotency-Key: <uuid>
{ "file_id": "upl_77a" }
→ 202 Accepted
Location: /v1/import-jobs/imp_42
{ "id": "imp_42", "status": "queued" }
GET /v1/import-jobs/imp_42
→ 200 OK Retry-After: 10
{ "id": "imp_42", "status": "running", "progress": 0.4 }
Polling is simple and works through any firewall; you pay with wasted calls and up to one polling interval of delay. A webhook tells the seller the moment the job is done, but the seller needs a public endpoint and must verify signatures. Offer both: the webhook for speed, the status URL as the source of truth. More in long-running jobs.
Choosing the style
You don’t need a debate here; you need a default and one sentence of reasoning. The full comparison lives in REST vs GraphQL vs gRPC.
| Situation | Default | Why | What you pay |
|---|---|---|---|
| Apps, browsers and partners calling you | REST + JSON over HTTPS | Every client, proxy and CDN understands it; GETs cache | Several calls per screen; fixed response shapes |
| Service-to-service calls at high rates | gRPC | Typed contract, compact binary messages, deadlines, streaming | Browsers need a proxy; harder to read by eye |
| Many clients wanting different slices of the same data | GraphQL, or a backend-for-frontend | One round trip, shaped by the client | Harder HTTP caching; query cost limits; N+1 lookups |
| Server must push updates to a user | SSE one way, WebSocket both ways | No polling delay | Millions of open connections to run |
| Telling another company something happened | Webhooks | The partner doesn’t poll | Retries, signatures, out-of-order events |
| Work that takes seconds to hours | REST 202 + status URL (+ webhook) |
No connection held open | Client must poll or receive a callback |
Conventions that read well on a whiteboard
Interviewers don’t grade naming, but clean names read as experience, and messy ones (/getBookingsForUser) invite questions you don’t need. These conventions come mostly from Google’s API Improvement Proposals and Microsoft’s guidelines:
| Convention | Example | Why |
|---|---|---|
| Plural nouns for collections | /v1/bookings, /v1/bookings/bk_5Tz9 |
AIP-122 requires plural collection names; the HTTP method is the verb |
| Nest one level, for real ownership | /v1/trains/{train_no}/availability |
Deep paths get brittle; filter instead: /v1/bookings?train_no=12951 |
| Non-CRUD actions as custom methods | POST /v1/bookings/{id}:cancel |
AIP-136: POST for side effects, verb after a colon; a /cancel sub-path also reads fine |
| Reads never change state | GET only for reads |
RFC 9110 defines GET as safe, so crawlers and prefetchers may call it freely |
| Filters, sort and paging in the query | ?status=confirmed&sort=-created_at&limit=20 |
One resource URL serves every view |
| IDs as opaque strings | "bk_5Tz9", not 1042 |
AIP-122: ID fields should be strings; unguessable IDs slow enumeration |
| Timestamps in RFC 3339, UTC | "2026-11-01T04:40:00Z" |
Microsoft’s guidelines require RFC 3339; UTC removes time-zone bugs |
| Money as integer minor units plus currency | { "amount": 162050, "currency": "INR" } |
0.1 + 0.2 is not exactly 0.3 in floating point; paise are exact |
| Enums as strings that may grow | "status": "awaiting_payment" |
Readable in logs; Microsoft advises telling clients new values may appear |
| Major version in the path | /v1/... |
Breaking changes go to /v2; see What is an API? |
Don’t design the database in the API step
Field lists of twenty columns, foreign keys and indexes belong to the data model, later. In the API step, show the two or three fields that change the design (hold_id, expires_at, next_cursor) and write “…” for the rest. The interviewer is checking that you think from the client inwards, not that you can type a schema.
Worked examples
Each example is the endpoint list you’d write, one sketch, and the choice an interviewer is likely to poke at.
URL shortener
POST /v1/links { "long_url": "https://shop.example/sale?id=88", "alias": null }
→ 201 Created { "code": "3cVrDte", "short_url": "https://sho.rt/3cVrDte" }
GET /{code} → 302 Found Location: <long_url>
GET /v1/links/{code}/stats?from=…&to=…
The interesting choice is the redirect code. RFC 9110 marks 301 Moved Permanently as heuristically cacheable, so a browser may skip your server on the next click: less load, but those clicks vanish from analytics and you can’t retarget or block the link for that browser. 302 Found brings every click back to you, at the price of serving every redirect. Most designs choose 302 and absorb the load with caching, as the URL shortener design does. The redirect lives at the root, not under /v1, because short links must stay short.
Ride hailing
POST /v1/rides Idempotency-Key { pickup, drop, product }
→ 201 { "ride_id": "rd_19", "status": "matching", "fare_estimate": {...} }
GET /v1/rides/{ride_id}/events SSE stream: driver_assigned, location, arrived
PUT /v1/drivers/me/location { "lat": 12.9716, "lng": 77.5946, "recorded_at": "…Z" }
POST /v1/rides/{ride_id}:accept (driver) → 200, or 409 if another driver won
Driver location is the volume question. Assume 500,000 online drivers sending a fix every 4 seconds: 500,000 ÷ 4 = 125,000 writes per second. PUT fits because each fix replaces the last one rather than adding to a list, so a retried update is harmless, and recorded_at lets the server ignore a late, older fix. At that rate many designs move driver updates onto the WebSocket the driver app already holds, trading an HTTP request per fix, with its headers and token check, for one small message on an open connection. The rider watches a one-way SSE stream. :accept returns 409 when two drivers race for one ride, because only one conditional update can win. See Design Uber and real-time connections.
News feed
POST /v1/media-uploads { "content_type": "image/jpeg", "size_bytes": 4000000 }
→ 201 { "media_id": "med_3x", "upload_url": "<pre-signed PUT URL>", "expires_at": "…Z" }
PUT <upload_url> raw bytes, straight to object storage
POST /v1/posts Idempotency-Key { "text": "…", "media_ids": ["med_3x"] }
GET /v1/feed?limit=20&cursor=<opaque>
→ 200 { "items": [...], "next_cursor": "…" }
PUT /v1/posts/{post_id}/likes/me
DELETE /v1/posts/{post_id}/likes/me
Three choices to defend. First, photo bytes never pass through your API servers. The API hands out a pre-signed URL, a short-lived link signed with your credentials that lets the app PUT one object straight into storage. Amazon’s S3 documentation allows up to 7 days of validity through the SDKs, but anyone holding the URL can use it, so keep it to minutes. Second, the feed pages with an opaque cursor, never an offset, because new posts arriving at the top would shift every page. Third, a like is a PUT on likes/me, so double-taps and retries leave exactly one like. Details in Design a news feed and large file uploads.
Sending the photo inside the JSON is worse than it looks: base64 turns every 3 bytes into 4 characters, so a 4 MB photo becomes about 4 × 4 ÷ 3 ≈ 5.3 MB of request body that an API server must buffer, parse and forward.
Weak and strong answers side by side
| Weak answer | Strong answer | |
|---|---|---|
| Endpoint | POST /createBooking |
POST /v1/bookings |
| Identity | userId in the body |
User from the bearer token; ownership checked per object |
| Retries | Not mentioned | Idempotency-Key header; a retry replays the first result |
| Lists | GET /bookings returns everything |
limit (default 20, max 100) and an opaque cursor |
| Success | 200 {"ok": true} |
201 Created, a Location header, the booking in the body |
| Errors | 200 with an error message inside |
The right status class plus a problem-details body; 409 when berths are gone |
| Slow work | Request held open for 30 seconds | 202 with a status URL, plus a webhook |
| Uploads | Base64 photo inside the JSON | Pre-signed URL straight to object storage |
| Time | 15 minutes on every CRUD route | About 5 minutes, core requirements only |
Going deeper: the follow-ups interviewers probe
“Why not just send userId?” Because the client controls the body: anyone can change the ID and act as someone else. Take identity from the verified token, and on every request that names an object (GET /v1/bookings/bk_5Tz9), check that the caller may access that object. OWASP ranks this failure, Broken Object Level Authorization, first in its API Security Top 10 for 2023, and warns that merely comparing the token’s user ID with an ID parameter covers only a small subset of cases. Random IDs slow enumeration, but they are not a permission check.
“What if a list has a million items?” Every list gets a limit and a cursor from day one. AIP-158 explains why it can’t wait: a client with 75 items that always received all 75 silently gets only the first page once paging appears. Cursors also stay fast deep into a list, where an offset makes the database read and throw away every skipped row.
“The client timed out. Can it retry?” GET, PUT and DELETE are idempotent by RFC 9110’s definition, so yes. POST and PATCH are not, so the client sends an Idempotency-Key: a fresh UUID per operation, reused on every retry. The IETF draft standardising the header suggests 409 when a retry arrives while the first attempt is still running and 422 when a key is reused with a different body. Stripe’s v1 API, for example, takes keys up to 255 characters and may prune them once they are 24 hours old.
“Users upload 50 MB videos. Through your API?” No. Big bodies tie up API connections and memory for the whole upload, and they scale with bytes, not requests. Hand out a pre-signed URL, let object storage take the bytes, and keep the API for metadata.
“What if the request takes 30 seconds?” Return 202 Accepted with a status resource, do the work from a queue, and notify by webhook or let the client poll with Retry-After. Never make a client hold a connection open for slow work.
“The home screen needs five calls. Isn’t that slow on mobile?” Yes. Assume 100 ms per round trip on a mobile network: five sequential calls cost 5 × 100 = 500 ms before anything renders, while one aggregate call costs about 100 ms plus a few milliseconds of fan-out inside the data centre. Add a backend-for-frontend endpoint such as GET /v1/home, or use GraphQL. See API composition and chatty I/O.
“PUT, PATCH or POST?” POST creates in a collection when the server picks the ID, or runs an action. PUT replaces the whole resource at a known URL, so repeating it is harmless, which suits “set driver location”. PATCH changes some fields: a patch that sets values is safe to repeat in practice, one that increments is not. A state change with rules and side effects, like cancelling with a refund, reads better as an action (:cancel) than as PATCH {"status": "cancelled"}.
“How do you version it?” /v1 in the path from day one. Add optional fields freely; anything that removes, renames or changes meaning goes to /v2, and the old version stays up while clients move. The full story is in What is an API?.
“The app shows ‘not found’ right after creating a booking.” The follow-up read hit a lagging replica. Return the created resource in the 201 body so the client needs no follow-up read, and send a user’s reads of their own fresh writes to the primary. AIP-121 makes this a rule: after a successful create, a get must return the resource.
“What stops one client hammering you?” Rate limits per user, API key and IP at the gateway, answering 429 Too Many Requests with Retry-After, which RFC 9110 allows as seconds or a date. See rate limiting.
Cheat sheet
The API step on one whiteboard:
API: REST + JSON over HTTPS, /v1, Authorization: Bearer <token> (user = token)
FR1 GET /v1/things?filter=…&limit=20&cursor=… → 200 { items, next_cursor }
FR2 POST /v1/things + Idempotency-Key → 201 + Location + thing
FR3 GET /v1/things/{id} → 200 | 404
FR4 POST /v1/things/{id}:verb → 200 | 409
FR5 POST /v1/jobs (slow) → 202 + Location: /v1/jobs/{id}
Push: GET /v1/things/{id}/events (SSE) or a WebSocket, only if live
Partners: webhooks, signed and retried
Errors: 400 · 401 · 404 · 409 · 429 + Retry-After · 5xx (problem+json)
Internal: gRPC between services
| Check | Default answer |
|---|---|
| Identity | From the token; /users/me alias; object-level check on every request |
| Lists | limit (default 20, max 100), opaque cursor, next_cursor |
| Writes that cost money or stock | Idempotency-Key; replay the first result |
| Creates | 201 + Location + the resource |
| Slow work | 202 + status URL + Retry-After; webhook when done |
| Big files | Pre-signed URL to object storage |
| Live updates | SSE one way, WebSocket both ways |
| Abuse | 429 + Retry-After |
| Formats | String IDs, RFC 3339 UTC, money in minor units, string enums |
A time budget, not a rule
These numbers are a rule of thumb, not a standard: about five minutes for the whole step. Roughly one minute to list endpoints, two to sketch the one or two tricky requests, one for the scale details, and thirty seconds each for errors and protocol. If the interviewer waves you on early, move on; if they push on one endpoint, a deep dive has started, so follow it.
How to say it in the interview
“For the API, I’ll map each requirement to one endpoint, REST and JSON for the apps. Availability is
GET /v1/trains/{train_no}/availability, served from cache. Holding berths isPOST /v1/holds, which returns a hold ID and an expiry ten minutes out, or 409 if the berths are gone. Booking isPOST /v1/bookingswith the hold ID and an Idempotency-Key header, so a retry after a dropped connection can’t book or charge twice; it returns 201 with the booking inawaiting_payment, and the payment gateway confirms by webhook. ‘My bookings’ is a cursor-paginated GET, and cancel isPOST /v1/bookings/{id}:cancel. The user always comes from the token. Internally, booking talks to inventory over gRPC.”
Why not pass the user ID in the request? The client controls the body, so anyone could book as anyone. Identity comes from the verified token, and each endpoint checks that the caller owns the object it names; missing that check is OWASP’s top API risk.
How do you make booking safe to retry?
The client generates a UUID per booking attempt and sends it as Idempotency-Key on every retry. The server stores the key with the result in the same transaction and replays the first response; a retry that arrives mid-flight gets 409.
What if the request takes 30 seconds?
Then it shouldn’t be a blocking call. Return 202 Accepted with a status URL, run the work on a worker, and let the client poll with Retry-After or receive a webhook.
How do you paginate a feed?
With limit and an opaque cursor, never an offset, because new posts at the top would shift offsets and repeat items. For a ranked feed, the cursor points into a snapshot ranked once per session.
REST or GraphQL here? REST: there are few client types, the reads cache well and the tooling is simple. I’d switch to GraphQL or a backend-for-frontend if web, Android and partner apps each needed different shapes of the same data.
Where to go next
- The delivery framework: the full interview arc around this step.
- What is an API?: contracts, status classes, problem details and versioning.
- REST vs GraphQL vs gRPC: the style choice in depth.
- Idempotency: how keys are stored, scoped and replayed.
- Pagination: offset vs cursor: cursors, page tokens and ranked feeds.
- Rate limiting: the algorithms and the 429 contract.
- Webhooks and real-time connections: the push half of an API.
- Contention and double booking: what happens behind
POST /v1/holds. - Design Ticketmaster (BookMyShow): a full booking design.
- Authentication: sessions, tokens and JWT: where the token comes from.
References and further reading
- RFC 9110 — HTTP Semanticsrfc-editor.orgSafe and idempotent methods, POST and PUT semantics, 201 with Location, the noncommittal 202, 409 Conflict, 301 vs 302 caching and the Retry-After header.
- Google AIP-121 — Resource-oriented designgoogle.aip.devNouns as resources and a few standard verbs, why an API that mirrors the database schema is an anti-pattern, and read-after-create consistency.
- Google AIP-122 — Resource namesgoogle.aip.devPlural collection identifiers, string IDs, and aliases such as users/me for the authenticated caller.
- Google AIP-136 — Custom methodsgoogle.aip.devActions that don’t fit create, get, update or delete: POST for side effects and a colon-separated verb such as :archive.
- Google AIP-158 — Paginationgoogle.aip.devPaginate every list from the first version, opaque page tokens that grant no access, and why adding paging later breaks clients.
- Stripe API reference — Idempotent requestsdocs.stripe.comClient-generated keys (V4 UUIDs suggested) up to 255 characters, replayed responses, and pruning after at least 24 hours in API v1.
- The Idempotency-Key HTTP Header Field (IETF draft-ietf-httpapi-idempotency-key-header-08)datatracker.ietf.orgThe header being standardised for POST and PATCH, with 400 for a missing key, 409 for a concurrent retry and 422 for a reused key.
- Amazon S3 User Guide — Download and upload objects with presigned URLsdocs.aws.amazon.comTime-limited upload URLs that need no AWS credentials, up to 7 days of validity through the SDKs, and why they must be guarded like bearer tokens.
- OWASP API Security Top 10 (2023) — API1: Broken Object Level Authorizationowasp.orgThe top API risk: callers changing object IDs to reach other users’ data, and why checking the token’s user ID alone is not enough.
- Microsoft Azure REST API Guidelinesgithub.com202 plus a status monitor for long-running operations, Retry-After while polling, RFC 3339 timestamps and extensible string enums.