{"id":2008,"date":"2026-07-19T23:01:04","date_gmt":"2026-07-19T17:31:04","guid":{"rendered":"https:\/\/lastroundai.com\/blog\/?post_type=iq&#038;p=2008"},"modified":"2026-07-19T23:01:04","modified_gmt":"2026-07-19T17:31:04","slug":"api-design","status":"publish","type":"iq","link":"https:\/\/lastroundai.com\/interview-questions\/api-design","title":{"rendered":"API Design Interview Questions (2026): Q&#038;A"},"content":{"rendered":"<p>A staff engineer candidate interviewing for a platform team in early 2026 got asked to design the endpoint for canceling an order. He drew up <code>DELETE \/orders\/{id}<\/code>, which sounds reasonable until the interviewer pointed out that canceling isn&#8217;t deleting, the order still exists, it just moves to a canceled state with a refund workflow attached. That single distinction, between removing a resource and changing its state, is the difference between a candidate who has shipped APIs that survived a few years of change requests and one who hasn&#8217;t.<\/p>\n<p>The formal rules here are older than most people assume. RFC 9110, the current HTTP semantics standard, spells out exactly which methods are safe (don&#8217;t change server state) and which are idempotent (repeating the request produces the same result as sending it once). GET and HEAD are both safe and idempotent, PUT and DELETE are idempotent but not safe, and POST is neither (<a href=\"https:\/\/www.rfc-editor.org\/rfc\/rfc9110.html\" target=\"_blank\" rel=\"noopener noreferrer\">RFC 9110, HTTP Semantics<\/a>). Almost every practical API design question on this page traces back to that one paragraph of the spec, whether the interviewer says so out loud or not.<\/p>\n<p>This page covers the questions that come up across REST API design interviews for backend, platform, and staff-level roles: resource modeling and HTTP verbs, status codes and error payloads, pagination and filtering, versioning strategies, authentication and rate limiting, idempotency and concurrency control, and the harder tradeoff questions around REST versus GraphQL versus gRPC, backward compatibility, and long-running operations.<\/p>\n<div class=\"iq-stats not-prose\"><div class=\"iq-stat\"><span class=\"iq-stat__value\">50<\/span><span class=\"iq-stat__label\">Questions<\/span><\/div><div class=\"iq-stat\"><span class=\"iq-stat__value\">RFC 9110 (HTTP Semantics)<\/span><span class=\"iq-stat__label\">Core Standard<\/span><\/div><div class=\"iq-stat\"><span class=\"iq-stat__value\">GET, PUT, DELETE, HEAD<\/span><span class=\"iq-stat__label\">Idempotent Methods<\/span><\/div><div class=\"iq-stat\"><span class=\"iq-stat__value\">Design Discussion + Code<\/span><span class=\"iq-stat__label\">Format<\/span><\/div><\/div>\n<h2>Resources, HTTP verbs, and what &#8220;RESTful&#8221; actually means<\/h2>\n<p>Every API design loop starts here. The questions look basic on paper, but a candidate who fumbles the safe\/idempotent distinction usually keeps fumbling for the rest of the interview.<\/p>\n<div class=\"iq-dsec iq-dsec--easy\"><div class=\"iq-dsec__row\"><h2 class=\"iq-dsec__h\" id=\"easy\"><span class=\"iq-dsec__dot\" aria-hidden=\"true\"><\/span>Easy questions<\/h2><span class=\"iq-dsec__n\">11<\/span><\/div><div class=\"iq-dsec__bar\" aria-hidden=\"true\"><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What makes an API &#039;RESTful,&#039; and is that term actually well defined?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Fundamentals<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>REST, representational state transfer, comes from Roy Fielding&#8217;s 2000 dissertation and describes a set of architectural constraints: a client-server split, statelessness between requests, cacheable responses, a uniform interface, and a layered system. In practice, &#8220;RESTful API&#8221; has become a loose label for &#8220;an HTTP API that uses resource-shaped URLs and the standard verbs,&#8221; and most production APIs that call themselves RESTful violate at least one of Fielding&#8217;s original constraints, usually HATEOAS.<\/p>\n<p>I don&#8217;t think that&#8217;s a scandal. It&#8217;s worth knowing the formal definition so you can answer the question precisely, but in an interview I&#8217;d rather hear you describe the practical conventions your API actually follows than recite the six constraints from memory without being able to name which ones your last project skipped.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What&#039;s the difference between a safe method and an idempotent method?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">HTTP Methods<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Safe means the method doesn&#8217;t change server state, a client can call it any number of times with zero side effects. Idempotent means repeating the same request produces the same end state as sending it once, even if it does change something the first time. Every safe method is idempotent, but not every idempotent method is safe.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">text<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-text\">\n\nGET     safe: yes   idempotent: yes\n\nHEAD    safe: yes   idempotent: yes\n\nPUT     safe: no    idempotent: yes\n\nDELETE  safe: no    idempotent: yes\n\nPOST    safe: no    idempotent: no\n\nPATCH   safe: no    idempotent: no (by spec, though often implemented idempotently)\n<\/code><\/pre><\/div><\/p>\n<p>PUT replacing a resource with the same payload twice leaves it in the same state both times, that&#8217;s idempotent. POST creating a new order every time you call it is the textbook non-idempotent case, call it twice and you get two orders.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">PUT versus PATCH, when do you use each?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">HTTP Methods<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>PUT replaces the entire resource with whatever you send, any field you omit from the payload is treated as unset or reset to a default. PATCH applies a partial update, only the fields you include change, everything else stays as it was.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">text<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-text\">\n\nPUT \/users\/42\n\n{ \u201cname\u201d: \u201cAda\u201d, \u201cemail\u201d: \u201cada@example.com\u201d }\n\n\/\/ entire user record is replaced with exactly this\nPATCH \/users\/42\n\n{ \u201cemail\u201d: \u201cnew@example.com\u201d }\n\n\/\/ only email changes, name is untouched\n<\/code><\/pre><\/div><\/p>\n<p>The mistake I see most often is a PUT endpoint that actually behaves like PATCH under the hood, silently ignoring missing fields instead of nulling them out. That breaks the PUT contract and confuses any client that relies on PUT actually replacing the resource.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">Should URLs use nouns or verbs, and what&#039;s the exception?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Resource Modeling<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>URLs should be nouns, the resource, and the HTTP verb carries the action. <code>GET \/users<\/code> not <code>GET \/getUsers<\/code>. The common exception is an action that doesn&#8217;t map cleanly onto CRUD, sending an email, triggering a batch job, canceling an order, where a verb-shaped sub-path on top of a POST is the pragmatic answer.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">text<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-text\">\n\nPOST \/orders\/{id}\/cancel\n\nPOST \/users\/{id}\/reset-password\n\nPOST \/reports\/{id}\/export\n<\/code><\/pre><\/div><\/p>\n<p>Purists will tell you to model every action as a resource, &#8220;create a cancellation resource under the order.&#8221; That&#8217;s technically more RESTful, and it&#8217;s also more ceremony than most teams want for an operation that&#8217;s genuinely just &#8220;do this thing to this resource.&#8221; Both are defensible. Pick the pattern that keeps your team&#8217;s API surface readable and stick with it consistently.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What does &#039;stateless&#039; actually mean in REST, and why does it matter for scaling an API?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Fundamentals<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Stateless means every request carries everything the server needs to process it, credentials, the resource being acted on, any context, rather than the server remembering anything about a client between requests. The server doesn&#8217;t hold a session in memory that ties a client to one specific instance.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">text<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-text\">\n\n\/\/ Stateless: every request is self-contained\n\nGET \/orders\/42\n\nAuthorization: Bearer eyJhbGci\u2026\n\/\/ Stateful (what REST avoids): server remembers \u201cthis session is logged in\n\n\/\/ as user 42\u201d in memory on a specific instance\n<\/code><\/pre><\/div><\/p>\n<p>This is what lets you put a load balancer in front of ten identical API instances and send any request to any instance without caring which one handled the client&#8217;s last request. A stateful design ties a client to a specific server, which makes horizontal scaling and failover much harder.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">Should a collection endpoint use a plural or singular noun, \/users or \/user?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Resource Modeling<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Plural, consistently, for both the collection and a single item within it. <code>\/users<\/code> for the collection, <code>\/users\/42<\/code> for one specific user. Mixing singular and plural across endpoints, or switching conventions between resources, is one of the fastest ways to make an API feel inconsistent to anyone integrating against it.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">text<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-text\">\n\nGET \/users          \/\/ collection\n\nGET \/users\/42       \/\/ single resource within that collection\n\nGET \/users\/42\/orders  \/\/ nested collection, still plural\n<\/code><\/pre><\/div><\/p>\n<p>This is a convention, not a technical requirement, GET works identically either way. But picking one and applying it everywhere is what makes an API predictable enough that a developer can guess the next endpoint&#8217;s shape correctly without checking the docs.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What&#039;s the difference between a 200 and a 201 response, and when do people get it wrong?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Status Codes<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>200 OK is the general success response. 201 Created is specifically for a request, almost always a POST, that resulted in a new resource being created, and it should come with a <code>Location<\/code> header pointing at the new resource&#8217;s URL.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">text<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-text\">\n\nPOST \/orders\n\n{ \u201cproduct_id\u201d: \u201cp_1\u201d, \u201cquantity\u201d: 2 }\nHTTP\/1.1 201 Created\n\nLocation: \/orders\/789\n\n{ \u201cid\u201d: \u201c789\u201d, \u201cstatus\u201d: \u201cpending\u201d }\n<\/code><\/pre><\/div><\/p>\n<p>The common mistake is returning 200 from a creation endpoint, which works fine functionally but loses the semantic signal that tells a generic HTTP client, or a developer skimming your API logs, that something new now exists at a specific new address.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What is content negotiation, and how does the Accept header drive it?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Content Negotiation<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Content negotiation lets a client tell the server what response format it wants, using the <code>Accept<\/code> header, and lets the server tell the client what it actually sent back, using <code>Content-Type<\/code>. Most modern APIs only ever support JSON, so negotiation is often a formality, but it becomes real the moment an API needs to serve both JSON and, say, CSV exports from the same endpoint.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">text<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-text\">\n\nGET \/reports\/42\n\nAccept: text\/csv\n\n\u2192 Content-Type: text\/csv\n\nid,name,total\n\n1,Order A,50.00\nGET \/reports\/42\n\nAccept: application\/json\n\n\u2192 Content-Type: application\/json\n\n{ \u201cid\u201d: 1, \u201cname\u201d: \u201cOrder A\u201d, \u201ctotal\u201d: 50.00 }\n<\/code><\/pre><\/div><\/p>\n<p>If a client asks for a format the server can&#8217;t produce, the correct response is 406 Not Acceptable, though in practice a lot of APIs skip that and just default to JSON regardless of what was requested, which is a defensible simplification if JSON really is the only format you&#8217;ll ever support.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">Why do most public APIs today default to JSON instead of XML?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Fundamentals<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>JSON maps directly onto the data structures most programming languages already use, objects and arrays, so parsing it usually needs a single built-in function call with no separate library. XML needs a dedicated parser, has more verbose syntax for the same data, and its extra features, namespaces, attributes versus elements, mostly go unused in typical API payloads anyway.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">text<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-text\">\n\n{ \u201cid\u201d: 42, \u201cname\u201d: \u201cAda\u201d }\n&lt;user&gt;&lt;id&gt;42&lt;\/id&gt;&lt;name&gt;Ada&lt;\/name&gt;&lt;\/user&gt;\n<\/code><\/pre><\/div><\/p>\n<p>XML still shows up in specific domains where it never left, SOAP-based enterprise integrations, some financial and healthcare protocols with strict schema validation requirements, but for a general-purpose REST API built after roughly 2015, JSON is close to the default assumption unless a specific integration partner requires otherwise.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What&#039;s the difference between a 401 and a 403, and why do people mix them up?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Status Codes<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>401 Unauthorized means the request has no valid credentials, or the credentials it has aren&#8217;t accepted, the client needs to authenticate. 403 Forbidden means the client is authenticated fine, but doesn&#8217;t have permission to do this particular thing. The confusing part is the name, &#8220;Unauthorized&#8221; sounds like it should mean &#8220;not allowed,&#8221; but it actually means &#8220;not authenticated.&#8221;<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">text<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-text\">\n\n401: no token, or an expired\/invalid token \u2192 client should log in or refresh\n\n403: valid token, but this user isn\u2019t an admin \u2192 client should stop, not retry\n<\/code><\/pre><\/div><\/p>\n<p>Getting this wrong in practice matters because clients often behave differently on each: a 401 typically triggers a token refresh and retry, a 403 shouldn&#8217;t, since retrying with the same valid-but-insufficient credentials will never succeed.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">Should page size be capped, and what happens if you don&#039;t cap it?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Pagination<\/span><span class=\"iq-badge iq-badge--easy\">Easy<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Yes, always cap the maximum page size a client can request, and return a sane default when the client doesn&#8217;t specify one. Without a cap, a client, or a misbehaving script, can request <code>limit=1000000<\/code> and force your database to load and serialize a response that can take down the endpoint for everyone else hitting it at the same time.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">text<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-text\">\n\nGET \/orders?limit=5000   \/\/ client requests too much\n\n\/\/ server clamps to its own max, e.g. 100, and returns that instead of erroring\n<\/code><\/pre><\/div><\/p>\n<p>Clamping silently to the max, rather than returning a 400, is usually the friendlier choice for well-intentioned clients. Whether you clamp or reject is a smaller decision than making sure a cap exists at all.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-dsec iq-dsec--medium\"><div class=\"iq-dsec__row\"><h2 class=\"iq-dsec__h\" id=\"medium\"><span class=\"iq-dsec__dot\" aria-hidden=\"true\"><\/span>Medium questions<\/h2><span class=\"iq-dsec__n\">17<\/span><\/div><div class=\"iq-dsec__bar\" aria-hidden=\"true\"><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">How do you decide whether something should be its own resource or a nested field on a parent resource?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Resource Modeling<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>A good heuristic: if a thing has its own identity, its own lifecycle, and other resources might reference it independently, model it as its own resource with its own URL. If it only ever makes sense attached to its parent and never gets fetched, updated, or referenced on its own, keep it as a nested field.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">text<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-text\">\n\n\/\/ Comments have their own identity, lifecycle, can be deleted individually\n\nGET \/posts\/{id}\/comments\n\nGET \/comments\/{commentId}\n\/\/ An address embedded on a user probably doesn\u2019t need its own endpoint\n\nGET \/users\/{id}  \/\/ { \u2026, \u201caddress\u201d: { \u201cstreet\u201d: \u201c\u2026\u201d, \u201ccity\u201d: \u201c\u2026\u201d } }\n<\/code><\/pre><\/div><\/p>\n<p>I&#8217;ve seen teams over-normalize this and turn every embedded object into its own resource with its own CRUD endpoints nobody calls directly. That adds URL surface area and versioning burden for no real benefit. Model the thing that actually gets addressed independently, not every noun in your data model.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">422 versus 400, when would you use each for a validation failure?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Status Codes<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>400 Bad Request is for a request that&#8217;s malformed at the syntax level, invalid JSON, a missing required field, a value that doesn&#8217;t parse into the expected type. 422 Unprocessable Entity, or Unprocessable Content in the newer wording, is for a request that&#8217;s well-formed and understood, but fails a semantic or business rule, an email address that&#8217;s syntactically valid but already registered, an end date that&#8217;s before the start date.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">text<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-text\">\n\n400: { \u201cemail\u201d: \u201cnot-an-email\u201d }              \/\/ malformed\n\n422: { \u201cemail\u201d: \u201cuser@site.com\u201d }              \/\/ valid shape, but already taken\n<\/code><\/pre><\/div><\/p>\n<p>A lot of production APIs collapse both cases into 400 and skip 422 entirely, which is a defensible simplification if your client code doesn&#8217;t need to distinguish &#8220;fix your request format&#8221; from &#8220;your request is well-formed but violates a rule.&#8221; I&#8217;d rather see a candidate use the distinction consistently than pretend to care about it and then use 400 for everything anyway.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">Why does 429 matter, and what should the response actually include?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Status Codes<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>429 Too Many Requests tells the client it&#8217;s been rate limited, and unlike a generic 4xx, the client should back off and retry rather than treat it as a permanent failure. The useful part of a 429 response is the headers, not just the status code: a <code>Retry-After<\/code> header telling the client exactly how long to wait before trying again, so it doesn&#8217;t have to guess or hammer the endpoint with its own retry logic.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">text<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-text\">\n\nHTTP\/1.1 429 Too Many Requests\n\nRetry-After: 30\n\nX-RateLimit-Limit: 100\n\nX-RateLimit-Remaining: 0\n\nX-RateLimit-Reset: 1735689600\n<\/code><\/pre><\/div><\/p>\n<p>A 429 with no Retry-After header and no rate limit headers technically follows the spec but leaves every client guessing at a backoff strategy. That&#8217;s the detail that separates a rate limiter designed for real integrators from one bolted on as an afterthought.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">A client sends a request that&#039;s fine, but a downstream service you depend on times out. What status code do you return, and does it matter which side failed?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Error Handling<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>502 Bad Gateway or 504 Gateway Timeout, both in the 5xx range, because the failure is on your side of the boundary even though the immediate cause was a dependency, not a bug in your own code. The client did nothing wrong, so a 4xx would be misleading and would discourage a retry that might actually succeed.<\/p>\n<p>It genuinely matters which one you pick: 502 signals the upstream returned something invalid, 504 signals it never responded in time. A client&#8217;s retry strategy can reasonably differ between the two, a 504 suggests the operation might have partially completed on the upstream side, which changes whether a naive retry is safe.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">Offset pagination versus cursor pagination, what&#039;s the actual tradeoff?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Pagination<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Offset pagination, <code>?page=3&limit=20<\/code> or <code>?offset=40&limit=20<\/code>, is simple to implement and lets a client jump to an arbitrary page directly. It breaks down at scale in two ways: performance, since the database has to scan and discard every row before the offset, and correctness, since a row inserted or deleted while a client is paging shifts every subsequent page and can cause the client to see duplicates or skip rows entirely.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">text<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-text\">\n\nGET \/orders?offset=40&amp;limit=20     \/\/ offset-based\n\nGET \/orders?cursor=eyJpZCI6NDJ9&amp;limit=20  \/\/ cursor-based, opaque token\n<\/code><\/pre><\/div><\/p>\n<p>Cursor pagination encodes a pointer, often the last-seen sort key, into an opaque token and asks the database for &#8220;everything after this point,&#8221; which stays fast and stable regardless of concurrent writes, at the cost of not being able to jump to an arbitrary page number. JSON:API&#8217;s spec reserves standard link names, first, last, prev, next, for exactly this pattern (<a href=\"https:\/\/jsonapi.org\/format\/\" target=\"_blank\" rel=\"noopener noreferrer\">JSON:API specification<\/a>).<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">URI versioning versus header versioning, what&#039;s the real argument for each?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Versioning<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>URI versioning, <code>\/v1\/orders<\/code>, puts the version directly in the path. It&#8217;s the most visible option, trivial to test in a browser, easy to route at the infrastructure layer, and impossible for a client to get wrong by accident. Header versioning, an <code>Accept<\/code> header like <code>application\/vnd.myapi.v2+json<\/code>, keeps the URL itself stable across versions, which fits the argument that a resource&#8217;s identity shouldn&#8217;t change just because its representation format changed.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">text<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-text\">\n\nGET \/v2\/orders\/42                              \/\/ URI versioning\n\nGET \/orders\/42\n\nAccept: application\/vnd.myapi.v2+json          \/\/ header versioning\n<\/code><\/pre><\/div><\/p>\n<p>In practice, URI versioning wins most of the time because it&#8217;s dramatically easier for API consumers to discover, debug, and pin in their own code, even though it&#8217;s the less &#8220;pure&#8221; REST answer. I&#8217;d rather work with a pragmatic v1\/v2 URL scheme every client understands than a theoretically cleaner header scheme half the integrators get wrong.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What actually counts as a breaking change versus a safe, backward-compatible addition?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Versioning<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Adding a new optional field to a response, adding a new endpoint, or adding a new optional query parameter are all safe, existing clients that don&#8217;t know about the new field simply ignore it. Removing a field, renaming a field, changing a field&#8217;s type, or making a previously optional request field required are all breaking, they will crash or silently misbehave existing client code that never asked for the change.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">text<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-text\">\n\n\/\/ Safe: adding \u201cloyalty_tier\u201d to the user response, existing clients ignore it\n\n\/\/ Breaking: renaming \u201cemail\u201d to \u201cemail_address\u201d\n\n\/\/ Breaking: changing \u201camount\u201d from an integer of cents to a float of dollars\n<\/code><\/pre><\/div><\/p>\n<p>The subtle one people get wrong: changing a field from nullable to non-nullable feels harmless because you&#8217;re &#8220;just being more specific,&#8221; but a client that had defensive null-checking code around that field is fine, and a client that relied on the field sometimes being absent to trigger different behavior will break. Treat any change to an existing field&#8217;s meaning as breaking until proven otherwise.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">API key versus OAuth 2.0 bearer token, when do you use which?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Authentication<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>An API key is a static secret identifying which application or account is calling, simple to issue and simple for a server-to-server integration to use, but it doesn&#8217;t carry any notion of a specific user or a limited scope of permission unless you build that on top yourself. OAuth 2.0 gives you delegated, scoped, time-limited access, useful the moment a third-party app needs to act on behalf of an individual user without ever seeing that user&#8217;s actual password.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">text<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-text\">\n\nX-API-Key: sk_live_abc123                       \/\/ static, app-level identity\n\nAuthorization: Bearer eyJhbGciOiJIUzI1NiIs\u2026   \/\/ OAuth token, scoped, expiring\n<\/code><\/pre><\/div><\/p>\n<p>Server-to-server integrations where the calling system is trusted as a whole are usually fine with an API key. Anything where a third-party app needs &#8220;access to this specific user&#8217;s calendar, nothing else, for 30 days&#8221; needs OAuth&#8217;s scoping and expiry, an API key alone can&#8217;t express either of those constraints.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">Should rate limits be per API key, per IP, or per user, and does it matter which?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Rate Limiting<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>It matters a lot, and the wrong choice creates real problems. Per-IP limiting breaks down behind a shared corporate NAT or a mobile carrier&#8217;s proxy, where thousands of legitimate users share one visible IP and get throttled as if they were one client. Per-API-key or per-authenticated-user limiting is more accurate but does nothing for unauthenticated endpoints, which still need an IP-based fallback as a baseline defense.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">text<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-text\">\n\n\/\/ Authenticated endpoints: limit by API key or user ID\n\n\/\/ Unauthenticated endpoints (login, signup, public search): limit by IP,\n\n\/\/ accepting the shared-NAT tradeoff since there\u2019s no identity to key on yet\n<\/code><\/pre><\/div><\/p>\n<p>Most production systems end up layering both: a coarse IP-based limit as a first line of defense against unauthenticated abuse, and a finer per-key or per-user limit for everything behind auth.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">How do you make a POST endpoint safe to retry, given that POST isn&#039;t idempotent by default?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Idempotency<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Idempotency keys. The client generates a unique key, usually a UUID, and sends it with the request, typically as a header. The server stores which keys it has already processed along with the response it returned, and if the same key comes in again, it returns the stored response instead of creating a second resource.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">text<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-text\">\n\nPOST \/payments\n\nIdempotency-Key: 6b2f1c9e-2a4d-4f3e-9c1a-8d7e3f2b1a90\n{ \u201camount\u201d: 5000, \u201ccurrency\u201d: \u201cUSD\u201d }\n\/\/ Same key sent again (client retry after a timeout): server returns the\n\n\/\/ original payment\u2019s response instead of charging the card twice\n<\/code><\/pre><\/div><\/p>\n<p>This is exactly the mechanism payment processors like Stripe are known for, and for good reason, a network timeout on a payment request is genuinely ambiguous, the charge might have succeeded before the response was lost. An idempotency key is what lets a client safely retry without risking a duplicate charge.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What&#039;s optimistic concurrency control, and how do ETags implement it for a REST API?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Concurrency<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Optimistic concurrency assumes conflicts are rare, so instead of locking a resource while someone edits it, the server lets everyone read and edit freely, but rejects a write if the resource changed since the client last fetched it. An ETag is an opaque version identifier the server returns with a GET, and the client sends it back on a subsequent update as an <code>If-Match<\/code> header so the server can check whether it still matches the current version before applying the write.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">text<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-text\">\n\nGET \/documents\/42\n\n\u2192 ETag: \u201cv7\u201d\nPUT \/documents\/42\n\nIf-Match: \u201cv7\u201d\n\n{ \u2026updated content\u2026 }\n\n\u2192 200 OK if the document is still at v7\n\n\u2192 412 Precondition Failed if someone else already updated it to v8\n<\/code><\/pre><\/div><\/p>\n<p>412 is the signal to the client that it needs to re-fetch the current version, merge or reconcile the conflicting changes, and try the update again with the new ETag, rather than blindly overwriting someone else&#8217;s concurrent edit.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">Two clients PATCH the same order at nearly the same time with different fields, what&#039;s the safest default behavior if you&#039;re not using ETags?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Concurrency<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Without any version check, the safest default is last-write-wins per field, not per request, meaning the database applies each individual field update atomically rather than treating the whole PATCH as one blind overwrite of the entire record. That way, if client A updates the shipping address and client B updates the status at nearly the same moment, both changes survive instead of one silently clobbering the other.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">text<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-text\">\n\nUPDATE orders SET status = \u2018shipped\u2019 WHERE id = 42;\n\nUPDATE orders SET shipping_address = \u2018\u2026\u2019 WHERE id = 42;\n\n\/\/ applied as two independent field-level updates, not one full-record overwrite\n<\/code><\/pre><\/div><\/p>\n<p>This isn&#8217;t a substitute for real concurrency control when correctness genuinely matters, financial balances, inventory counts, those need ETags or a database-level compare-and-swap. But for ordinary field updates where two edits genuinely don&#8217;t conflict, field-level writes avoid a whole class of accidental data loss that a naive full-record PUT would cause.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">How does HTTP caching actually work for a REST API, and what&#039;s the difference between ETag and Cache-Control?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Caching<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Cache-Control tells a client, or an intermediate cache like a CDN, how long a response can be reused without asking the server again, <code>max-age=60<\/code> means &#8220;good for 60 seconds, don&#8217;t even ask.&#8221; ETag-based validation is a different mechanism entirely, the client always asks the server, but the server can respond with a cheap 304 Not Modified instead of the full payload if the resource&#8217;s ETag hasn&#8217;t changed since the client last fetched it.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">text<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-text\">\n\nGET \/products\/42\n\nCache-Control: max-age=60, must-revalidate\n\nETag: \u201cv12\u201d\n\/\/ 60 seconds later, client re-requests and sends:\n\nIf-None-Match: \u201cv12\u201d\n\n\u2192 304 Not Modified (empty body, saves bandwidth) if unchanged\n\n\u2192 200 OK with full new body and new ETag if it changed\n<\/code><\/pre><\/div><\/p>\n<p>Use Cache-Control for data that&#8217;s fine being briefly stale, product listings, public content. Use ETag validation for data where you want the client checking freshness every time but don&#8217;t want to pay the bandwidth cost of resending an unchanged payload.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What should a health-check endpoint actually verify, beyond just returning 200?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Reliability<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>A bare <code>GET \/health<\/code> that always returns 200 the moment the process is running tells you the process exists, not that it&#8217;s actually able to do its job. A useful health check verifies the dependencies the service actually needs, can it reach its database, can it reach the downstream services it calls on the request path, without going so far that a health check itself becomes a heavy, cascading operation.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">text<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-text\">\n\nGET \/health\n\n{\n\n  \u201cstatus\u201d: \u201cok\u201d,\n\n  \u201cchecks\u201d: {\n\n    \u201cdatabase\u201d: \u201cok\u201d,\n\n    \u201ccache\u201d: \u201cok\u201d,\n\n    \u201cpayment_gateway\u201d: \u201cdegraded\u201d\n\n  }\n\n}\n<\/code><\/pre><\/div><\/p>\n<p>The balance to strike: check enough to be meaningful to your load balancer and monitoring, but don&#8217;t have the health check itself hammer a downstream dependency so hard on every poll that it becomes part of the load problem it&#8217;s supposed to detect.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What&#039;s the purpose of a request ID or correlation ID header, and where does it need to show up?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Observability<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>A correlation ID is a unique value generated at the edge of a request, either by the client or the first service that receives it, and passed along through every downstream call the original request triggers. Its whole job is letting you find every log line across every service that belongs to one specific user-facing request, instead of trying to reconstruct a timeline from timestamps alone.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">text<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-text\">\n\nX-Request-Id: 6b2f1c9e-2a4d-4f3e-9c1a-8d7e3f2b1a90\n\/\/ Service A logs: [6b2f1c9e\u2026] received request\n\n\/\/ Service A calls Service B, forwarding the same header\n\n\/\/ Service B logs: [6b2f1c9e\u2026] processing payment\n<\/code><\/pre><\/div><\/p>\n<p>It needs to show up in the response too, not just in your internal logs, so a client reporting a bug can hand you the exact ID and you can trace their specific failed request instead of asking them to describe what happened and guessing at the timestamp.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">When would GraphQL genuinely be a better fit than REST, and when is it overkill?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">GraphQL vs REST<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>GraphQL earns its complexity when you have many different clients, web, mobile, a partner integration, each needing a different shape and depth of the same underlying data, and a REST API would otherwise force you into either over-fetching, sending fields no client on this call needs, or a combinatorial explosion of narrow endpoints to serve each client&#8217;s exact shape.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">text<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-text\">\n\n\/\/ REST: mobile client gets fields it doesn\u2019t need, or you build\n\n\/\/ \/users\/42?fields=name,avatar as a workaround endpoint\nquery {\n\n  user(id: \u201c42\u201d) { name avatar }\n\n}\n\n\/\/ GraphQL: client asks for exactly what it needs, one query, one round trip\n<\/code><\/pre><\/div><\/p>\n<p>It&#8217;s overkill for a single internal service with one or two known consumers, where the flexibility GraphQL buys you costs real complexity, resolver performance tuning, N+1 query problems inside resolvers, a whole separate caching story, that a plain REST endpoint with a couple of well-designed query parameters would&#8217;ve handled without any of that overhead.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">gRPC versus REST for internal service-to-service communication, what&#039;s the actual tradeoff?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">gRPC vs REST<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>gRPC uses Protocol Buffers over HTTP\/2, which gives you a strongly typed contract generated directly into client and server code, smaller payloads than JSON, and built-in support for bidirectional streaming. REST over JSON is human-readable straight out of the box, debuggable with curl and a browser, and has essentially universal tooling and library support across every language and platform, public or internal.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">text<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-text\">\n\n\/\/ gRPC: typed, generated stubs, binary wire format, streaming built in\n\n\/\/ REST\/JSON: readable in a browser dev tools tab, curlable, no codegen step needed\n<\/code><\/pre><\/div><\/p>\n<p>gRPC tends to win for internal service-to-service calls where both ends are your own code and you control the deploy of both, since the type safety and performance gains are real and you don&#8217;t pay for the debuggability tax as often. REST wins the moment you have external or third-party consumers, since JSON&#8217;s universal readability and REST&#8217;s familiarity lower the integration cost for people who aren&#8217;t on your team.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-dsec iq-dsec--hard\"><div class=\"iq-dsec__row\"><h2 class=\"iq-dsec__h\" id=\"hard\"><span class=\"iq-dsec__dot\" aria-hidden=\"true\"><\/span>Hard questions<\/h2><span class=\"iq-dsec__n\">12<\/span><\/div><div class=\"iq-dsec__bar\" aria-hidden=\"true\"><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">Why is PATCH not idempotent by the HTTP spec, even though most people treat it like it is?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">HTTP Methods<\/span><span class=\"iq-badge iq-badge--hard\">Hard<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>RFC 9110 defines idempotency by the effect on server state, not by the shape of the request. PATCH describes a set of changes to apply, and if that patch is expressed as a relative operation, like &#8220;increment the counter by one&#8221; or a JSON Patch &#8220;add&#8221; to an array, applying it twice produces a different end state than applying it once. That&#8217;s the technical reason PATCH isn&#8217;t guaranteed idempotent.<\/p>\n<p>Most real-world PATCH endpoints only accept partial replacements of fields, &#8220;set status to shipped,&#8221; which happen to be idempotent in practice. That&#8217;s a property of how your API chooses to interpret the patch document, not a guarantee the HTTP method gives you for free. If your PATCH endpoint accepts increment-style operations, you need your own idempotency mechanism on top, the same idempotency key pattern used for POST.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">Should error codes in your API be your own enum, or should you reuse HTTP status codes as the only signal?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Error Handling<\/span><span class=\"iq-badge iq-badge--hard\">Hard<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Use both, at different layers. The HTTP status code tells any generic HTTP-aware client, a load balancer, a monitoring dashboard, a browser, broadly what kind of thing happened, without it needing to understand your domain at all. A domain-specific error code in the body tells your actual API client precisely what went wrong in terms it can act on.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">text<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-text\">\n\nHTTP\/1.1 409 Conflict\n\n{ \u201cerror\u201d: { \u201ccode\u201d: \u201cSEAT_ALREADY_BOOKED\u201d, \u201cmessage\u201d: \u201c\u2026\u201d } }\n<\/code><\/pre><\/div><\/p>\n<p>Relying only on the HTTP status code forces every distinct failure into one of roughly sixty status codes, which is nowhere near enough resolution for a real domain. Relying only on your own error code and returning 200 for everything throws away all the tooling, caching, retry logic, and monitoring built around standard HTTP semantics. Neither one alone is enough.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">Why does offset pagination on a frequently-written table produce duplicate or missing rows for the client?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Pagination<\/span><span class=\"iq-badge iq-badge--hard\">Hard<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Offset pagination assumes a stable ordering across requests. If a new row gets inserted ahead of the current offset between the client&#8217;s page-1 and page-2 requests, everything shifts down by one, and the row that was at the boundary between the two pages either gets shown twice or skipped entirely, depending on which direction the insert happened.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">text<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-text\">\n\n\/\/ Client fetches page 1: rows 1-20 (sorted by created_at desc)\n\n\/\/ A new row is inserted at the top before page 2 is requested\n\n\/\/ Client fetches page 2: offset=20, but row 20 has shifted to position 21\n\n\/\/ Result: row 20 is never shown, or row 19 shows up twice, depending on sort direction\n<\/code><\/pre><\/div><\/p>\n<p>Cursor pagination avoids this because the cursor is a pointer to an actual row, not a numeric position, so it stays correct regardless of how many rows get inserted elsewhere in the table.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">How long should you support an old API version once you ship a new one, and how do you decide when to sunset it?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Versioning<\/span><span class=\"iq-badge iq-badge--hard\">Hard<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>There&#8217;s no universal number, but the decision shouldn&#8217;t be arbitrary. It depends on how much usage the old version still gets, how much it costs you to maintain two code paths, and what commitment you made to integrators when you shipped v1 in the first place. A public API with third-party developers you can&#8217;t directly contact typically needs a much longer runway, six months to a year or more with clear deprecation notices, than an internal API where you can grep for every caller and coordinate the cutover directly.<\/p>\n<p>Whatever the number, the mechanism matters more than the duration: return a <code>Deprecation<\/code> or <code>Sunset<\/code> header on every response from the old version so automated tooling can flag it, and actually track per-client usage of the deprecated version so you know when it&#8217;s safe to turn off rather than guessing.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">If you can&#039;t version by URL or header for some reason, how would you evolve a response schema without breaking existing clients?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Versioning<\/span><span class=\"iq-badge iq-badge--hard\">Hard<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Additive-only changes: new fields get added alongside the old ones rather than replacing them, and anything you actually want to remove gets deprecated in place, marked in documentation, possibly flagged with a response header, but kept returning the old value for a long transition window rather than deleted outright the moment you&#8217;d prefer to stop maintaining it.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">text<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-text\">\n\n\/\/ Before\n\n{ \u201camount_cents\u201d: 5000 }\n\/\/ Transition period: both fields present, old one still populated correctly\n\n{ \u201camount_cents\u201d: 5000, \u201camount\u201d: { \u201cvalue\u201d: 50.00, \u201ccurrency\u201d: \u201cUSD\u201d } }\n\/\/ Only after usage of amount_cents drops to near zero do you actually remove it\n<\/code><\/pre><\/div><\/p>\n<p>This is slower and uglier than a clean version bump, but it&#8217;s the only option if you genuinely can&#8217;t force clients onto a new version, which is common for internal APIs with dozens of undocumented consumers nobody wants to track down individually.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">If you&#039;re using JWTs for auth, how do you actually revoke one before it expires?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Authentication<\/span><span class=\"iq-badge iq-badge--hard\">Hard<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>You can&#8217;t, not directly, that&#8217;s the tradeoff you accept by choosing a stateless, self-contained token. A JWT is valid until its expiry claim says otherwise, and there&#8217;s no server-side session to delete. The workarounds all reintroduce some state: a denylist of revoked token IDs checked on every request, which brings back the database lookup JWTs were supposed to let you avoid, or short expiry times paired with a refresh token you can actually revoke server-side.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">text<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-text\">\n\n{\n\n  \u201csub\u201d: \u201cuser_123\u201d,\n\n  \u201cexp\u201d: 1735689600,\n\n  \u201cjti\u201d: \u201ca1b2c3\u201d   \/\/ unique token ID, needed if you want a revocation list at all\n\n}\n<\/code><\/pre><\/div><\/p>\n<p>Teams that pick JWTs specifically because &#8220;no database lookup on every request&#8221; often end up adding one back the moment a real security incident forces them to revoke a token immediately. Short-lived access tokens, 15 minutes is common, plus a revocable refresh token, gets you most of the performance benefit without pretending revocation isn&#8217;t a real requirement.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">Where should authorization checks live, in the API gateway or in the service handling the request?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Authorization<\/span><span class=\"iq-badge iq-badge--hard\">Hard<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Coarse checks, is this token valid, is this client allowed to call this API at all, belong at the gateway, where they&#8217;re cheap to enforce once for every request and keep unauthenticated garbage away from your services entirely. Fine-grained checks, can this specific user edit this specific order, almost always need to live in the service itself, because that decision depends on data, who owns the order, what organization it belongs to, that the gateway usually doesn&#8217;t have loaded.<\/p>\n<p>Pushing fine-grained authorization into the gateway sounds appealing for consistency, but it usually means the gateway ends up needing a copy of your domain&#8217;s ownership model just to make that call, which turns a stateless routing layer into another service you have to keep in sync. Keep the gateway dumb about your domain and let the service that owns the data make the final call.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">How long should you keep idempotency keys around, and what happens if two requests with the same key arrive with different bodies?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Idempotency<\/span><span class=\"iq-badge iq-badge--hard\">Hard<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Long enough to cover any realistic client retry window, 24 hours is a common choice, then expire and reclaim the key. Keeping them forever means an unbounded storage cost for no real benefit, since no client is retrying a request from a year ago.<\/p>\n<p>A same-key-different-body request is a client bug, and the safest server behavior is to reject it with a 422 rather than silently processing either the old or the new payload. Silently accepting the new body would defeat the whole point of the key, since it means &#8220;retry&#8221; and &#8220;different request&#8221; become indistinguishable to the server.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">Why is optimistic concurrency usually preferred over pessimistic locking for a public-facing API?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Concurrency<\/span><span class=\"iq-badge iq-badge--hard\">Hard<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Pessimistic locking, holding a lock on the resource for the duration of an edit, requires every client to correctly acquire and release the lock, and a client that crashes, disconnects, or just forgets mid-edit leaves the resource locked for everyone else until a timeout kicks in. For an API where you don&#8217;t control the client, a browser tab left open, a mobile app that gets backgrounded, that&#8217;s a real operational hazard.<\/p>\n<p>Optimistic concurrency doesn&#8217;t have that failure mode, since there&#8217;s no lock to leak, only a version check at write time. The cost is that clients need to handle the conflict case, a 412 response, gracefully, usually by re-fetching and either merging or asking the user to resolve the conflict. That&#8217;s a better tradeoff for public APIs than trusting every client to release a lock correctly.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What&#039;s the risk of putting business logic that depends on request order behind a stateless, horizontally scaled API?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Scalability<\/span><span class=\"iq-badge iq-badge--hard\">Hard<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Statelessness is what lets you scale an API horizontally, any instance can handle any request, since none of them are holding client-specific state in memory. The risk shows up when your logic silently assumes ordering that the infrastructure doesn&#8217;t actually guarantee, two requests from the same client hitting two different instances at nearly the same time, with no shared lock or ordering mechanism between them, can be processed in either order or even concurrently.<\/p>\n<p>If ordering genuinely matters, a sequence number the client increments, or a database-level constraint that rejects an out-of-order update, needs to enforce it explicitly. You can&#8217;t rely on &#8220;the requests probably arrive in the order they were sent&#8221; once you&#8217;re behind a load balancer distributing across multiple stateless instances.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">A batch endpoint processes 500 items and item 300 fails halfway through a multi-step write. How do you avoid leaving data in an inconsistent state?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Bulk Operations<\/span><span class=\"iq-badge iq-badge--hard\">Hard<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Decide up front whether the batch is all-or-nothing or best-effort, and design the failure handling to match, don&#8217;t let it become accidentally inconsistent because nobody decided. All-or-nothing wraps the entire batch in a single database transaction and rolls back everything if any item fails, which is correct but means one bad record blocks 499 good ones. Best-effort processes each item independently and reports per-item results, which is more forgiving to the caller but means a partial batch failure is a normal, expected outcome your API contract needs to document clearly.<\/p>\n<p>The mistake to avoid is a batch that&#8217;s implemented as best-effort under the hood, each item committed independently, but documented and treated by callers as if it were all-or-nothing. That mismatch is exactly how a caller ends up assuming a batch either fully succeeded or fully failed, retries the whole thing after a partial failure, and ends up with duplicate records for the items that already succeeded the first time.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">What is HATEOAS, and why do so few production APIs actually implement it?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">HATEOAS<\/span><span class=\"iq-badge iq-badge--hard\">Hard<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Hypermedia as the engine of application state, one of Fielding&#8217;s original REST constraints, means a response includes links describing what actions are possible next, so a client doesn&#8217;t need to hardcode URL patterns, it navigates the API the way a browser navigates linked web pages.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">text<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-text\">\n\n{\n\n  \u201cid\u201d: 42,\n\n  \u201cstatus\u201d: \u201cpending\u201d,\n\n  \u201c_links\u201d: {\n\n    \u201cself\u201d: { \u201chref\u201d: \u201c\/orders\/42\u201d },\n\n    \u201ccancel\u201d: { \u201chref\u201d: \u201c\/orders\/42\/cancel\u201d },\n\n    \u201cpay\u201d: { \u201chref\u201d: \u201c\/orders\/42\/pay\u201d }\n\n  }\n\n}\n<\/code><\/pre><\/div><\/p>\n<p>It&#8217;s rare in practice because most API clients are written once against fixed documentation and don&#8217;t actually follow links dynamically, so the extra payload and design effort buys little real benefit for the common case of &#8220;a mobile app calling endpoints its developer already knows about.&#8221; It matters more for APIs meant to be explored generically, a truly generic client that adapts as the API evolves without a code change, which is a narrower use case than most teams are actually solving for.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-dsec iq-dsec--scenario\"><div class=\"iq-dsec__row\"><h2 class=\"iq-dsec__h\" id=\"scenario\"><span class=\"iq-dsec__dot\" aria-hidden=\"true\"><\/span>Real-time scenario questions<\/h2><span class=\"iq-dsec__n\">10<\/span><\/div><div class=\"iq-dsec__bar\" aria-hidden=\"true\"><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">Why would you design a cancel-order endpoint as a state transition instead of a DELETE?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Resource Modeling<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>DELETE implies the resource stops existing. An order that&#8217;s been canceled hasn&#8217;t disappeared, it still needs to exist for the refund, the audit trail, and the customer&#8217;s order history. Modeling cancellation as a DELETE either forces you to fake a soft-delete under the hood, which confuses anyone reading the API surface, or it genuinely deletes data you&#8217;ll regret losing later.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">text<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-text\">\n\nPOST \/orders\/{id}\/cancel\n\n\/\/ or, if you model status as a sub-resource:\n\nPUT \/orders\/{id}\/status  { \u201cstatus\u201d: \u201ccanceled\u201d }\n<\/code><\/pre><\/div><\/p>\n<p>The general rule: DELETE for resources whose existence is genuinely done, a draft comment nobody&#8217;s replied to. A state transition endpoint, usually a POST on an action sub-path, for anything where the record needs to persist with a new status attached.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">Design an error response body. What fields does it actually need?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Error Handling<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Google&#8217;s API design guidance settles on a model with a numeric code, a human-readable message, and a details array for machine-readable context (<a href=\"https:\/\/google.aip.dev\/193\" target=\"_blank\" rel=\"noopener noreferrer\">Google AIP-193, Errors<\/a>). The core idea worth stealing regardless of whose convention you follow: separate the message meant for a developer to read from the machine-readable identifier a client can branch on programmatically.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">text<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-text\">\n\n{\n\n  \u201cerror\u201d: {\n\n    \u201ccode\u201d: \u201cINVALID_ARGUMENT\u201d,\n\n    \u201cmessage\u201d: \u201cend_date must be after start_date\u201d,\n\n    \u201cdetails\u201d: [\n\n      { \u201cfield\u201d: \u201cend_date\u201d, \u201creason\u201d: \u201cmust be after start_date\u201d }\n\n    ]\n\n  }\n\n}\n<\/code><\/pre><\/div><\/p>\n<p>The mistake I see constantly: an error body that&#8217;s just <code>{ \"error\": \"Something went wrong\" }<\/code>. That gives a client nothing to branch on and nothing to show the user beyond a generic failure message. A stable machine-readable code is what lets client code do something smarter than &#8220;display a toast and give up.&#8221;<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">How would you design filtering and sorting on a list endpoint so it stays consistent as new filterable fields get added?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Filtering<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Query parameters that map directly onto field names, with a consistent operator convention for anything beyond equality, tend to age better than a bespoke query language you invent per endpoint. Reserve a small set of suffix or bracket conventions for comparisons, and keep sorting on a dedicated <code>sort<\/code> parameter rather than overloading filter parameters to also mean order.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">text<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-text\">\n\nGET \/orders?status=shipped&amp;total_gte=100&amp;sort=-created_at\n\n\/\/ status=shipped        equality filter\n\n\/\/ total_gte=100         comparison filter\n\n\/\/ sort=-created_at      descending sort, \u201c-\u201d prefix for direction\n<\/code><\/pre><\/div><\/p>\n<p>The failure mode to avoid is a different filter syntax on every endpoint because each one was built by a different engineer at a different time. Pick the convention once, document it, and apply it everywhere, even if the convention itself is a little arbitrary.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">How would you design rate limiting so it&#039;s fair to a burst of legitimate traffic but still stops abuse?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Rate Limiting<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>A fixed window counter, &#8220;100 requests per minute,&#8221; is the simplest to implement but has a real edge case: a client can send 100 requests in the last second of one window and another 100 in the first second of the next, 200 requests in roughly two seconds while technically staying within both windows&#8217; limits. A token bucket or sliding window log smooths that out, allowing bursts up to a cap while still enforcing an average rate over time.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">text<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-text\">\n\n\/\/ Token bucket: bucket refills at a steady rate, requests consume tokens\n\n\/\/ Allows a legitimate burst (e.g. a client catching up after a network blip)\n\n\/\/ without allowing sustained abuse once the bucket is empty\n<\/code><\/pre><\/div><\/p>\n<p>Whichever algorithm you pick, return the limit, remaining count, and reset time as headers so well-behaved clients can self-throttle before they ever hit 429, rather than discovering the limit by getting rejected.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">How would you design an endpoint that kicks off a long-running operation, like generating a large export file?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Async Operations<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Return 202 Accepted immediately with a reference to a job resource the client can poll, instead of holding the connection open until the export finishes, which risks a timeout on either side for anything that takes more than a few seconds.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">text<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-text\">\n\nPOST \/exports\n\n\u2192 202 Accepted\n\n{ \u201cjob_id\u201d: \u201cexp_123\u201d, \u201cstatus\u201d: \u201cpending\u201d }\nGET \/exports\/exp_123\n\n\u2192 { \u201cstatus\u201d: \u201cprocessing\u201d }\n\n\u2192 { \u201cstatus\u201d: \u201ccompleted\u201d, \u201cdownload_url\u201d: \u201chttps:\/\/\u2026\u201d }\n<\/code><\/pre><\/div><\/p>\n<p>A common upgrade on top of polling is a webhook, the client registers a callback URL and the server posts to it once the job finishes, saving the client from repeatedly polling an endpoint that&#8217;s returning &#8220;still processing&#8221; ninety percent of the time. Polling is simpler to implement on both sides and a reasonable default unless your clients specifically need the lower latency of a push notification.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">A single endpoint is suddenly the slowest thing in your API, walk through how you&#039;d find out why.<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Performance<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Start with what the endpoint actually does end to end: is it a database query that&#8217;s missing an index and now scanning a growing table, a fan-out to several downstream services where one of them slowed down, or serialization overhead on a response that&#8217;s grown much larger than it used to be. Tracing, a request ID that follows the call through every downstream hop with timing at each step, is what actually answers this instead of guessing.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">text<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-text\">\n\ntrace_id: abc-123\n\n  \u2192 auth check: 3ms\n\n  \u2192 DB query (orders): 840ms  \u2190 the actual bottleneck\n\n  \u2192 inventory service call: 12ms\n\n  \u2192 response serialization: 4ms\n<\/code><\/pre><\/div><\/p>\n<p>Nine times out of ten in my experience it&#8217;s a database query that used to be fast on a small table and slowed down as the table grew, missing an index, or an N+1 pattern where the endpoint loops and issues one query per related row instead of a single join or batched fetch.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">How would you design a bulk endpoint that creates or updates many records in one call?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Bulk Operations<\/span><span class=\"iq-badge iq-badge--medium\">Medium<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Accept an array of items in a single POST, and return a per-item result array so the caller knows exactly which items succeeded and which failed, rather than a single pass-fail status for the entire batch. A batch of a thousand records where item 500 has a bad value shouldn&#8217;t force the other 999 to fail along with it.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">text<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-text\">\n\nPOST \/orders\/bulk\n\n{ \u201citems\u201d: [ { \u201cproduct_id\u201d: \u201cp1\u201d, \u201cqty\u201d: 2 }, { \u201cproduct_id\u201d: \u201cbad\u201d, \u201cqty\u201d: -1 } ] }\n\u2192 207 Multi-Status\n\n{\n\n  \u201cresults\u201d: [\n\n    { \u201cindex\u201d: 0, \u201cstatus\u201d: \u201ccreated\u201d, \u201cid\u201d: \u201co_1\u201d },\n\n    { \u201cindex\u201d: 1, \u201cstatus\u201d: \u201cerror\u201d, \u201cerror\u201d: { \u201ccode\u201d: \u201cINVALID_QUANTITY\u201d } }\n\n  ]\n\n}\n<\/code><\/pre><\/div><\/p>\n<p>207 Multi-Status is the honest response code here, since the request as a whole neither fully succeeded nor fully failed. Plenty of APIs skip it and just return 200 with the per-item results array, which is a reasonable simplification as long as the response body makes each item&#8217;s outcome unambiguous.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">How would you design an endpoint that needs to update two related resources atomically, say transferring money between two accounts?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Resource Modeling<\/span><span class=\"iq-badge iq-badge--hard\">Hard<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>You model the transfer itself as its own resource, not as two separate PUT calls the client is responsible for sequencing. <code>POST \/transfers<\/code> with both account IDs and the amount in the body, and the server owns the atomicity of debiting one account and crediting the other inside a single database transaction.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">text<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-text\">\n\nPOST \/transfers\n\n{ \u201cfrom_account\u201d: \u201cacc_1\u201d, \u201cto_account\u201d: \u201cacc_2\u201d, \u201camount\u201d: 5000 }\n\n\/\/ server wraps both balance updates in one DB transaction, returns the\n\n\/\/ transfer resource with a status once it\u2019s committed\n<\/code><\/pre><\/div><\/p>\n<p>The wrong answer here is asking the client to call two separate endpoints and hoping they both succeed. Any two-call sequence over the network is a place a partial failure leaves your data inconsistent. If the operation genuinely spans two resources, give it its own resource and its own atomicity guarantee.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">How would you design pagination and filtering consistently if your API needs to support both a REST interface and a GraphQL layer on top of the same data?<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">API Architecture<\/span><span class=\"iq-badge iq-badge--hard\">Hard<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>Push pagination and filtering logic down into a shared data-access layer both interfaces call into, rather than letting REST handlers and GraphQL resolvers each reimplement their own cursor logic against the database directly. GraphQL&#8217;s Relay-style connection spec and a REST cursor-based scheme are different shapes on the wire, but they can wrap the exact same underlying cursor and query logic.<\/p>\n<p><div class=\"iq-code not-prose\"><div class=\"iq-code__bar\"><span class=\"iq-code__lang\">text<\/span><button class=\"iq-code__copy\" type=\"button\">Copy<\/button><\/div><pre><code class=\"language-text\">\n\n\/\/ Shared layer\n\nfunction fetchOrders({ cursor, limit, filters }) { \u2026 }\n\/\/ REST handler\n\nGET \/orders?cursor=xyz&amp;limit=20  \u2192 calls fetchOrders(\u2026)\n\/\/ GraphQL resolver\n\norders(first: 20, after: \u201cxyz\u201d) { \u2026 } \u2192 calls fetchOrders(\u2026)\n<\/code><\/pre><\/div><\/p>\n<p>The failure mode to avoid is two independently maintained pagination implementations against the same table, which drift apart over time, one gets a bug fix or an index optimization the other never receives, and eventually they return subtly different results for what should be the same query.<\/p>\n<p><\/div><\/div><\/div>\n<div class=\"iq-qa\"><button class=\"iq-qa__q\" type=\"button\" aria-expanded=\"false\"><span class=\"iq-qa__qtext\">Your team wants to add a new required field to a widely used API response. Walk through how you&#039;d actually ship that without breaking clients.<\/span><span class=\"iq-qa__meta\"><span class=\"iq-qa__tag\">Breaking Changes<\/span><span class=\"iq-badge iq-badge--hard\">Hard<\/span><span class=\"iq-qa__chev\" aria-hidden=\"true\"><\/span><\/span><\/button><div class=\"iq-qa__a\"><div class=\"iq-qa__a-inner\"><\/p>\n<p>You don&#8217;t add it as required, not at first, no matter how much the data model wants it to be required conceptually. Ship it as optional, and populate it for every response so existing clients that ignore unknown fields are unaffected, while clients that want to start relying on it can begin doing so. Only after usage data confirms every active client is sending or handling the field correctly would you consider a major version bump that finally marks it required, and even then it&#8217;s usually a new version most teams choose not to force.<\/p>\n<p>The tempting fast path, &#8220;everyone should be sending this anyway, let&#8217;s just require it,&#8221; ignores every client you haven&#8217;t personally audited, an old mobile app version still in app stores, a partner&#8217;s integration nobody on your team has looked at in a year, a scheduled batch job someone wrote once and forgot about. Backward compatibility isn&#8217;t about what a well-behaved client does, it&#8217;s about what every client that exists actually does.<\/p>\n<p><\/div><\/div><\/div>\n<h2>How to prepare for an API design interview<\/h2>\n<p>Skip re-reading a REST cheat sheet the night before. Design one real endpoint end to end on paper: pick a resource, decide its verbs, write the actual JSON for a success response and an error response, decide your pagination scheme, and write down what happens when two clients hit it at the same time. Most API design interviews are really testing whether you&#8217;ve thought through the second and third order questions once, not whether you can recite HTTP status codes from memory.<\/p>\n<p>Across backend and platform mock interviews run through LastRoundAI, the idempotency-key question and the offset-versus-cursor pagination question come up more often than any status-code trivia. Neither is exotic, but a lot of candidates who&#8217;ve built plenty of CRUD APIs have genuinely never had to design for a retry or a concurrent write, because their APIs never got popular enough for that failure mode to show up in production. It&#8217;s worth rehearsing out loud, not just reading about, since the follow-up question is almost always &#8220;what happens if that request gets sent twice.&#8221;<\/p>\n<p>One more thing worth knowing heading into 2026: the Google API design guide that a lot of the error-model and versioning conventions above are pulled from moved to a new documentation domain this year, the older cloud.google.com\/apis\/design URLs now redirect to google.aip.dev and docs.cloud.google.com. If an interviewer references &#8220;AIP&#8221; style numbering for error codes or resource naming, that&#8217;s the guide they mean, and it&#8217;s worth a skim beforehand if API design is the explicit focus of your loop.<\/p>\n<h2>Get the reps in before the real thing<\/h2>\n<p>Sketching an endpoint on a whiteboard is easier than defending it once an interviewer asks what happens when two requests race, or why you chose PUT instead of PATCH. <a href=\"\/products\/mock-interviews\">LastRoundAI&#8217;s mock interview mode<\/a> runs live system design and API design rounds with real-time follow-up questions in your browser, and the free plan includes 15 credits a month that reset monthly rather than piling up unused. Starter is $19\/mo if a handful of sessions isn&#8217;t enough runway.<\/p>\n<p>Once your answers hold up under a follow-up, the slower part of the job hunt is usually just getting in front of enough backend and platform roles that actually test API design instead of a leetcode-only loop. <a href=\"\/products\/auto-apply\">Auto-Apply<\/a> queues tailored applications for your review, 10 a month on the free plan, up to 400 a month on the Ultimate plan, and nothing goes out until you approve it.<\/p>\n<p>Questions about either product go to contact@lastroundai.com. That&#8217;s the only inbox we check.<\/p>\n<div class=\"iq-sources not-prose\"><div class=\"iq-sources__title\">Sources &amp; further reading<\/div><ul class=\"iq-sources__list\"><li><a href=\"https:\/\/www.rfc-editor.org\/rfc\/rfc9110.html\" target=\"_blank\" rel=\"nofollow noopener\">RFC 9110, HTTP Semantics<\/a><\/li><li><a href=\"https:\/\/google.aip.dev\/193\" target=\"_blank\" rel=\"nofollow noopener\">Google AIP-193, Errors<\/a><\/li><li><a href=\"https:\/\/jsonapi.org\/format\/\" target=\"_blank\" rel=\"nofollow noopener\">JSON:API Specification<\/a><\/li><li><a href=\"https:\/\/restfulapi.net\/rest-api-versioning\/\" target=\"_blank\" rel=\"nofollow noopener\">restfulapi.net, REST API Versioning<\/a><\/li><\/ul><\/div>\n","protected":false},"excerpt":{"rendered":"<p>A staff engineer candidate interviewing for a platform team in early 2026 got asked to design the endpoint for canceling an order. He drew up DELETE \/orders\/{id}, which sounds reasonable until the interviewer pointed out that canceling isn&#8217;t deleting, the order still exists, it just moves to a canceled state with a refund workflow attached&#8230;.<\/p>\n","protected":false},"author":3,"featured_media":2033,"comment_status":"open","ping_status":"closed","template":"","meta":{"_kad_post_transparent":"","_kad_post_title":"","_kad_post_layout":"","_kad_post_sidebar_id":"","_kad_post_content_style":"","_kad_post_vertical_padding":"","_kad_post_feature":"","_kad_post_feature_position":"","_kad_post_header":false,"_kad_post_footer":false,"_kad_post_classname":"","footnotes":""},"tags":[],"class_list":["post-2008","iq","type-iq","status-publish","has-post-thumbnail","hentry"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v27.8 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>API Design Interview Questions (2026) | LastRoundAI<\/title>\n<meta name=\"description\" content=\"50 real API design interview questions: HTTP verbs, idempotency, pagination, versioning, rate limiting, ETags, and REST vs GraphQL vs gRPC, with answers.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/lastroundai.com\/interview-questions\/api-design\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"API Design Interview Questions (2026) | LastRoundAI\" \/>\n<meta property=\"og:description\" content=\"50 real API design interview questions: HTTP verbs, idempotency, pagination, versioning, rate limiting, ETags, and REST vs GraphQL vs gRPC, with answers.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/lastroundai.com\/interview-questions\/api-design\" \/>\n<meta property=\"og:site_name\" content=\"LastRound AI\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data1\" content=\"39 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/lastroundai.com\\\/interview-questions\\\/api-design\",\"url\":\"https:\\\/\\\/lastroundai.com\\\/interview-questions\\\/api-design\",\"name\":\"API Design Interview Questions (2026) | LastRoundAI\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/lastroundai.com\\\/interview-questions\\\/api-design#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/lastroundai.com\\\/interview-questions\\\/api-design#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/iq-api-design-og.png\",\"datePublished\":\"2026-07-19T17:31:04+00:00\",\"description\":\"50 real API design interview questions: HTTP verbs, idempotency, pagination, versioning, rate limiting, ETags, and REST vs GraphQL vs gRPC, with answers.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/lastroundai.com\\\/interview-questions\\\/api-design#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/lastroundai.com\\\/interview-questions\\\/api-design\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/lastroundai.com\\\/interview-questions\\\/api-design#primaryimage\",\"url\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/iq-api-design-og.png\",\"contentUrl\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/iq-api-design-og.png\",\"width\":1200,\"height\":630,\"caption\":\"API Design interview questions \u2014 LastRoundAI\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/lastroundai.com\\\/interview-questions\\\/api-design#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/lastroundai.com\\\/blog\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Interview Questions\",\"item\":\"https:\\\/\\\/lastroundai.com\\\/interview-questions\"},{\"@type\":\"ListItem\",\"position\":3,\"name\":\"API Design Interview Questions (2026): Q&#038;A\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/#website\",\"url\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/\",\"name\":\"LastRound AI\",\"description\":\"Interview Assistant prep, tech careers and AI tools\",\"publisher\":{\"@id\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/#organization\",\"name\":\"LastRound AI\",\"url\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/lastroundai-transprant-logo-optimized-BxEo2Wtq.png\",\"contentUrl\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/lastroundai-transprant-logo-optimized-BxEo2Wtq.png\",\"width\":400,\"height\":400,\"caption\":\"LastRound AI\"},\"image\":{\"@id\":\"https:\\\/\\\/lastroundai.com\\\/blog\\\/#\\\/schema\\\/logo\\\/image\\\/\"}}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"API Design Interview Questions (2026) | LastRoundAI","description":"50 real API design interview questions: HTTP verbs, idempotency, pagination, versioning, rate limiting, ETags, and REST vs GraphQL vs gRPC, with answers.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/lastroundai.com\/interview-questions\/api-design","og_locale":"en_US","og_type":"article","og_title":"API Design Interview Questions (2026) | LastRoundAI","og_description":"50 real API design interview questions: HTTP verbs, idempotency, pagination, versioning, rate limiting, ETags, and REST vs GraphQL vs gRPC, with answers.","og_url":"https:\/\/lastroundai.com\/interview-questions\/api-design","og_site_name":"LastRound AI","twitter_card":"summary_large_image","twitter_misc":{"Est. reading time":"39 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/lastroundai.com\/interview-questions\/api-design","url":"https:\/\/lastroundai.com\/interview-questions\/api-design","name":"API Design Interview Questions (2026) | LastRoundAI","isPartOf":{"@id":"https:\/\/lastroundai.com\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/lastroundai.com\/interview-questions\/api-design#primaryimage"},"image":{"@id":"https:\/\/lastroundai.com\/interview-questions\/api-design#primaryimage"},"thumbnailUrl":"https:\/\/lastroundai.com\/blog\/wp-content\/uploads\/2026\/07\/iq-api-design-og.png","datePublished":"2026-07-19T17:31:04+00:00","description":"50 real API design interview questions: HTTP verbs, idempotency, pagination, versioning, rate limiting, ETags, and REST vs GraphQL vs gRPC, with answers.","breadcrumb":{"@id":"https:\/\/lastroundai.com\/interview-questions\/api-design#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/lastroundai.com\/interview-questions\/api-design"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/lastroundai.com\/interview-questions\/api-design#primaryimage","url":"https:\/\/lastroundai.com\/blog\/wp-content\/uploads\/2026\/07\/iq-api-design-og.png","contentUrl":"https:\/\/lastroundai.com\/blog\/wp-content\/uploads\/2026\/07\/iq-api-design-og.png","width":1200,"height":630,"caption":"API Design interview questions \u2014 LastRoundAI"},{"@type":"BreadcrumbList","@id":"https:\/\/lastroundai.com\/interview-questions\/api-design#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/lastroundai.com\/blog"},{"@type":"ListItem","position":2,"name":"Interview Questions","item":"https:\/\/lastroundai.com\/interview-questions"},{"@type":"ListItem","position":3,"name":"API Design Interview Questions (2026): Q&#038;A"}]},{"@type":"WebSite","@id":"https:\/\/lastroundai.com\/blog\/#website","url":"https:\/\/lastroundai.com\/blog\/","name":"LastRound AI","description":"Interview Assistant prep, tech careers and AI tools","publisher":{"@id":"https:\/\/lastroundai.com\/blog\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/lastroundai.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/lastroundai.com\/blog\/#organization","name":"LastRound AI","url":"https:\/\/lastroundai.com\/blog\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/lastroundai.com\/blog\/#\/schema\/logo\/image\/","url":"https:\/\/lastroundai.com\/blog\/wp-content\/uploads\/2026\/06\/lastroundai-transprant-logo-optimized-BxEo2Wtq.png","contentUrl":"https:\/\/lastroundai.com\/blog\/wp-content\/uploads\/2026\/06\/lastroundai-transprant-logo-optimized-BxEo2Wtq.png","width":400,"height":400,"caption":"LastRound AI"},"image":{"@id":"https:\/\/lastroundai.com\/blog\/#\/schema\/logo\/image\/"}}]}},"_links":{"self":[{"href":"https:\/\/lastroundai.com\/blog\/wp-json\/wp\/v2\/iq\/2008","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/lastroundai.com\/blog\/wp-json\/wp\/v2\/iq"}],"about":[{"href":"https:\/\/lastroundai.com\/blog\/wp-json\/wp\/v2\/types\/iq"}],"author":[{"embeddable":true,"href":"https:\/\/lastroundai.com\/blog\/wp-json\/wp\/v2\/users\/3"}],"replies":[{"embeddable":true,"href":"https:\/\/lastroundai.com\/blog\/wp-json\/wp\/v2\/comments?post=2008"}],"version-history":[{"count":1,"href":"https:\/\/lastroundai.com\/blog\/wp-json\/wp\/v2\/iq\/2008\/revisions"}],"predecessor-version":[{"id":2032,"href":"https:\/\/lastroundai.com\/blog\/wp-json\/wp\/v2\/iq\/2008\/revisions\/2032"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/lastroundai.com\/blog\/wp-json\/wp\/v2\/media\/2033"}],"wp:attachment":[{"href":"https:\/\/lastroundai.com\/blog\/wp-json\/wp\/v2\/media?parent=2008"}],"wp:term":[{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/lastroundai.com\/blog\/wp-json\/wp\/v2\/tags?post=2008"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}