REST API Interview Questions · 2026

REST API Interview Questions (2026): Design, Status Codes & Scenarios

REST is still the backbone of nearly every API interview loop in 2026, even after a decade of "GraphQL will replace it" takes. Postman's 2025 State of the API Report puts REST adoption at 93 percent among the 5,700-plus developers, architects, and executives it surveyed, more than double GraphQL's 33 percent (Postman, 2025). Whatever a team bolts on top, GraphQL, gRPC, webhooks, the resource-and-HTTP-verb model underneath is still what most engineers ship, and it's still what most interviewers test.

Here's an opinion that might be wrong: I think most REST interview prep spends too much time quizzing candidates on status code trivia, naming every 4xx code in order, and not nearly enough on the two questions that actually separate a mid-level candidate from a senior one. Should PATCH be idempotent? And when is a 404 lying to you on purpose? Neither has a memorized answer. You either understand the HTTP contract or you're guessing out loud.

This page walks through REST API interview questions the way they show up in real loops, organized by topic rather than by company, since the same questions repeat across employers once you've seen the shape of them. It leans harder on design and status codes than on auth, since that's where candidates who've shipped a few CRUD endpoints but never designed an API from scratch tend to get caught. The last section is one design scenario, the kind an interviewer hands you with fifteen minutes and a whiteboard.

52Questions
Design & Status CodesCore Topic
93%REST Share (2025)
1 full walkthroughDesign Scenario

What actually makes an API RESTful

Every loop touches this, even briefly, because it's the fastest way to tell whether a candidate has read Roy Fielding's actual 2000 dissertation or just absorbed "REST means JSON over HTTP" from a tutorial somewhere.

Easy questions

15

Six constraints: client-server separation, statelessness, cacheability, a uniform interface, a layered system, and optionally code-on-demand. Together they describe a style, not a protocol. REST isn't a standard you implement, it's a set of trade-offs a team opts into.

Almost every "REST API" skips the hypermedia part of the uniform interface, the part that would let a generic client discover what it can do next just by reading the response. Most APIs ship resources and verbs and call it a day. Not a small omission by Fielding's own definition, but it's the norm, and interviewers mostly just want to hear you know the gap exists.

Safe means the method doesn't change server state at all, GET, HEAD, and OPTIONS are safe. Idempotent means calling it once or calling it five times in a row leaves the server in the same end state either way, PUT and DELETE qualify even though they do change state.

Every safe method is automatically idempotent, since doing nothing five times is still nothing. Not every idempotent method is safe, PUT clearly changes data. POST and PATCH are the two that usually aren't either, though PATCH depends entirely on what it's patching.

401 means the request has no valid credentials at all, or the ones it sent were rejected, it's really "who are you," and a compliant 401 response should include a WWW-Authenticate header telling the client how to authenticate. 403 means the server knows exactly who you are and you still don't have permission for this specific resource or action.

Mixing these up is also a small security leak. Returning 404 instead of 403 for a resource a user can't see is a deliberate choice some APIs make so an unauthorized user can't even confirm it exists.

URIs should name resources, not actions: /orders, not /getOrders or /createOrder. The HTTP method already carries the verb, GET /orders retrieves them, POST /orders creates one, so repeating the verb in the path is redundant. It's the single most common naming mistake in real codebases.

Content-Type describes what the client is actually sending in the request body right now, application/json, say. Accept describes what representation the client wants back, application/json, application/xml, whatever the server supports. They can differ. A client can send XML and ask for JSON back.

An API key identifies the calling application or project, not a specific human. It's a shared secret with no built-in expiration or scope unless you design one in. OAuth 2.0 is an authorization framework, a set of flows for letting a user grant a third-party app limited access without handing over a password. JWT is just a token format, a signed, self-contained set of claims, that OAuth (or anything else) can choose to use as its access token, but doesn't have to.

That last part surprises people: you can run OAuth 2.0 with opaque tokens that mean nothing on their own and require a database lookup, and you can use a JWT for a plain session without OAuth anywhere in the picture. They're not the same layer of the stack.

Offset/limit (?offset=40&limit=20) is easy to build and easy to reason about, jump to page 3, jump to page 12, whatever. Cursor-based pagination (?after=eyJpZCI6NDJ9) points at a specific record instead of a numeric position and asks the server for whatever comes after it.

Filtering usually lives in query params matching a field name, ?status=active&created_after=2026-01-01. Sorting typically uses a sort param with a prefix for direction, ?sort=-created_at,name sorts by newest first, then name ascending as a tiebreaker. Neither is standardized by the HTTP spec itself, it's convention, but it's a convention nearly every public API has converged on independently.

