If you remember five things
The whole answer in one glance. Everything below defends these five calls.
- Nouns plus verbs. Your API is a set of resources (nouns) and HTTP methods act on them. Learn the pattern for one resource and you can guess every other one.
- Stateless. Each request carries everything the server needs, so any instance can serve any request. That is the foundation of horizontal scaling.
- Status codes do the talking. 2xx, 4xx, 5xx carry meaning so clients and proxies handle failures without parsing your error strings.
- Idempotency keys for unsafe retries. Networks drop responses and clients retry. A key on POST stops a retried create from charging twice or placing a duplicate order.
- Cursor pagination on anything that grows. Offset gets slow and shows duplicates when data shifts. A cursor stays fast and stable at any depth.
1The core idea#
REST stands for Representational State Transfer. Ignore the academic baggage. The model you actually need is simple.
Your API is a set of nouns, and HTTP verbs are the things you do to them.
- A resource is a thing in your system: a user, an order, a playlist, a comment.
- Each resource gets a URL that identifies it.
- The verb you send says what you want to do to it.
So instead of inventing function names like createUser, deleteUserById, and getAllUsers, you have one noun, users, and you vary the verb:
GET /userslists usersPOST /userscreates oneGET /users/42fetches user 42PUT /users/42replaces user 42PATCH /users/42updates part of user 42DELETE /users/42removes user 42
That is the whole trick: URLs name things, verbs act on them.
- Once a client learns the pattern for one resource, they can guess it for every other resource in your API.
- That predictability is the real value. A client team can integrate against
/invoiceson day one after learning/ordersthe week before.
Two more properties matter for interviews.
Statelessness: the server remembers nothing between requests.
- Each request carries everything the server needs: auth token, parameters, body.
- Any request can go to any instance, so you run 20 copies behind a load balancer with no sticky sessions and no shared memory.
- This is the foundation of horizontal scaling.
- Payoff: when one instance crashes mid-traffic, the load balancer stops sending it requests and nothing is lost, because no state lived on that box.
Uniform interface: same conventions everywhere.
- Same verbs, same status code meanings, same naming across every endpoint.
- A new engineer reads your route list and gets it in minutes, instead of learning that
/fetchUserDataand/user-getdo almost the same thing.
Resources are nouns, HTTP methods are verbs. Everything else (status codes, pagination, versioning) is a convention layered on top. If you internalize noun-plus-verb, you stop translating function names into URLs and start designing APIs a client can predict.
2How it works#
A REST call is just an HTTP request and response. Here is the anatomy of one.
A client wants to create an order. It sends:
POST /orders
Authorization: Bearer <token>
Content-Type: application/json
{ "item_id": "sku_993", "quantity": 2 }
The server validates the token, checks the body, writes to the database, and replies:
201 Created
Location: /orders/7781
Content-Type: application/json
{ "id": 7781, "item_id": "sku_993", "quantity": 2, "status": "pending" }
What that response is doing:
201says a new resource was created, not just that the call succeeded.Locationtells the client where the resource now lives.- A well-behaved client stores
/orders/7781and polls it for status instead of guessing the URL.
Here is the path a typical request takes through a real deployment:
Status codes carry meaning. Use them properly and clients handle failures without parsing your error strings. The families:
- 2xx success.
200 OK,201 Created,204 No Contentfor a delete with nothing to return. - 3xx redirection.
304 Not Modifiedfor caching. - 4xx the client messed up.
400bad request,401not authenticated,403authenticated but not allowed,404not found,409conflict,429too many requests. - 5xx the server messed up.
500generic failure,503service unavailable.
Reveal the answer
Idempotency is the property that makes distributed systems survivable.
- An idempotent request produces the same result whether you send it once or five times.
GET,PUT, andDELETEare naturally idempotent: deleting order 7781 twice leaves it deleted either way.POSTis not, because two POSTs create two orders.- This matters because networks drop responses. A client that never got your
201will retry, and now you have a duplicate order. - The fix is an idempotency key: a unique header the client sends (for example
Idempotency-Key: a1b2c3) so the server recognizes the retry, dedupes it, and returns the original201the second time instead of creating a second order.
3A worked example: the write endpoint at real scale#
Put numbers on it so it stops being abstract. Say you are designing the order API for a mid-size storefront. Here is the back-of-the-envelope I would say out loud.
- Traffic. 5 million orders per day averages to about 58 writes per second. Order traffic is bursty, so size for a peak near 5x the average: about 290 writes per second.
- Reads dominate. Customers check order status far more than they place orders. Assume 20 reads per write: about 5,800 reads per second at peak. This is why I care so much about caching
GET /orders/{id}. - Payload size. An order JSON is roughly 1 KB. At 290 writes/s that is about 290 KB/s inbound, trivial. Reads are the volume: 5,800 reads/s at 1 KB is about 5.8 MB/s, still small, but the cache hit rate is what keeps the database alive.
- The retry math that justifies the key. At a 0.1% response-loss rate with client retries, 5 million orders a day means about 5,000 duplicate
POST /ordersattempts daily. Without a key, that is 5,000 duplicate orders a day. On a payments API, 5,000 double charges a day.
So the shape I would land on:
POST /orders Idempotency-Key required, returns 201 + Location
GET /orders/{id} cacheable, served from cache on the hot path
GET /orders?cursor=... cursor pagination, limit default 20 max 100
PATCH /orders/{id} status transitions only
Why the caching line is the whole game:
- At 5,800 reads/s, if the cache absorbs 90% of reads, the database sees about 580 reads/s instead of 5,800.
- That is the difference between one modest Postgres primary with a read replica and a painful sharding conversation.
- Saying this with the numbers is what separates "I know REST verbs" from "I have run this."
4Trade-offs#
This is where interviews get interesting. There is rarely one right answer, so explain what you give up.
PUT versus PATCH: replace the whole thing, or patch the fields you send.
PUTreplaces the whole resource. Send a user object with only an email and you may wipe out their name, because you handed a partial body to a "replace" operation.PATCHupdates only the fields you send.PUTis simpler and idempotent by nature.PATCHis lighter over the wire and safer for partial edits, but you have to be careful about its semantics.- I default to
PATCHfor updates in most real systems.
| Decision | Option A | Option B | When I pick which |
|---|---|---|---|
| Partial update | PUT replaces whole resource | PATCH updates sent fields | PATCH for most updates, PUT only when the client truly owns the full object |
| Create semantics | POST not idempotent | PUT to a client-chosen id, idempotent | PUT-with-id when the client can generate a stable id, otherwise POST plus idempotency key |
Pagination styles: offset is easy, cursor is correct at scale.
- When a list has a million rows, you cannot return all of them.
- Offset is fine for a small admin table. For an infinite feed where new items arrive constantly, use cursors.
Offset
- Looks like
?limit=20&offset=40- Pro
- Simple, and you can jump to any page.
- Con
- Slow on deep pages, and items shift if data changes.
- Breaks when
OFFSET 100000walks and discards 100,000 rows before returning 20.
Cursor
- Looks like
?limit=20&cursor=abc- Pro
- Stable and fast at any depth.
- Con
- Cannot jump to page 50, more work to build.
- Wins because
- A cursor on
(created_at, id)is an indexed seek: page 5,000 costs the same as page 1.
- Offset breaks on a live feed because inserting a row shifts everything and the user sees duplicates.
- A cursor keyed on
(created_at, id)turns deep pagination into an indexed seek that costs the same whether you are on page 1 or page 5,000.
Chatty versus chunky: small resources, or screen-shaped responses.
- A strict REST design gives you small, focused resources.
- But a mobile screen might need a user, their last five orders, and their cart. Three round trips on a slow connection feels sluggish.
- At 200 ms per round trip on a weak mobile network, three sequential calls is 600 ms of spinner.
- Fight it by letting clients expand related data (
?include=orders,cart) or by adding a purpose-built endpoint for that screen. - The trade-off: more flexibility means a bigger API surface and harder caching. This is exactly the gap GraphQL was built to close, which is a great thing to mention.
Versioning: your API will change and you cannot break shipped clients overnight.
| Approach | Example | Notes |
|---|---|---|
| URL path | /v2/users |
Visible, easy to route and cache, most common |
| Header | Accept: version=2 |
Cleaner URLs, harder to test by hand |
- I prefer URL versioning for public APIs: you can paste it in a browser and a CDN can cache different versions separately.
- The honest answer: the best versioning is avoiding breaking changes. Add fields, do not remove or rename them.
- A mobile app you shipped last year is still in someone's pocket, so the
/v1contract has to keep working for years.
5Where it is used#
REST is the default for public web APIs, and the examples are everywhere.
- Payment providers expose
POST /chargesandGET /charges/ch_123. - Source control platforms expose
GET /repos/owner/name/issues. - Cloud providers manage infrastructure through REST endpoints.
Reach for REST when:
- You have clear resources that map to nouns: users, posts, orders, invoices.
- Your clients are varied (web, mobile, third-party) and you want something predictable they can all consume.
- You want HTTP caching, CDNs, and standard tooling. A
GETwith the right cache headers can be served from a CDN edge and never touch your servers. That is how you keep the 90% cache hit rate from the worked example honest.
Reach for something else when REST's shape fights your problem:
- GraphQL when clients need wildly different slices of data and you are tired of either over-fetching or building a new endpoint per screen. This is the fix for chatty-versus-chunky.
- gRPC for internal service-to-service calls where you want low latency, binary payloads, and a strict contract. Often 2x to 5x smaller payloads with protobuf, but harder to debug and not browser-friendly.
- WebSockets when the server needs to push updates, like a live chat or a trading ticker. REST is request-response, so it cannot push.
A strong move is to default to REST for the public API and quietly use gRPC between your internal services. That is what a lot of real systems do, and naming the split unprompted tells me you have shipped both.
6Common pitfalls and gotchas#
Verbs in the URL.
POST /createUserorGET /getUserOrdersdefeats the point.- The verb belongs in the HTTP method, not the path. Keep URLs as nouns.
Returning 200 for everything.
- I have seen APIs return
200 OKwith a body like{ "error": "not found" }. - Now every client parses the body to know if the call worked, and no proxy or monitoring tool can tell success from failure.
- Your dashboards show 100% success while customers are failing. Let the status code do its job.
Non-idempotent retries.
- A
POST /paymentswith no idempotency key plus a network blip plus a client retry charges the customer twice. - Recall the math: 0.1% lost responses on millions of requests is thousands of double charges a day.
- This is a real incident in real companies. Mention idempotency keys and you sound like someone who has been paged at 3am.
Leaky or inconsistent naming.
- Pick a convention and hold it.
- Plural nouns (
/ordersnot/order), and eithersnake_caseorcamelCasefor fields, not both. - Mixing them is the small thing that makes an API annoying to use.
No pagination on list endpoints.
GET /ordersthat returns every order ever created falls over the day a customer has 50,000 of them.- Paginate list endpoints from day one, even with a generous limit.
Exposing internal errors.
- A
500that dumps a stack trace or SQL error leaks your internals and helps attackers. - Return a clean error with a correlation ID the client can quote in a support ticket, and log the details server-side.
Returning 200 for failures, or shipping a POST /payments with no idempotency story. Both tell me you have never operated an API under real traffic. If I say "the client retried after a timeout" and you have no answer, the interview is effectively over.
7How it shows up in interviews#
When I ask someone to "design the API for X," here is what I am listening for.
Do they model resources cleanly?
- Weak: rattles off
getOrders,addOrder,updateOrderStatus. - Strong: "the resource is
orders, soGET /orders,POST /orders,GET /orders/{id}, and for status I would usePATCH /orders/{id}." - The strong answer shows they get noun-plus-verb instead of translating function names into URLs.
Do they pick correct status codes?
- Weak: "it returns a success or an error."
- Strong: "
201on create with aLocationheader,409if the order already exists,422if the payload fails validation." - Specific codes signal real experience.
Do they handle scale and failure?
- This is where beginners and strong candidates separate.
- Bring up pagination before I ask. Bring up idempotency keys on the payment endpoint. Mention rate limiting with
429and aRetry-Afterheader. - Each is a one-sentence tell that you have run APIs in production, not just read about them.
Can they defend a trade-off?
- If I push with "why REST and not GraphQL here?", a strong answer is: "the clients all need roughly the same shape of data and I want CDN caching on reads, so REST is simpler. If the mobile team started needing very different slices, I would reconsider."
- That is a real engineer weighing options, not reciting a definition.
Here is the contrast boiled down, because it is the whole game:
Weak: "I would have an endpoint
/getOrdersto get orders and/addOrderto add one. It returns the order, or an error if something goes wrong."Strong: "The resource is
orders.POST /orderswith anIdempotency-Keyheader returns201plus aLocation, and the key stops a retried request from creating a duplicate.GET /orders/{id}is cacheable and serves most of my read traffic from cache, which at a 20-to-1 read-write ratio is what keeps the database off the floor. List isGET /orders?cursor=...with cursor pagination so deep pages stay fast. Status changes go throughPATCH /orders/{id}, and I return409on a conflicting transition."
- Same question. The second answer names verbs, codes, headers, a caching strategy, and a failure mode without being asked.
- The through-line: be specific, name the verbs and codes, and surface the failure cases on your own.
Name the verb, name the code, name the failure. That is the difference between an answer that passes and one that stands out.
"The client sent POST /orders, timed out, and retried. How do you avoid a duplicate?"
"Offset or cursor pagination for an order feed, and why?"
"401 or 403: valid token, but the user is not allowed to touch this resource?"
"PUT or PATCH to update one field on a user?"
"Why REST here instead of GraphQL or gRPC?"
What you can now explain in the room
- Why REST is just resources (nouns) plus HTTP methods (verbs), and why that predictability is the point.
- Why statelessness is the thing that lets you scale horizontally behind a load balancer.
- Which status code to return when, including the 401-versus-403 distinction.
- Why POST needs an idempotency key, with the retry math that proves it.
- When to use cursor pagination instead of offset, and why offset dies on deep pages.
- How to defend REST against GraphQL and gRPC, and when you would switch.
Next, try the System Design Question walkthrough on designing a URL shortener, where you will put these endpoints to work on a real resource.
Get a prep plan built around your REST API Design interview
Tell us the role and your interview date. We will send back a dated, day by day plan. Free.