API design interview questions that go past the verbs
Everybody can name the verbs and say what a 404 means. The API round doesn't spend long there. It spends its time on what happens when a request arrives twice, when rows move underneath a paginated read, and when a client you can't upgrade is still calling last year's shape.
API design is the most deceptive area in a backend interview, because the opening question sounds like vocabulary. What's the difference between PUT and POST. What does REST stand for. You answer in fifteen seconds, you feel fine, and then the interviewer asks the second question and the round actually starts.
The reason interviewers keep coming back to this area is that an API is a promise to somebody you can't call on the phone. Every design decision in it is really a decision about what a stranger's code does when things go wrong. That's a judgement question wearing a vocabulary question's clothes, and it's why the follow-up is where the round is decided.
Idempotency, which is most of the round in one word
If you prepare one thing for an API interview, prepare this. The textbook version is that GET, PUT and DELETE are idempotent and POST isn't, which is true and worth about two seconds of credit. The question underneath it is why anyone cares, and the answer is retries.
A client sends a request. The response never arrives. The client has no way to tell the difference between a request that never landed and a request that landed, succeeded, and lost its response on the way back. Those two situations look identical from outside and need opposite handling, so the client does the only thing it can: it tries again. Your API decides whether that's harmless or whether it charges somebody twice.
- Idempotent means the same result, not the same response. Deleting an already-deleted thing leaves the world in the state the caller asked for. Whether you answer 204 or 404 the second time is a separate decision, and being able to separate those two is a good signal on its own.
- The interesting endpoints are the ones that aren't naturally idempotent. Creating a payment, sending an email, posting a message. This is where idempotency keys come in: the client generates a key, sends it with the request, and you store the result against that key so a repeat returns the first outcome instead of doing the work again.
- The key has to be stored with the work, not beside it. If you write the charge and the key in two separate steps, a crash between them puts you back where you started. Saying that out loud tends to end the follow-ups, because it's the part people who have only read about idempotency keys leave out.
Customers occasionally get charged twice. Walk me through it.
Junior answer
That sounds like the request is being sent more than once, probably a double click on the button or a retry from the client. I'd disable the button after the first click, and add a check on the server for a recent identical charge on that account so we don't create a second one.
Senior answer
The double click is the easy half, and the client fix alone won't hold, because network retries produce exactly the same duplicate without anybody clicking anything. So I'd treat it as a server problem: the client sends an idempotency key with the charge, we write the key and the charge in one transaction, and a repeat with the same key returns the original result rather than starting a new one. The check-for-a-recent-similar-charge version is a race, not a fix. Two requests can both read no recent charge before either writes one, and it'll pass in testing because you can't hit that window by hand. The database is the only thing that can settle it, so the key gets a unique constraint and the duplicate loses on insert.
Both answers stop the duplicate charge in the demo. Only one of them still works at three in the morning when a proxy retries. The tell is whether the candidate reaches for a read-then-write check or for a constraint.
That read-then-write trap is the same one that runs through the database round, and interviewers notice when you recognise it in both places.
Pagination, and what moves underneath it
Usually asked as a design question: this endpoint returns a list, it's grown to a few million rows, what do you do. Page size and a limit parameter are the obvious part. The graded part is what happens while somebody is reading.
Offset pagination is the one everybody writes first. Page three is limit 20 offset 40. It's easy, it lets you jump to an arbitrary page, and it has two problems that only show up in production. Rows inserted or deleted while the user is paging shift the window, so items get skipped or shown twice, and nobody ever files that bug because it looks like the user misread the screen. And a large offset makes the database walk and discard everything before it, so page five hundred is genuinely slower than page one.
Cursor pagination gives up random access and fixes both. Instead of counting from the start you say give me the rows after this one, where the cursor encodes the sort key of the last row you saw. The database seeks straight to it, so page five hundred costs what page one costs, and inserts elsewhere in the list can't shift what you're reading.
Two more things worth having ready. Total counts are expensive on large tables and are usually the slowest part of a list endpoint, so being able to say we'd drop the exact count, or cap it, or compute it separately, shows you've paid the bill before. And the cursor should be opaque to the client: base64 and treat it as a token, so you can change what's inside it later without breaking anybody.
Versioning, which is really a deprecation question
How do you version an API. Most candidates answer the syntax question, URL path against a header, argue both sides briefly, and stop. The syntax is the least interesting part of it, and interviewers know that because they've had the argument internally and it didn't matter much either way.
The senior version starts one step earlier: most changes shouldn't need a version at all. Adding a field, adding an optional parameter, adding a new endpoint. Those are additive, and they only break clients that were written to reject anything they didn't expect, which is why every API document tells clients to ignore unknown fields. Being able to sort a change into additive or breaking, quickly and out loud, is the skill being tested.
- Breaking: removing or renaming a field, tightening validation, changing a type, changing the meaning of an existing value, making an optional parameter required.
- Additive: new fields, new endpoints, new optional parameters, a new enum value if and only if clients were told how to handle unknown ones.
- The one people get wrong: adding a new value to an existing enum. It looks additive, and it breaks every client with an exhaustive switch on that field. Whether it's safe depends entirely on what you promised, which is the point.
Then the real question. You've shipped v2. What happens to v1. An answer that stops at we'd keep both running is incomplete, because the cost of an API version isn't the routing, it's that every future change has to be made twice, in two shapes, forever. What earns credit is having a plan to end it: know who's still calling the old version, because you're logging it per client, tell them with a real date, and have a position on whether you ever switch it off for the customer who never migrates. Most companies don't, and saying so honestly is better than pretending you'd sunset a paying customer's integration on schedule.
Status codes and error bodies
This gets asked as trivia and graded as judgement. Nobody cares whether you remember 409 from 422. What they're listening for is whether you understand that a status code exists to tell the caller what to do next.
There are only about four things a caller can do with a failure: fix the request and try again, authenticate and try again, wait and try the identical request again, or give up and tell a human. Every code you choose should point at exactly one of those. That's the reason 4xx and 5xx is the split that matters most: 4xx says don't retry this as it stands, the request is the problem, and 5xx says it wasn't your fault, retrying is reasonable. Get that backwards and a client either hammers you with a request that can never succeed, or gives up on a blip.
The same logic decides the error body. A human-readable message is for a developer reading logs. A machine-readable code is for the client's code, and it's the only part you can safely promise not to change.
{
"error": {
"code": "insufficient_funds",
"message": "The card was declined for insufficient funds.",
"retryable": false,
"request_id": "req_8f21c0"
}
}A client is being rate limited. What do you send back?
Junior answer
429 Too Many Requests, with a message explaining they've hit the limit and should slow down.
Senior answer
429, and a Retry-After header, because otherwise the only sensible thing the client can do is guess. Without it a well-behaved client backs off blindly and a badly-behaved one retries in a tight loop, which is exactly the traffic you were trying to shed. I'd also send the limit and the remaining budget on normal responses, so a client can pace itself before it ever gets a 429, and I'd make sure the limiting happens before the expensive work rather than after it. A 429 that costs a database query still costs you the query.
The status code is the part everyone gets right. What the client is supposed to do with it is the part being scored, and here it's one header.
Design an API for X
The longer form of this round. Design an API for a booking system, a file upload, a notification service. It's a small system design exercise and it rewards an order, because candidates who start naming endpoints immediately end up rewriting them when the awkward requirement shows up.
- Ask who the client is. A mobile app you ship, a third-party integrator, and an internal service want genuinely different APIs, and it changes your answer on versioning and error detail.
- Name the resources before the endpoints. Get the nouns and their relationships right and most of the routes fall out on their own.
- Walk the write path first. Creation is where idempotency, validation and conflict live, and it's what the interviewer wants to talk about.
- Say what happens when it fails halfway. This is the single highest-value minute in the exercise, and most candidates never spend it.
- Cover auth, limits and pagination on anything returning a list. Briefly is fine. Missing them entirely is what gets noted.
Long-running work is the twist that comes up most. If the operation takes thirty seconds, don't hold the connection open for it. Accept the request, answer 202 with a location for a status resource, and let the client poll or take a webhook. Reaching for that shape unprompted reads as somebody who has actually shipped one.
Where REST stops being the honest answer
Worth having a view on, because it's a common closing question and defensiveness scores badly. REST maps well onto things that behave like resources. It maps poorly onto operations that aren't nouns, and forcing them through anyway is how you end up creating a resource called a cancellation that nobody ever reads.
Bulk operations are similarly awkward. One request that updates two hundred rows has no clean answer to what a partial failure looks like, and the honest version is that you pick between all-or-nothing and a per-item result list, and each one costs the client something. Say which you'd pick and why. Naming GraphQL or an RPC style as a better fit for a specific case, rather than as a general preference, tends to land well, because it shows you're choosing rather than defaulting.
None of this is obscure. All of it is one layer below where the preparation material stops, which is exactly where the second question lands.
Common questions
Do I need to know the Richardson maturity model or HATEOAS?
You should be able to say what HATEOAS is and that almost nobody implements it fully. Beyond that it rarely earns anything. Interviewers are far more interested in what your API does on a retry than in where it sits on a maturity scale.
PUT or PATCH for an update?
PUT replaces the resource, PATCH changes part of it. In practice most APIs want partial updates, so PATCH is the common answer. The follow-up worth preparing is how you tell the difference between a field the client omitted and a field it wants set to null.
How much should I say about authentication?
Enough to place it: what identifies the caller, where the credential lives, and what expiry and revocation look like. Deep token mechanics belong to the security round, and it's usually a separate conversation.
Is it a problem if I've only built internal APIs?
No, as long as you don't answer as though every client is one you control. Say that up front and reason about the third-party case out loud. Interviewers are checking that you can imagine a caller you can't ship a fix to.