PUT is defined against a URI the client already knows. You send PUT /users/42 and either update user 42 or create it at that exact address if it doesn't exist yet. Send the same request five times and you land in the same final state every time, so PUT is idempotent by definition.

POST is defined against a collection, and the server decides the identity of the new thing. POST /users creates a new user and the server picks the id, usually returned in a Location header. Send that same POST five times and you get five different users, because nothing about POST promises the server won't create a new resource each time. If your API design has clients guessing IDs and PUTting to them, that's a sign you're building a client-assigned-ID system, which is valid but a different contract than the usual auto-increment or generated-UUID collection.

CORS is a browser-enforced rule that stops JavaScript running on one origin from silently reading responses from a different origin unless that origin explicitly allows it. It exists because cookies get sent automatically with requests, so without CORS, a malicious site could make your browser call your bank's API using your logged-in session and read the response.

The preflight OPTIONS request only fires for "non-simple" requests, meaning anything with a custom header, a method other than GET/POST/HEAD, or a Content-Type other than form-encoded or plain text. Almost every JSON API triggers it because Content-Type: application/json counts. The browser sends OPTIONS with Access-Control-Request-Method and Access-Control-Request-Headers, and the server has to answer with matching Access-Control-Allow-* headers before the browser will even attempt the real request. A missing or wrong Access-Control-Allow-Origin on that OPTIONS response is the single most common cause of "it works in Postman but not in the browser" tickets.

The client that just POSTed to create a resource doesn't know its final URI yet, especially if the server generated the ID. The Location header tells the client exactly where to GET that new resource going forward, so it doesn't have to guess or parse an ID out of the response body and construct the URL itself.

It also matters for tooling that isn't your own frontend. Browsers, HTTP clients, and API gateways all understand Location as the canonical "The point you just made" pointer, separate from whatever fields happen to be in the JSON body. If you skip it, every consumer of your API has to invent their own convention for finding the new resource, which usually means digging an id field out of the response and string-concatenating a URL, which breaks the moment your URL structure changes.

500 means your application code hit an unhandled exception, a bug, a null reference, an unexpected database error, something the server didn't anticipate and couldn't recover from gracefully. It's a signal that something in your code needs fixing.

503 means the server is intentionally not able to handle the request right now, usually because it's overloaded, in the middle of a deploy, or a downstream dependency it needs is down. A well-behaved 503 comes with a Retry-After header telling the client roughly when to try again. The distinction matters for monitoring and for clients: a spike in 500s should page an engineer, a 503 during a known maintenance window shouldn't, and clients should generally retry a 503 with backoff but not blindly retry a 500 without understanding why it happened first.

Content negotiation is the mechanism where the client and server agree on the format of the response without needing separate URLs for each format. The client sends an Accept header like Accept: application/json or Accept: application/xml, and the server picks a representation it can produce that matches, then confirms what it actually sent back with the Content-Type header on the response.

Most modern APIs negotiate on format only in theory since almost everyone just returns JSON, but the same mechanism is used in practice for language (Accept-Language), compression (Accept-Encoding, where the server responds with Content-Encoding: gzip), and versioning schemes that put the version inside a custom media type like application/vnd.myapi.v2+json. If a server can't produce anything the client asked for, the correct response is 406 Not Acceptable, though in practice most APIs just ignore an Accept header they don't support and return JSON anyway rather than failing the request outright.

HEAD asks the server for exactly the headers a GET would return, but with no body. The server runs the same logic it would for GET, including status code and Content-Length, and just leaves the body off the wire.

Clients use it to check things cheaply. A client can HEAD a large file to read Content-Length before deciding whether to download it, check Last-Modified or ETag to see if a cached copy is still fresh without pulling the whole payload, or verify a resource exists with a 200 versus 404 before doing something more expensive. It's underused in practice because most teams never implement it explicitly, but if your framework maps HEAD to the same handler as GET and just strips the body, you get it for free.

/users is a collection, GET returns a list, POST creates a new member, and each item in it gets its own address like /users/42. /users/me is a singleton, a single, always-present resource scoped to whoever is authenticated, and it doesn't take an ID because there's only ever one answer for a given caller.

The practical reason singletons like /users/me or /account/settings exist is convenience and security. The client doesn't need to know its own user ID to fetch its profile, which avoids a class of bugs where a client accidentally requests someone else's ID, and it means the server can enforce "you can only ever see your own data" at the routing layer instead of relying on every handler to re-check ownership. The tradeoff is that /me isn't cacheable the same way a normal resource is, since the response depends on who's asking, so it needs Cache-Control: private or no caching at all.

