Stack Overflow's 2024 Developer Survey found that 90 percent of professional developers reach for official API or SDK documentation as a primary way to solve problems on the job, ahead of almost everything else on the list (Stack Overflow, 2024). That number tells you something about what an API developer interview loop is actually testing. It isn't testing whether you memorized a framework. It's testing whether you can read a contract between two systems that don't trust each other, and defend a design choice when someone pushes back on it.
Here's a take that might be wrong: I think "API Developer" as a standalone job title is quietly disappearing, folded into "Backend Engineer" or "Platform Engineer" at most companies now. The postings that still use the narrower title tend to run a narrower loop too, heavier on HTTP semantics, auth, and status codes, lighter on distributed systems and infrastructure. If your posting says API Developer specifically, weight your prep that way.
This guide compiles 46 API developer interview questions across four areas that show up across REST-heavy, GraphQL-adjacent, and hybrid loops in 2025 and 2026: design fundamentals, authentication and security, status codes and error handling, and versioning, pagination, and performance. Every question is difficulty-tagged. If you want the deeper Fielding-constraints treatment of REST specifically, LastRoundAI's REST API interview questions guide goes further on that one topic alone; this page covers more ground at less depth per topic.
REST and API design questions
Design questions are where an interviewer figures out if you've actually shipped something or just used one. They don't take long to ask, but a sloppy answer here usually predicts a sloppy answer everywhere else.
Easy questions
16PUT replaces the entire resource and is idempotent, sending the same PUT five times in a row leaves the resource in the same end state. PATCH applies a partial update and isn't idempotent by definition, though a specific patch document (setting a field to an exact value) often behaves idempotently in practice anyway.
The follow-up that actually separates candidates: what happens to fields you leave out of a PUT body? A correct PUT implementation resets them to their defaults or nulls them out, since PUT means "this is now the entire resource," not "update whatever I mentioned." Candidates who treat PUT like a partial update get caught here almost every time.
Roy Fielding's six constraints: client-server separation, statelessness, cacheability, a uniform interface, a layered system, and optionally code-on-demand. Together they describe a style a team opts into, not a protocol you implement by checking boxes.
Nearly every production 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. Saying so out loud is usually the right answer. Interviewers mostly want to hear that you know the gap exists, not that you've built HATEOAS into a side project nobody uses.
URIs name resources, not actions: /invoices, not /getInvoices or /createInvoice. The HTTP method already carries the verb, so repeating it in the path is redundant and it's the single most common naming mistake in real codebases, including plenty that otherwise look well designed.
Hypermedia as the Engine of Application State means a response includes links to what you can do next, an order response including a "cancel" link only when the order is still cancellable, for instance. It's level 3 of the Richardson Maturity Model.
Almost nobody ships it. (Side note: I've never once heard an interviewer ask a candidate to recite the Richardson Maturity Model by name. They ask about HATEOAS directly and see if you connect it back.) Know the definition, know it exists, and move on with your prep time.
JWTs are stateless and self-contained, so validating one doesn't require a database lookup, which is the entire performance argument for using them. The cost: you can't revoke a JWT before it expires without maintaining a deny-list, which quietly brings back the database lookup you were trying to avoid in the first place.
Session tokens require a shared store (Redis, a database) but let you revoke instantly by deleting the session record. Neither option is strictly better. It depends on whether instant revocation or stateless validation matters more for the specific system.
An API key identifies the calling application, not a specific human, and it's a shared secret with no built-in expiration 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. A JWT is just a token format that OAuth (or anything else) can choose to use, but doesn't have to.
That last part surprises people in interviews. You can run OAuth with opaque tokens that require a database lookup, and you can use a JWT for a plain session with no OAuth anywhere in the picture.
Authentication answers "who are you." Authorization answers "what are you allowed to do." Most API security bugs aren't authentication failures, the login usually works fine, they're authorization failures: a valid, logged-in user reaching a resource or action they were never supposed to reach.
CORS is a browser-enforced rule, not a server-side security boundary, it stops a browser from letting JavaScript on one origin read a response from another unless the server explicitly allows it. The common misconfiguration is Access-Control-Allow-Origin: * combined with Access-Control-Allow-Credentials: true, a combination browsers actually reject outright, but plenty of teams get partway there with an origin reflection bug that accomplishes the same unsafe thing.
200 OK: a successful GET, PATCH, or PUT that returns a body. 201 Created: a successful POST that made a new resource, ideally paired with a Location header pointing at it. 204 No Content: it worked and there's deliberately nothing to send back, a typical DELETE response.
400 Bad Request: the server couldn't even parse what it received. 401 Unauthorized: no valid credentials were provided. 403 Forbidden: the server knows exactly who you are, and you still can't do this. 404 Not Found: the resource doesn't exist, or the server wants an unauthorized caller to think it doesn't. 422 Unprocessable Entity: valid syntax, failed business rules. 429 Too Many Requests: you're rate limited. 500 Internal Server Error: something broke on the server's side, not yours.
401 means "who are you," no valid credentials were supplied, or the ones sent were rejected outright, and a compliant 401 response should include a WWW-Authenticate header telling the client how to authenticate. 403 means the server knows who you are and you still don't have permission for this specific resource or action.
Mixing these up leaks information. Returning 404 instead of 403 for a resource an unauthorized user shouldn't even know exists is a deliberate choice plenty of APIs make on purpose, not a mistake.
Rarely, and mostly for resources that have permanently moved, 301, or for a POST that should redirect the client to check the status of a newly created async job, 303 See Other. Most API clients (fetch libraries, SDKs, curl scripts) don't follow redirects the way browsers do by default, so leaning on 3xx for anything beyond "this URL moved" tends to surprise people who didn't expect to need redirect-following logic in their HTTP client config.
A 4xx means the client sent something the server can't or won't act on, bad input, missing auth, no access. A 5xx means the server itself failed to do its job even with a perfectly valid request. Alerting on 5xx rates makes sense, that's your bug. Alerting on 4xx rates the same way just tells you clients are sending bad requests, which might be a documentation problem, not an incident.
Because "it broke" with a screenshot and a timestamp that's off by whatever your server's clock skew happens to be gives support and engineering almost nothing to search for. A request ID in the error body, echoed back from a header the server generated, turns that into a single log query instead of a guessing game across a distributed system.
CPU time on the server to compress, and CPU time on the client to decompress, both usually cheap compared to the network time saved on anything but tiny payloads. Brotli compresses tighter than gzip but costs more CPU to do it, which matters if you're compressing at high request volume rather than once and caching the compressed result.
An index on whatever column the API's most common query filters or sorts by. It sounds almost too simple to be an interview answer, but a missing index on a hot query path is a more common real-world cause of API slowness than anything to do with the application layer, caching strategy, or framework choice.
Distributed tracing with per-span timing, so a single slow request shows you exactly which database call, external API call, or serialization step ate the time, instead of one aggregate number for the whole request. Guessing based on "the code looks slow" wastes far more engineering time than instrumenting the request path once and looking at the actual trace.
Medium questions
26Returning 100 orders and firing 101 database queries, one to fetch the orders and one more per order to fetch its line items, is the classic case. It shows up any time a response nests related data that got fetched lazily instead of together.
Three real fixes, and they're not interchangeable: eager loading (a join, or a single query with the relation preloaded), a dedicated batch endpoint that accepts a list of IDs, or, in GraphQL, a DataLoader-style batching layer that coalesces individual resolver calls into one query per tick of the event loop.
-- N+1 (bad): one query per order
SELECT * FROM orders WHERE user_id = 42;
-- then, for each order returned:
SELECT * FROM line_items WHERE order_id = ?;
-- fixed: single join
SELECT o.*, li.*
FROM orders o
JOIN line_items li ON li.order_id = o.id
WHERE o.user_id = 42;POST isn't idempotent by default, so a client retry after a timeout can create a duplicate. The fix is an idempotency key: the client generates a unique string once, before the first attempt, and sends it in a header. The server stores which keys it's already processed and replays the stored response for a repeat instead of running the operation twice.
Stripe popularized this pattern for payments specifically, which makes sense, a duplicated payment is a much worse outcome than a duplicated log line.
POST /payments HTTP/1.1
Host: api.example.com
Content-Type: application/json
Idempotency-Key: 7c1a9e2d-44b0-4f3a-9d6e-2b5f8a10c9d1
{
"amount_cents": 4200,
"currency": "usd"
}REST endpoints have a fixed shape, so they over-fetch (a user endpoint returns the whole object when a client only needed the display name) or under-fetch (three round trips to assemble one screen). GraphQL lets a client ask for exactly the fields it needs in one request, at the cost of HTTP-level caching, since almost every GraphQL call is a POST to the same endpoint regardless of what it's asking for.
Postman's 2025 State of the API Report puts REST adoption at 93 percent among the developers it surveyed, more than double GraphQL's 33 percent (Postman, 2025). GraphQL wins for specific problems, not as a general replacement, which is a more useful way to frame the trade-off than picking a side.
Two common approaches: authorization logic inside each resolver, or a schema directive like @auth(requires: ADMIN) attached directly to the field definition. The directive approach keeps the rule visible in the schema itself instead of buried in resolver code nobody reads until something leaks.
One thing people forget: introspection has to be disabled in production, or the schema, including any fields you thought were "just internal," is queryable by anyone who asks nicely.
gRPC runs on HTTP/2, uses Protocol Buffers instead of JSON, and is built for fast, strongly-typed service-to-service calls inside a system one team controls end to end. Pick it for internal traffic where every millisecond and every byte on the wire matters. Pick REST for anything public-facing where a third-party developer needs to read the docs and just try it with curl.
A synchronous call assumes the client will wait for an answer within one request's lifetime, fine for reads and quick writes. An event-driven or webhook model assumes the work outlives that lifetime, so the server calls the client back later instead. Most real systems mix both, synchronous for anything under roughly a second, async plus a callback for anything that isn't.
Authorization code, paired with PKCE for public clients like mobile apps or 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 human user at all. The implicit grant, which returned a token directly in a redirect URL, is discouraged now, since it exposed tokens in browser history with no way to verify who actually requested one.
Validate an HMAC-SHA256 signature computed over the raw request body using a shared secret, reject any timestamp older than roughly five minutes to block replay attempts, and always use a constant-time comparison, hmac.compare_digest in Python or crypto.timingSafeEqual in Node, never a plain ==, which leaks timing information an attacker can exploit to guess the signature byte by byte.
import hmac
import hashlib
import time
def verify_webhook(payload, signature, secret, timestamp, tolerance=300):
if abs(time.time() - int(timestamp)) > tolerance:
return False
expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)BOLA is when an API checks that a user is authenticated but never checks that they own the specific object they're requesting, so GET /invoices/1044 returns someone else's invoice just because the requester is logged in as anyone at all. It's API1:2023 on OWASP's list because it's both the most common finding and the easiest to miss, every object-fetching endpoint needs its own ownership check, and it's easy to add one everywhere except the one endpoint someone forgot (OWASP API Security Top 10, 2023).
API4:2023 covers any endpoint with no cap on how much a single request can cost the server, no pagination limit, no request body size limit, no rate limit, no timeout on an expensive operation. An endpoint that lets a client ask for ?limit=1000000 with no server-side ceiling is a resource-consumption risk even if the auth on it is perfect (OWASP, 2023).
Serializing an ORM object directly instead of mapping it to an explicit response DTO. The model gains an internal field six months later, a password hash, an internal risk score, a partner's discount rate, and it silently ships in every response that serializes the model, since nobody remembered the serializer wasn't allow-listing fields in the first place.
Both, and treating it as only one or the other is the mistake. It stops credential-stuffing and scraping abuse (security) and it stops one noisy client from starving capacity for everyone else (performance). Most production rate limiters key off both a per-IP and a per-account or per-API-key bucket for exactly that reason.
Issue the new key alongside the old one and accept both during an overlap window, days to weeks depending on how often clients deploy, then deprecate the old key with advance notice, then revoke it. Rotating instantly with no overlap window is how you find out which of your customers never rotate anything on their own schedule.
400 means the server couldn't parse what it received at all, malformed JSON, a missing required header. 422 means the request was syntactically fine but failed a business rule, an email field with a value that isn't a valid email, an end date before a start date. 422 technically comes from WebDAV (RFC 4918), not the core HTTP spec, a detail that trips up interviewers as often as candidates, but it's become the de facto standard for validation failures regardless.
A Retry-After header telling the client how long to back off, in seconds or as a timestamp. Most production APIs also send X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset on every response, limited or not, so a well-behaved client throttles itself before it ever earns a 429 in the first place.
A predictable shape regardless of endpoint, something close to {"type": "...", "title": "...", "status": 422, "detail": "...", "instance": "..."}. Without it, every team on a platform invents its own error envelope, and every client integrating with more than one service ends up writing a different parser for each one.
Idempotency is about the server's end state, not the response you get back. The first DELETE removes the resource and returns 204. The second finds nothing there anymore and returns 404. Different status code, identical outcome, the resource doesn't exist either way, which is exactly what idempotent means.
Offset pagination (?offset=40&limit=20) is simple, but if a new row gets inserted while a user is paging through results sorted by creation date, "item number 40" now refers to something different than it did a request ago. Users see duplicates, or skip records entirely, mid-scroll.
Cursor-based pagination points at a specific record's stable ID or timestamp instead of a shifting numeric position, so it doesn't have this problem. The trade-off: cursor pagination can't jump straight to "page 7," only forward or backward from wherever the client already is.
-- offset (breaks under concurrent inserts)
SELECT * FROM events ORDER BY created_at DESC OFFSET 40 LIMIT 20;
-- cursor-based (stable under concurrent inserts)
SELECT * FROM events
WHERE created_at < :last_seen_timestamp
ORDER BY created_at DESC
LIMIT 20;Token bucket allows short bursts as long as the average stays under the limit over time, tokens refill at a fixed rate and each request consumes one. Sliding window counts requests within a moving time frame instead of a rigid fixed window, which avoids the "burn the entire quota in the first second after a reset" problem a naive fixed window allows. Token bucket is simpler to implement and reason about; sliding window is fairer under bursty, uneven traffic.
Cache-Control tells a client (or an intermediary cache) how long a response is good for without asking again. ETag is a fingerprint of the response body, so a client can send If-None-Match on the next request and get a cheap 304 Not Modified instead of re-downloading data that hasn't changed.
Neither helps for a per-user, constantly-changing, or write-heavy response, a real-time notifications feed, a shopping cart total. Caching helps the reads that stay the same for a while, not the ones that don't.
Roughly in order: database query time (often the single biggest chunk, especially with any N+1 pattern), serialization of the response body, network round trips (DNS, TLS handshake, the request itself), and application logic, which is usually the smallest slice of the total despite getting the most optimization attention.
LastRoundAI's own Interview Copilot targets sub-200ms response latency for live suggestions during a call, which only holds up because almost every millisecond of budget goes to the database and network legs, not the model logic itself. The lesson generalizes: profile before you optimize the part that's easiest to see in the code, not the part that's actually slow.
Opening a fresh TCP connection and authenticating against a database costs tens of milliseconds, sometimes more under load, and doing it on every single request adds that cost to every single request. A connection pool keeps a set of already-authenticated connections open and hands them out to whichever request needs one, then returns them to the pool instead of closing them.
A query for 50 posts, each resolving its own author field independently, triggers 50 separate author lookups if each resolver just queries the database directly and in isolation. DataLoader batches all the author-ID requests that happen within a single tick of the event loop into one query, then hands each resolver its specific result from that batch.
Every request carries everything the server needs, auth token, any context, so no request depends on hitting the one server instance that "remembers" it. That's what statelessness buys you: any instance behind the load balancer can serve any request. Sticky sessions, pinning a client to one server because that server holds in-memory session state, are the usual sign a team broke this constraint without meaning to.
Same pattern as the long-running-operation design question earlier in this guide: return 202 Accepted immediately, hand back a job ID and a status URL, and let the client poll or receive a webhook when it's done. Holding an HTTP connection open for 30 seconds works until one slow dependency, a flaky third-party call, an overloaded queue, turns into a cascade of exhausted connection pools further up the stack.
Return 202 Accepted immediately with a Location header pointing at a status resource, instead of holding the connection open while the work runs. The client polls that resource, or better, subscribes to a webhook the server calls once the job finishes.
The mistake candidates make here is trying to force a synchronous response out of something that fundamentally isn't one. If a job might take 30 seconds or 30 minutes, no client timeout setting is going to save you.
POST /exports HTTP/1.1
Content-Type: application/json
{ "format": "csv" }
HTTP/1.1 202 Accepted
Location: /exports/jobs/9f21
GET /exports/jobs/9f21 HTTP/1.1
HTTP/1.1 200 OK
{ "status": "processing", "progress_pct": 40 }Hard questions
10Regular TLS authenticates the server to the client. mTLS also authenticates the client to the server using a client certificate, so both sides prove identity before any application data moves. It's worth the certificate-management overhead for service-to-service calls inside a zero-trust internal network, where you can't rely on network location alone to imply trust. It's rarely worth it for a public API with thousands of third-party consumers, the operational cost of issuing and rotating client certs at that scale usually isn't.
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, a browser's fetch() error handling, stops working correctly unless someone writes custom code to peek inside every response body first. Status codes exist precisely so a machine doesn't have to parse your body to know something broke. Use them for what they're for.
Three common approaches: URL versioning (/v1/orders), header or Accept-based versioning (Accept: application/vnd.example.v2+json), and a query parameter (?version=2). URL versioning is the least "pure" by REST theory, a resource's identity technically shouldn't change with its representation, but it's the easiest to test with a browser or curl and the easiest for someone to understand at 2am during an incident.
My honest opinion: I'd pick URL versioning over header versioning almost every time, for a boring reason. Header versioning is invisible in access logs, which matters during an outage, not during a design review, exactly when nobody remembers to check it.
Look at p95 and p99 latency, not the average. An average can look perfectly healthy while one in twenty requests takes three seconds, which is exactly the experience a real user notices and an average silently hides. Set the target based on what the calling system or human actually needs, not an arbitrary round number that felt reasonable in a meeting.
I don't have great data on how this differs for internal service-to-service APIs versus public developer-facing ones specifically, only that the public ones tend to get held to a stricter, more visible bar because a slow public API shows up in someone else's support queue, not only your own dashboard.
This is the lost update problem, and it happens constantly in real systems because most PATCH endpoints just read, modify, and write without checking whether the underlying row moved in between. The fix is optimistic concurrency control: every resource carries a version, either a plain integer column or a hash derived from its content that you expose as an ETag. The client reads the resource, gets back the current version, and sends it along on the write, either as a version field in the body or as an If-Match header carrying the ETag. The server does the update conditionally, something like UPDATE orders SET status = ?, version = version + 1 WHERE id = ? AND version = ? in a single statement, and checks the affected row count. If it's zero, someone else already wrote first, and you return 409 Conflict or 412 Precondition Failed instead of applying the change.
Pessimistic locking (SELECT FOR UPDATE, holding a row lock across the request) is the other option, but it ties up a database connection for the whole request lifecycle, which is a bad trade in a stateless HTTP API with unpredictable client latency. Optimistic concurrency doesn't hold anything open, it just rejects stale writes and lets the client decide whether to refetch and retry or surface a conflict to the user. The place teams get bitten is exactly the scenario in the question: two support agents editing the same customer record, or two services incrementing the same counter, with no version check anywhere, so the last write silently wins and the first edit vanishes with no error and no log entry.
Staging environments and manual QA don't scale once you have more than a handful of consumer teams calling your API, because staging only tests the version of the consumer that happens to be deployed there at that moment, not the three other versions still running in production. Consumer-driven contract testing solves this differently: each consumer writes a contract describing the exact requests it makes and the response shape it depends on, and that contract gets published to a shared broker (Pact is the common tool here). The provider's CI pipeline then runs every published contract against the real API on every build, before merge, not after deploy. If a field gets renamed or a status code changes in a way that breaks a contract, the provider's build fails immediately, with the specific consumer and field named in the failure.
This flips the usual failure mode from "downstream team files a ticket three days after your deploy" to "your own CI blocks the merge." It's not free: someone has to own the contracts, keep them current as consumers evolve, and resist the temptation to make them so loose they stop catching anything. For schema-typed APIs, the lighter-weight version is a breaking-change linter run in CI, tools like openapi-diff for REST or buf breaking for protobuf, which flag removed fields, changed types, and narrowed enums automatically without needing per-consumer contracts at all. Most mature API teams run both: automated schema diffing for cheap, broad coverage, and contract tests for the handful of high-value consumer relationships where a break is expensive.
An in-memory counter only knows about requests that instance itself received. If you're running four API instances behind a round-robin load balancer and your limit is 100 requests per minute, a client can send 400 requests per minute and each instance will cheerfully report it's under its own local limit of 100, because none of them can see the other three. Sticky sessions "fix" this by routing a client to the same instance every time, but that just turns rate limiting into a load balancing problem and breaks the moment an instance restarts or you scale up.
The real fix is a shared counter store, almost always Redis, with the increment-and-check done atomically so two instances can't both read "count is 99, allow it" in the same millisecond and let request 100 and 101 both through. A Lua script executed via EVAL is the standard way to make the read-check-increment sequence atomic in one round trip, since Redis executes the whole script without interleaving other clients' commands. For a sliding window instead of a fixed bucket, a common pattern is a sorted set per client keyed by timestamp, where you ZREMRANGEBYSCORE anything older than the window, then ZCARD to count what's left, then ZADD the new request, all inside a MULTI or Lua script so the count stays accurate. Clock skew matters less here than people expect, since you're using the Redis server's own clock as the single source of truth rather than trusting timestamps from individual API nodes, which is exactly why you centralize the state instead of trying to synchronize four separate in-memory clocks.
A dependency that returns errors fast is annoying but cheap, your caller gets a quick failure and can retry or degrade gracefully. A dependency that hangs is expensive, because every caller thread or connection that's waiting on it is now tied up doing nothing until it times out. If that dependency is called from a shared thread pool with, say, 50 workers, and calls start taking 30 seconds instead of 50 milliseconds, the pool fills up with stuck requests within seconds, and now every other request, even ones that have nothing to do with the slow dependency, is queued behind it. That's how a single flaky downstream service takes down an entire API tier that calls a dozen other things fine.
The circuit breaker pattern (Hystrix popularized it, resilience4j is the common modern implementation) tracks the error and latency rate for calls to a dependency, and once it crosses a threshold, the breaker trips open and starts failing fast without even attempting the call, for a cooldown period. After the cooldown it goes half-open and lets a small number of probe requests through to see if the dependency recovered, closing the breaker again if they succeed. This only works if you also set aggressive timeouts, since a circuit breaker can't trip on latency it never observes because the call is still hanging. The complementary pattern is bulkhead isolation, giving each downstream dependency its own dedicated thread pool or connection pool sized to what that dependency can tolerate, so a slow payment provider can exhaust its own 10-connection pool without touching the 40 connections reserved for everything else.
A REST endpoint has a fixed, known cost, you can load test it and know what one request does to the database. A GraphQL query is arbitrary until it's sent, and nested queries can multiply cheaply on the client side into something enormously expensive server side, for example a query that asks for users, then each user's orders, then each order's line items, then each line item's product, with no limit anywhere, turns into thousands of resolver calls from a query that looks small on the wire.
The standard defenses stack together rather than replacing each other. Depth limiting rejects queries nested past a fixed number of levels, which catches the naive attack but not a wide, shallow one. Complexity or cost analysis is the real answer, assigning a cost to every field (higher for fields that hit the database or an external service, multiplied by any list arguments like first or limit), summing it for the whole query at parse time, and rejecting anything over budget before a single resolver runs. Persisted queries or an allowlist go further for public-facing APIs, where the client only sends a hash of a pre-approved query instead of arbitrary GraphQL text, which removes the arbitrary-query attack surface entirely at the cost of flexibility. Server-side enforcement of pagination limits (ignoring or capping a client-requested first: 100000 regardless of what the schema technically allows) and disabling introspection in production round out the list, since introspection makes it trivial for an attacker to find the expensive parts of your schema in the first place.
If the app itself logs a fast completion time, the delay isn't in your handler code, it's in something the request sat in before your handler code ever ran, or something that happened after your handler returned. The two most common culprits are a saturated worker pool and a proxy timeout mismatch, and they look identical from the application's own logs because the app only times what it's aware of.
With a saturated pool, requests queue at the application server (or the runtime's own request queue, or a connection pool waiting on the database) before your handler is invoked, so the 200ms you're logging is real but it's measured from the moment the handler started, not from when the request arrived. If your load balancer or nginx has a 30-second upstream timeout and the queue wait pushes total time past that, nginx returns 504 to the client while your app, a few hundred milliseconds later, finishes the request successfully and logs a fast, misleadingly clean number. You catch this by looking at queue depth and time-in-queue metrics at the app server layer, not just handler duration, and by checking your connection pool's wait time, not just its size.
The other common cause is a keep-alive or timeout mismatch between the load balancer and the backend. If the backend's keep-alive timeout is shorter than the load balancer's idle timeout, the backend can close a connection the load balancer still considers valid, and the next request the load balancer tries to send down that connection gets reset, which upstream proxies frequently surface as a 502 or 504 rather than a clean connection-refused error. The fix there is making sure backend keep-alive timeout is set longer than whatever sits in front of it, not shorter, which is the opposite of what a lot of default configs ship with.
Across mock interviews tagged "API design" in LastRoundAI's Interview Copilot logs this year, the single question pair candidates fumble most isn't OAuth or GraphQL. It's 401 vs 403. Most candidates can define both correctly in isolation, then freeze the moment an interviewer asks why an API might deliberately return 404 instead of 403 for a resource a user isn't allowed to see. That specific follow-up, not the definitions, is usually what decides the round.
The status code section above reads like trivia. It isn't, and the candidates who treat it that way are the ones who get stuck on the first real follow-up. Knowing that 401 means "no valid credentials" is table stakes. Explaining why an API might return 404 instead of 403 on purpose, to avoid confirming a resource exists to someone who shouldn't be able to see it, is the answer that actually separates candidates.
The same pattern shows up in the security section. Candidates can usually define BOLA correctly when asked directly. Fewer can point to where in their own last project it could have happened, which is the version of the question senior loops actually ask.
Reading through a list of API developer interview questions is not the same as defending an answer out loud when an interviewer changes one detail on you mid-response. Reciting the difference between 401 and 403 in your head doesn't hold up the same way in a real conversation. LastRoundAI's Interview Copilot runs live during real interviews and feeds structured, real-time guidance on exactly this kind of follow-up, sub-200ms and invisible on a screen share, so you're not reconstructing an OAuth flow from memory under pressure. If a specific concept above is still foggy before you get that far, the Concept Explainer breaks it down the way an interviewer would actually probe it, not the way a textbook defines it.
The free plan includes 15 credits a month that reset monthly, so there's no real reason to save them for "the real interview" instead of a practice round this week. Questions about either product go to contact@lastroundai.com, the only inbox we check.
LastRoundAI runs a realistic mock interview and gives you real-time guidance on the exact questions above.
LastRound data
What we see on our side
Across 1,393 interview sessions configured on LastRound between January 2025 and July 2026, 109 were set up for backend development. That is a small sample and we are not going to dress it up as more, but it is first-hand rather than borrowed, and it is the pool these questions were sanity-checked against.
Frequently asked questions
What should I revise first for an API interview?
Revise HTTP semantics and idempotency before you revise framework specifics. Status codes, safe versus unsafe methods, retries and idempotency keys come up in almost every API loop regardless of stack, whereas questions about a particular framework are usually scoped to what the team actually runs.
Is REST still asked more than GraphQL?
In most loops, yes. REST remains the default for public and internal APIs at the majority of companies, so it carries more interview weight. GraphQL questions do appear, usually as a comparison: when would you reach for it, and what does it cost you in caching and rate limiting.
How deep do API interviews go on authentication?
Deeper than most candidates expect. Being able to describe the OAuth 2.0 authorization code flow end to end, and explain where tokens live and how they are refreshed, is a common bar. Vague answers about "using JWTs" tend to trigger follow-ups rather than satisfy the question.
Do I need to know API versioning strategies?
It comes up often enough to prepare. Have a view on URI versioning against header-based versioning, and be ready to say what you would do about a breaking change on an endpoint other teams already depend on.
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.