Medium questions

24

Every request has to carry everything the server needs to process it, auth credentials, any context, all of it. The server isn't allowed to remember anything about the client between requests. Whatever looks like session state, a logged-in user, a pagination cursor, travels with the request itself instead of living in server memory tied to a session ID.

The common mistake is a server-side session object kept in memory and pinned to one server instance through sticky load balancing. That's stateful, full stop, and it's exactly what breaks the moment a team tries to scale horizontally, since a request has to keep landing on the one server that remembers who you are.

Identification of resources through URIs, manipulation of those resources through representations, self-descriptive messages (a response says what it is, usually through Content-Type, without extra out-of-band explanation), and hypermedia as the engine of application state.

It's the constraint that most defines whether something feels RESTful, because it would let a generic HTTP client talk to any compliant API without prior knowledge of it, at least in theory. In practice almost nobody builds to that theory, which is its own kind of answer if an interviewer asks why.

Not by definition, and this is the question that catches candidates who memorized a table instead of understanding it. A PATCH that sets a field to a specific value, {"status": "shipped"}, is idempotent, running it twice ends in the same state. A PATCH that increments a counter, {"op": "increment", "field": "views"}, is not, running it twice adds two instead of one.

Earlier in this section PATCH got grouped with POST as "usually not idempotent." That's not quite right, it depends entirely on the patch document, not the verb itself. RFC 9110 leaves it implementation-defined for exactly that reason (RFC 9110).

Idempotency is about the server's end state, not the response you get back. The first DELETE removes the resource and probably returns 204 No Content. The second DELETE finds nothing there anymore and returns 404. Different status code, identical outcome, the resource doesn't exist either way.

That distinction, same effect versus same response, is exactly what trips people up when they try to define idempotency from memory instead of from the mechanism underneath it.

200 OK is the generic "it worked, here's a body" response. 201 Created is specifically for a successful POST that made a new resource, and it should come with a Location header pointing at the new resource's URI. 204 No Content means it worked and there's deliberately nothing to send back, a successful DELETE, or a PUT that doesn't need to echo the updated resource.

A 204 response with a body in it is a spec violation, not just a style nitpick. Some HTTP clients and proxies will choke on it or silently drop the body, since 204 tells them not to expect one.

400 means the server couldn't even parse what it received, malformed JSON, a missing required header, garbage syntax. 422 means the request was syntactically fine, valid JSON, correct field names, but failed a business rule, an email field that isn't actually a valid email, an end date before a start date.

422 technically comes from WebDAV, RFC 4918, not the core HTTP spec, a fun fact that trips up interviewers as often as candidates. It's become the de facto standard for validation failures anyway. Rails-style APIs, and plenty of scaffolded validation layers elsewhere, lean on it.

Nest resources to express a real ownership relationship, /users/123/orders makes sense because an order genuinely belongs to a user. Past two levels deep, though, it usually gets unreadable and brittle. /users/123/orders/456/items/789/notes is a URI nobody wants to construct or debug.

Past that depth, flatten it. /items?order_id=456 does the same job as a nested path and reads better, and it keeps items addressable on their own instead of only reachable through a chain of parent IDs.

Path segments identify what resource you mean and where it sits in a hierarchy, the parts that change the meaning of the request entirely. Query parameters modify how you want that resource returned, filtering, sorting, pagination, optional fields.

A quick test: if removing it would point at a completely different resource, it belongs in the path. If removing it just changes the shape of the same resource's response, it belongs in the query string.

406 Not Acceptable means the server can't produce any representation matching what the Accept header asked for, so it fails on the way out. 415 Unsupported Media Type means the server can't process what the client actually sent in Content-Type, so it fails on the way in. Same family of problem, opposite direction.

Authorization code (paired with PKCE for public clients, mobile apps, single-page apps) is the default for a user logging in through a browser redirect. Client credentials is for machine-to-machine calls where there's no user at all, one backend service calling another. The implicit grant, which returned a token directly in a redirect URL, is now discouraged by current OAuth guidance, since it exposed the token in browser history with no way to verify who requested it. RFC 6749 still defines all three grants formally, even though the ecosystem has moved past recommending the implicit one (RFC 6749).

If a new row gets inserted while a user is paging through results sorted by creation date, offset pagination can show them the same item twice, or skip one entirely, because "item number 40" now refers to something different than it did a request ago. Cursor pagination doesn't have this problem, since it's anchored to a specific record's position, not a shifting number.

The cost: cursor pagination can't jump straight to "page 7," only forward or backward from where you already are. Fine for a feed that scrolls. A real trade-off worth naming out loud for an admin table with page numbers.

The server returns 429 Too Many Requests once a client crosses its limit, usually alongside a Retry-After header telling the client how long to back off. Most APIs also expose X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers on every response, limited or not, so a well-behaved client can slow itself down before it ever gets a 429.

Token bucket and sliding window are the two algorithms that come up most. Token bucket allows short bursts as long as the average stays under the limit, which matches real traffic better than a rigid fixed window that resets hard every 60 seconds and lets a client burn its entire quota in the first second.

Hypermedia as the Engine of Application State means a response includes links to what you can do next, not just the data itself. An order response might include a "cancel" link only if that order is still cancellable. It's level 3 of the Richardson Maturity Model, above plain resources and HTTP verbs.

I'll say something that might get pushback: I think HATEOAS is close to irrelevant for most interview prep, because almost nobody ships it. Know the definition, know it exists, and move on. Don't spend a weekend building hypermedia controls for an API that's never going to use them.

REST typically over-fetches (an endpoint returns a whole user object when you only needed the name) or under-fetches (three separate calls to assemble one screen). GraphQL lets the client specify exactly the fields it needs in one request, fixing both problems at the cost of HTTP-level caching, since every GraphQL request is usually a POST to the same endpoint, and a naive resolver can quietly trigger an N+1 query problem a REST endpoint's fixed shape would never allow.

gRPC runs over HTTP/2, uses Protocol Buffers instead of JSON, and is built for fast, strongly-typed service-to-service calls inside a system a team controls end to end. REST's plain-text JSON is slower to parse and bigger over the wire, but it's readable in a browser tab and debuggable with curl. Pick gRPC for internal microservice traffic where every millisecond matters. Pick REST for anything public-facing where a third-party developer needs to read the docs and just try it.

The strongest pattern is a predictable error body regardless of endpoint, something close to {"type": "...", "title": "...", "status": 422, "detail": "...", "instance": "..."}, which is roughly what RFC 7807's Problem Details format standardizes. Include a request or correlation ID in every error response too, so a user reporting "it broke" gives you something to grep the logs for instead of a screenshot and a timestamp that's off by your server's clock skew.

An ETag is an opaque fingerprint of a resource's current state, usually a hash of the content or a version number, that the server sends back on a GET response. The client stores it and, on the next request for the same resource, sends it back in an If-None-Match header. If nothing changed, the server responds 304 Not Modified with an empty body instead of resending the whole payload, and the client just reuses its cached copy.

The bandwidth savings are nice but the bigger use is concurrency control on writes, covered separately. For plain caching, the win is correctness over a naive time-based cache: Cache-Control: max-age assumes the content won't change within that window, but ETags let you revalidate on every request cheaply and only pay the full payload cost when something actually changed. The gotcha is that ETags need to be generated consistently, if two app servers behind a load balancer compute slightly different hashes for the same logical content, clients will get spurious cache misses or, worse, false matches if the hash is weak.

Optimistic concurrency assumes conflicts are rare, so instead of locking a row while someone edits it, you let everyone read freely and only check for a conflict at write time. The client fetches a resource, gets back an ETag representing its current version, and when it later sends a PUT or PATCH to update it, it includes that ETag in an If-Match header.

The server compares the If-Match value against the resource's current ETag before applying the write. If they match, nobody else has changed it since the client last read it, so the write proceeds and a new ETag is generated. If they don't match, the server rejects the write with 412 Precondition Failed instead of silently overwriting someone else's change.

The client's job on a 412 is to re-fetch the current state, decide how to reconcile its intended change with what changed underneath it, and retry with the new ETag. This is the standard fix for the lost update problem without resorting to database-level pessimistic locks, which don't scale well across a stateless API where a client could hold a "lock" indefinitely by never sending the follow-up request.

A sparse fieldset is a client-specified filter on which fields a response includes, usually via a query parameter like GET /users/42?fields=id,name,email instead of the full resource with every field the server knows about.

The main reason to support it is mobile clients and dashboards that only need a handful of fields out of a resource that might carry dozens, including expensive-to-compute or heavy ones like embedded images or long text blobs. Fetching the full object every time wastes bandwidth on a slow connection and CPU on the server if some fields require extra joins or computation to populate. The tradeoff is API surface complexity: every field becomes technically optional in the response, caching gets harder since the same URL can now return different shapes depending on the fields parameter, and you need a clear rule for what happens when a client asks for a field that doesn't exist, usually just silently omitting it rather than erroring.

Three round trips for one screen is the classic REST N+1 problem, and it gets worse linearly if you're rendering a list of ten posts and need the author for each one. The usual fix is a query parameter that tells the server to embed related resources inline, something like GET /posts/9?include=author,author.recentPosts, borrowed from the JSON:API spec's include parameter.

The server resolves all of that server-side in one request, typically with a single batched query or a data-loader pattern to avoid its own N+1 problem hitting the database, and returns a nested or a flat "included" section in the response depending on which convention you follow. The tradeoff against embedding everything by default is that responses get heavier and harder to cache, since now /posts/9 means something different depending on what was requested. This is one of the places where teams reach for GraphQL instead, since arbitrary nested field selection is what it's built for, but a well-designed include parameter gets you 80% of the benefit without a new query language.

Polling makes sense when events are frequent, latency doesn't matter much, or the consumer can't expose a public endpoint. Webhooks make sense when events are relatively rare and the consumer needs near-real-time notification, since it avoids the consumer hammering your API every few seconds asking "anything new yet."

A webhook that just POSTs a payload to a URL and hopes for the best isn't production-grade. You need a signature, usually an HMAC of the raw request body using a shared secret, sent in a header like X-Signature, so the receiver can verify the request actually came from you and wasn't forged or replayed with a tampered body. You need retries with exponential backoff when the receiver's endpoint is down or slow, capped at some max attempt count, and ideally a dead-letter or manual-replay mechanism for events that never get delivered. You also need idempotency on the receiving end, since retries mean the same event can arrive twice, so every webhook payload should carry a unique event ID the consumer can dedupe against.

Multipart/form-data uploaded straight to your API server is the simplest to implement and works fine for small files, but it means your app servers are holding open connections and buffering large payloads, which doesn't scale well and burns memory on instances that should be doing application logic, not acting as a file proxy.

Base64-encoding the file and putting it inside a JSON body is the worst of the common options, since base64 inflates size by roughly 33%, and most JSON parsers aren't built to stream multi-megabyte string fields efficiently, so you end up holding the whole thing in memory twice.

Presigned URLs are the standard production pattern: the client calls your API to get a short-lived, signed URL directly to object storage like S3, uploads the file straight there, and then notifies your API with a second request once it's done (or your API gets notified via an S3 event). This keeps large binary data off your application servers entirely, and the only real complication is handling the case where a client gets a presigned URL and never actually uploads, which needs some kind of cleanup job or expiring pending-upload record so you don't leak orphaned "reserved" entries in your database.

You announce it before you touch anything, using the Deprecation header (a date the field or endpoint became deprecated) and, ideally, a Sunset header giving the actual date it stops working, both on the still-functioning old endpoint. Some teams also add a Link header pointing to the replacement, so tooling and not just humans reading docs can discover the migration path.

You keep the old behavior fully working through that whole window, you don't quietly change response shape or semantics while it's "deprecated but still live," because that breaks trust and clients will stop believing your deprecation notices mean anything. You track actual usage of the deprecated endpoint, not just assume nobody's calling it, since undocumented internal tools and old mobile app versions still stuck in app stores are the usual reason a "deprecated six months ago" endpoint still gets hit in production. Only once usage drops to near zero, or the sunset date passes and you've made a real effort to contact remaining callers, do you actually remove it, and even then a 410 Gone with a clear message is kinder than a bare 404.

max-age=N tells any cache, browser or shared, that the response is good to reuse without revalidating for N seconds. Use it on public, non-personalized data that doesn't change often, like a product catalog endpoint.

private tells caches that the response is specific to one user and must not be stored in a shared cache like a CDN or corporate proxy, only in the requesting browser's own private cache. This is the one people forget on personalized endpoints, GET /api/dashboard or GET /api/cart, and forgetting it is exactly how one user's cached response ends up served to a different user sitting behind the same CDN edge node or corporate proxy.

no-store is the strongest directive, meaning don't cache this anywhere, not even briefly, not even in the browser's memory cache. Use it for anything with sensitive data you don't want lingering anywhere after the response is rendered, like a one-time token or a page showing full payment details. A common combination for authenticated, personalized API responses is Cache-Control: private, no-store, so nothing caches it and nothing shares it across users.

Hard questions

9

The order was probably created just fine, the response just never made it back before the client gave up and retried. POST isn't idempotent by default, so the naive retry creates a second order with identical data.

The fix is an idempotency key: the client generates a unique key once (a UUID works) and sends it in a header. The server stores which keys it's already processed, and if the same key shows up again, it replays the stored response instead of creating a second order. Stripe popularized this pattern, and it's the answer most interviewers are fishing for once they set up the "client retried" scenario.

http
POST /orders HTTP/1.1
Host: api.example.com
Content-Type: application/json
Idempotency-Key: 8f14e45f-ceea-4c9d-9a1e-3f8b2b0f9a12

{
 "sku": "WIDGET-42",
 "quantity": 3
}

Bad idea, and I'll take a stand on this one. The moment an API returns 200 for a failure, every generic tool that keys off status codes, monitoring dashboards, retry middleware, HTTP caches, browser fetch() error handling, stops working correctly without custom code peeking inside every response body first. (Side note: I've seen a production payment API do exactly this, return 200 with a failure flag buried three levels into the JSON. It turned a five-minute alert into a half-day incident review.) Status codes exist so a machine doesn't have to parse your body to know something broke. Use them.

Three common approaches: URI versioning (/v1/orders), a custom header or Accept-based versioning (Accept: application/vnd.example.v2+json), and a query parameter (?version=2). Each has a real cost. URI versioning is the least "pure" by REST theory, since a resource's identity shouldn't change with its representation, but it's the easiest to test with a browser or curl and the easiest for a new engineer to understand at 2am during an incident.

My honest take: I'd pick URI versioning over header versioning almost every time, for a boring reason, visibility beats purity. Header versioning is invisible in logs and invisible to anyone grepping access logs for which version a client is hitting. That convenience shows up during outages, not design reviews, exactly when nobody remembers to check it.

Here's the tension nobody mentions until it bites them. A JWT is self-contained specifically so the server doesn't have to look anything up to validate it, that's the whole performance win. But that also means there's no clean way to revoke one before it expires, the server has no record of it to delete.

Real fixes are workarounds more than solutions: keep expiration short (minutes, not days) and lean on a refresh token you can revoke server-side, or maintain a deny-list of revoked IDs, which quietly brings back the database lookup you were trying to avoid. No clean answer here, just a trade-off every team has to pick a side on.

The same idempotency-key pattern from the earlier POST /orders question, applied to an action that's arguably higher-stakes than a duplicate order. The client attaches an Idempotency-Key header generated once, before the first attempt, and the server checks it against a short-lived record of keys it's already processed before doing anything else. A duplicate key returns the stored result instead of re-running the send.

Interviewers running this scenario are checking one thing: does the candidate reach for this pattern unprompted, or only after being told point-blank "the client retried and now there are two applications." Naming it first is the tell of someone who's shipped this failure mode, not just read about it.

This is mass assignment. Somewhere in the code, the request body is being bound directly onto the user model or passed straight into an update query with something like User.update(req.body), which means any field the model has, role included, gets written if the attacker simply adds it to the JSON they send, regardless of what the frontend form actually exposes. The frontend never sending that field doesn't matter, because nothing stops a client from sending a raw HTTP request that the frontend never would.

The fix is an explicit allowlist between the request and the model, never binding the raw body straight onto anything persisted. A DTO or a validation schema that only recognizes displayName and rejects or ignores unknown fields closes the hole completely, because now role simply isn't a field the update path knows how to accept, whether it's an accident from a bug in the frontend or a deliberate attempt to escalate privilege.

// vulnerable
User.update(userId, req.body)

// fixed: only pull known-safe fields off the body
const { displayName } = req.body
User.update(userId, { displayName })

Frameworks with strong typing and schema validation, think a Zod or a Pydantic model that only declares displayName as a field, catch this by construction rather than relying on a developer remembering to allowlist manually every single time a new endpoint gets written.

This is the classic lost update problem. Both agents fetched the ticket, got the same starting state, and each PATCH request just writes its own version of the full or partial object back without knowing the other request happened in between. There's no error because nothing about the request was invalid, the server just processed two valid writes in sequence and the second one clobbered the first with no idea a conflicting write had just occurred.

The fix is optimistic concurrency using ETags and If-Match, covered in more detail elsewhere: agent A's GET returns an ETag, agent A's PATCH must include that ETag in If-Match, and if agent B's write already landed and changed the resource's ETag in between, agent A's PATCH gets rejected with 412 Precondition Failed instead of silently succeeding and overwriting agent B's change. That forces the client, and by extension the agent, to see a fresh copy and decide how to reconcile before writing again, rather than the system quietly picking whichever request happened to arrive last as the "winner."

Without that mechanism, the only alternative is pessimistic locking, actually locking the row or the ticket while someone's editing it, which works but creates its own headaches in a stateless API, since you now need some way to release a lock if the client that acquired it crashes or never comes back, usually a lock timeout, which reintroduces a version of the same race just on a longer timescale.

This is almost always a caching layer, a CDN or reverse proxy, serving a cached response for one user to a completely different user because the cache key didn't account for who was asking. If /api/dashboard returned Cache-Control: max-age=60 or something equivalent without also telling the cache the response varies per user, the CDN treats the URL as the entire cache key, stores the first user's response, and happily serves that exact same cached body to the next hundred people who hit the same path within the cache window, cookies and Authorization header completely ignored by the cache layer.

The fix has two parts. First, any endpoint whose response depends on the authenticated user needs Cache-Control: private, no-store or at minimum no-cache, so shared caches never store it in the first place. Second, if you legitimately do want to cache per-user responses at the edge for performance, the cache needs a Vary header, commonly Vary: Authorization or Vary: Cookie, so it stores a distinct cached copy per distinct value of that header instead of one shared copy for the URL. Most CDNs don't vary on Authorization by default and won't cache authenticated responses unless explicitly configured to, which is exactly why this bug is usually introduced by someone adding aggressive caching to speed up a slow endpoint without realizing that endpoint was personalized.

Adding a new optional field to a response, adding a new optional query parameter, or adding a whole new endpoint are all non-breaking, since any client written against the old contract keeps working unchanged. Removing a field, renaming a field, changing a field's type (a string ID becoming a number, or vice versa), changing an enum's set of valid values, making a previously-optional request field required, or changing the meaning of an existing status code are all breaking, even if the change feels small on your end.

The one that catches teams most often is changing a field from nullable to always-present or the reverse, and reordering or restructuring nested objects, since plenty of client code does naive property access that silently returns undefined and continues rather than throwing, which means the bug doesn't surface as an error, it surfaces weeks later as "why is this value blank for some users."

The way to catch this before shipping instead of after is contract testing against a schema, typically an OpenAPI spec checked into the same repo as the API, with a CI step that diffs the current spec against the previous release and fails the build on anything that matches a known-breaking pattern, removed fields, changed types, newly-required fields. Pair that with consumer-driven contract tests, where each client team maintains a small suite asserting the exact shape they depend on, and the API can't merge a change that breaks any registered consumer's contract without that consumer explicitly updating their own test first.

Real-time scenario questions

4

You don't just accept an array on POST /orders and pretend it's the same endpoint, because the semantics of success and failure change completely. With a single resource, POST either succeeds or fails. With 500 items in one request, you need to decide what happens when item 217 fails validation, whether the whole batch rolls back or the other 499 still get created.

Most production designs make this an explicit different endpoint, like POST /orders/bulk, that always returns 207 Multi-Status (or 200 with a per-item results array if you don't want to deal with 207's XML-flavored history) containing an array where each entry has its own status and either the created resource or an error. This keeps a client from having to guess whether an all-or-nothing transaction happened. It's also worth capping batch size explicitly, both to protect the request from timing out and to keep a single slow item from stalling everything behind it, and being explicit in the docs about whether processing is synchronous or whether the bulk endpoint just returns a job ID for polling.

Start with one resource, /applications, representing the drafted item in a pending_review state. GET /applications?status=pending_review lists what's waiting, paginated with a cursor since this list grows constantly and a human is scrolling it live. PATCH /applications/{id} with {"status": "approved"} or {"status": "rejected"} updates the review decision, and it should be idempotent, calling it twice with the same status is a no-op that just returns the current state, not an error and not a duplicate action.

The actual send is a separate step, and that's the part candidates skip. Approving a record and sending it carry different blast radii, one flips a flag, the other has a real external side effect that can't be undone. Modeling "send" as its own action resource, POST /applications/{id}/send, keeps that side effect explicit instead of hiding it inside a PATCH.

http
PATCH /applications/48291 HTTP/1.1
Content-Type: application/json

{ "status": "approved" }

HTTP/1.1 200 OK
Content-Type: application/json

{ "id": 48291, "status": "approved", "sent": false }

You don't hold the HTTP connection open for 90 seconds, that ties up a server thread or connection slot the whole time and falls apart the moment a load balancer's own timeout, often 60 seconds by default, kills the connection first. Instead, POST /reports returns immediately with 202 Accepted, a job resource with a status of pending, and a Location header pointing at GET /reports/jobs/{id} where the client can check progress.

That polling endpoint returns the current status (pending, running, done, failed) and, once done, either the report data directly or a Location pointing at the finished resource. To stop clients from hammering it every 100ms, the server includes a Retry-After header on the response telling the client how long to wait before checking again, and well-behaved clients respect it. For clients that don't respect it, or third parties you don't control, you rate-limit the polling endpoint itself same as any other, and some teams add a webhook option as an alternative to polling entirely, so a client that registered a callback URL just gets notified when the report's ready instead of polling at all.

202 Accepted
Location: /reports/jobs/abc123
Retry-After: 5

{"id": "abc123", "status": "pending"}

REST is built around request-response over a resource: the client asks, the server answers once, the connection's done. There's no native concept of the server pushing updates to a client that hasn't asked again, which is fine for a CRUD API but breaks down the moment you need something like live order status, a chat feed, or live metrics, where the whole point is the server telling the client something changed without being asked.

Naive polling, hitting GET /orders/9/status every second, technically works but wastes requests, adds latency up to the polling interval, and doesn't scale if you've got thousands of clients all polling the same handful of hot resources. Server-Sent Events bolt a long-lived, one-directional stream onto an otherwise normal HTTP GET, the connection stays open and the server pushes events down it as they happen, which is a reasonable middle ground when you only need server-to-client updates and don't need the client to send anything back over the same connection. Long polling is the older version of the same idea, where the server just holds a GET request open until there's something to report instead of returning immediately.

For anything genuinely bidirectional, WebSockets or a separate protocol entirely is usually the honest answer, and most systems end up with a hybrid: REST for anything that's a normal request, a WebSocket or SSE channel for the subset of data that's actually live, rather than trying to force everything through one model.

How to prepare for a REST API interview in 2026

Skip another flashcard deck of status codes. Build one small API with three or four resources, add real pagination, add a PATCH endpoint, then deliberately break your own idempotency guarantees, call PATCH twice, call POST twice after killing the connection mid-response, and watch what happens. Reading the definition of idempotent doesn't teach you nearly as much as watching a duplicate order show up in your own database.

Across API-design mock interviews run through LastRoundAI's backend track, the follow-up that trips up the most candidates isn't "what's the difference between PUT and PATCH." It's "you just told me PATCH is idempotent, now design one that actually holds to that." Candidates who define idempotency cleanly freeze the moment they have to prove it in code. We don't have a clean percentage, only that it's the most repeated stumble in sessions tagged "API design" since March. That's the gap most REST API interview questions lists skip, a duplicate-order bug is harder to fake than a status code table.

On REST vs GraphQL vs gRPC specifically: know the trade-offs cold, but don't over-prepare a side you're not being hired for. If the job posting says REST, spend the rest of your prep time on status codes and idempotency, not on memorizing GraphQL's N+1 fixes nobody's going to ask about.

Practice the design question, not just the definitions

Reciting the difference between 401 and 403 is not the same as defending a resource model when an interviewer changes one requirement on you mid-answer. LastRoundAI's mock interview mode runs backend and API-design rounds with real-time follow-up questions instead of a static bank, and the free plan includes 15 credits a month that reset monthly, so there's no reason to hoard them for "the real interview."

If the harder part right now is getting in front of enough backend or API-focused roles rather than passing the interview once you land one, Auto-Apply queues tailored applications for your review, 10 a month on the free plan, up to 400 a month on the Ultimate plan, and every single one sits in a review queue until you approve it. Nothing goes out without you looking at it first, the same principle this page just spent nine sections defending.

Questions about either product go to contact@lastroundai.com. That's the only inbox we check.

How this list was built

Worth being straight about where these questions come from, because plenty of pages in this category are not. The set was compiled from a research pass across official documentation, vendor release notes, published engineering writing and public discussion of hiring processes, then cross-checked against the current version of each technology so nothing here describes behaviour that has since changed.

What that means in practice: these are the questions the material supports as reasonable and current for this role, not a transcript of any one company's loop. We have not sat in on your interview and we are not going to claim we have. Treat the list as well-sourced preparation rather than a leaked question bank, and expect your panel to phrase things their own way.

If you spot something out of date, tell us at contact@lastroundai.com and we will fix it.

Frequently asked questions

Do I need hands-on REST API experience to pass?

It shows quickly either way. Textbook answers hold up until the interviewer asks what you did when it broke, and that is usually the question that separates candidates. A small real project you can discuss honestly beats a longer list of familiarity claims.

Is REST API still worth learning in 2026?

For interview purposes the question is really whether the teams you are targeting use it, which is worth checking against their actual job postings rather than general popularity rankings. Where it is in use it tends to be deeply embedded and slow to replace.

Should I memorise REST API syntax for the interview?

Rarely worth it. Most interviewers care that you know what to reach for and why, and will not fail you for forgetting an exact flag. Being confidently wrong about behaviour costs far more than admitting you would check the documentation.

What is the most common mistake in REST API interviews?

Answering the question that was asked and stopping there. The strongest candidates add the trade-off or the failure mode without being prompted, which is what signals real use rather than revision.

Leave a Reply

Your email address will not be published. Required fields are marked *