RESTful API Design Questions
Designing resource-oriented HTTP APIs following REST constraints: resource modeling, URI structure, correct use of HTTP methods, statelessness, and HATEOAS trade-offs. Covers naming conventions, collection vs. singleton resources, filtering/sorting/pagination, and choosing appropriate status codes. The default paradigm most interview questions in this category probe.
Your organization wants a single standard shape for API error responses instead of every team inventing its own. Explain what the Problem Details for HTTP APIs standard (RFC 7807) specifies as required versus optional fields, and what you would add beyond the standard (for example a machine-readable error code, a correlation id, and a retryable flag) to make it genuinely useful for SDK authors and partner integrators.
Sample Answer
Direct answer. RFC 7807 (Problem Details for HTTP APIs) standardizes a small set of fields so error responses across different APIs, and different teams within one company, all have the same recognizable shape instead of every team inventing its own.
Required-in-spirit vs. optional fields. The standard defines five members, none of which are strictly mandatory by the RFC itself, but which only earn the "Problem Details" name when used together: type (a URI identifying the problem type, defaulting to "about:blank" if you have not documented one), title (a short, human-readable summary that should be the SAME for every occurrence of this problem type, not per-instance), status (the HTTP status code, repeated in the body for convenience since some clients only see the body), detail (a human-readable explanation specific to THIS occurrence, unlike the generic title), and instance (a URI identifying this specific occurrence, useful for correlation). The spec is explicitly EXTENSIBLE: you are expected to add your own fields on top for anything domain-specific.
What you would add beyond the standard. A machine-readable error_code (the standard's type/title are meant to be somewhat human-facing and are not guaranteed unique or stable enough for client code to branch on reliably), a correlation_id (the standard has no built-in concept of a trace or request id), and a retryable boolean (the standard says nothing about whether a client should retry). None of this conflicts with the spec, since RFC 7807 is deliberately a small, extensible core rather than a complete error contract.
Why bother with a named standard instead of just inventing your own shape. Two real benefits: existing HTTP client libraries and API gateways increasingly recognize the application/problem+json content type and can surface it specially (rather than treating every error body as an opaque, one-off shape), and new engineers or partner integrators who have seen RFC 7807 elsewhere immediately recognize the shape of your errors instead of needing to learn a company-specific convention from scratch.
Trade-offs and pitfalls. The most common mistake is adopting the standard's field NAMES but not its actual DISCIPLINE: repeating the same generic title across genuinely different problem types (so it stops being useful for grouping/aggregation), or putting instance-specific detail into title instead of detail, defeating the distinction the spec draws between the two.
HTTP/2 and HTTP/3 change some of the assumptions REST API design grew up under: request multiplexing over one connection, header compression, and (in HTTP/3) a transport built on QUIC instead of TCP. What actually changes for a REST API's design and operations when you move from HTTP/1.1 to HTTP/2 or HTTP/3, for example does the old advice to avoid too many small requests still apply, and what should change in your load-balancing and CDN configuration?
Sample Answer
Direct answer. HTTP/2's multiplexing removes the old "avoid too many small requests, batch them into one big one" advice almost entirely for requests to the SAME origin, since many requests can now share one connection with no head-of-line blocking at the HTTP layer; HTTP/3's move to QUIC (over UDP instead of TCP) additionally removes TCP-level head-of-line blocking and speeds up connection setup, which matters most on flaky mobile networks, and both change what your load balancer and CDN need to actually do.
Multiplexing changes API design guidance that predates it. Under HTTP/1.1, browsers opened a limited number of parallel TCP connections per origin, so an API client benefited from batching many small requests into fewer, larger ones (a classic piece of REST API advice: avoid chatty, many-small-calls API shapes). Under HTTP/2, many logical requests share ONE connection via multiplexed streams, so issuing several smaller, well-scoped requests to the same origin is no longer the performance problem it used to be; the old advice does not fully disappear (a request still has real per-call overhead: headers, server-side routing, business logic), but the SPECIFIC "too many TCP connections" penalty it was originally guarding against is gone. Concretely: under a common browser limit of about 6 concurrent connections per origin under HTTP/1.1, a page issuing 30 small API calls to the same origin would need to queue them into 5 sequential batches of 6; under HTTP/2's multiplexing, all 30 can be in flight at once over the single connection, with no batching required at all.
Header compression (HPACK/QPACK). HPACK is HTTP/2's header-compression format, and QPACK is its HTTP/3 counterpart, redesigned so header compression still works correctly when QUIC delivers streams out of order, which plain HPACK cannot tolerate. Both work by having each side maintain a shared table of previously-seen header name/value pairs: once a header (an auth token, a standard Accept or User-Agent value) has been sent once on a connection, later requests on that SAME connection can send a short reference into that table instead of the full header text again. Repeated headers across many requests to the same connection get compressed incrementally this way, which meaningfully reduces overhead for an API client making many small, frequent calls, exactly the pattern multiplexing now makes more attractive in the first place.
HTTP/3 and QUIC specifically. Because QUIC runs over UDP and manages its own stream multiplexing independently, one lost packet on one logical stream no longer blocks delivery of data on OTHER streams sharing the same connection, unlike TCP where a single lost packet stalls the entire connection until it is retransmitted; this specifically helps a mobile client on a lossy network, which is exactly the scenario where TCP-level head-of-line blocking used to hurt the most. QUIC's connection setup also folds the transport and TLS handshakes together, cutting the round trips needed before the first real request can even be sent, which matters disproportionately on high-latency mobile connections where each round trip is expensive. Concretely: a fresh HTTPS connection over TCP + TLS 1.3 needs 2 round trips before the client can send its first request byte (1 to establish the TCP connection, 1 for the TLS 1.3 handshake); QUIC combines both into one combined handshake, needing just 1 round trip for a new connection (and its 0-RTT mode can send request data on the very first flight when resuming a connection to a host the client has already talked to). On a mobile connection, where a single round trip commonly costs on the order of 50-100ms, that one saved round trip is a real, directly-felt latency difference before any actual request-response work even starts.
What changes in load-balancing and CDN configuration. Load balancers and CDNs need to actually terminate and understand HTTP/2 or HTTP/3 themselves (not simply pass bytes through) to preserve these benefits end-to-end, and per-request load-balancing algorithms that assumed "one connection roughly equals one in-flight request" need rethinking, since one HTTP/2 or HTTP/3 connection can now carry many concurrent logical requests, changing what "an overloaded backend" or "a slow connection" even looks like from the load balancer's point of view. Many CDNs and cloud load balancers already handle this transparently, but a self-managed edge layer built assuming HTTP/1.1 semantics needs deliberate upgrading, not an assumption that the protocol change is invisible to it.
Trade-offs and pitfalls. The most common mistake is treating this purely as an infrastructure upgrade with no API-design implications; an API whose CLIENTS were specifically designed around HTTP/1.1's connection limits (aggressively batching everything into one mega-request "to save connections") may actually see LESS benefit from multiplexing than a more naturally-shaped API would, since it already paid the batching cost the new protocols were meant to make unnecessary.
Design the REST endpoints for a Book resource that also has nested Reviews belonging to it: list, get one, create, update, and delete a book, plus list and add reviews for a book. For each endpoint give the HTTP method, the canonical path, the expected request body where relevant, and the success status code. How do you keep the response shape for the list endpoint from triggering an N+1 query when reviews are included, and how does pagination interact with the nested collection?
Sample Answer
Direct answer. Model Book as the primary collection and Review as a resource nested under it, since a review's identity is meaningfully scoped to the book it is about, and shape the list-with-reviews response to avoid an N+1 by embedding a bounded preview rather than forcing a separate request per book.
The endpoint set.
| Method | Path | Request body | Success code |
|---|---|---|---|
| GET | /books | none (supports ?page=/?limit= for pagination) | 200 |
| GET | /books/{bookId} | none | 200 |
| POST | /books | {"title": "...", "author": "...", "isbn": "..."} | 201, Location: /books/{id} |
| PUT | /books/{bookId} | full replacement, e.g. {"title": "...", "author": "...", "isbn": "..."} | 200 |
| DELETE | /books/{bookId} | none | 204 |
| GET | /books/{bookId}/reviews | none (supports ?page=/?limit= for pagination) | 200 |
| POST | /books/{bookId}/reviews | {"rating": 1-5, "comment": "..."} | 201, Location: /books/{bookId}/reviews/{reviewId} |
Avoiding N+1 on the list endpoint. A naive implementation of GET /books that also wants to show "average rating" or "review count" per book would run one extra query PER book in the returned page, which is the classic N+1 pattern. The fix is to compute those aggregate fields with a single query joined or grouped across the whole page of books at once (one SQL query returning book rows with their review counts pre-aggregated), not a per-book follow-up call; if the client needs the actual review TEXT (not just a count), that stays on the separate /books/{bookId}/reviews endpoint entirely, rather than being embedded in the list response at all, since embedding full review bodies for every book on a list page is exactly the kind of over-fetching a list view does not need. Concretely, for a page of 20 books, the naive approach costs 1 query for the books plus 20 individual per-book count queries -- 21 queries total; the single joined/grouped query described above returns that same page of books with their review counts pre-aggregated in exactly 1 query, regardless of how many books are on the page.
Pagination on the nested collection. GET /books/{bookId}/reviews paginates independently of the parent books list, using the same offset-style ?page=/?limit= convention already shown above for GET /books (so GET /books/42/reviews?page=2&limit=20 is a perfectly ordinary request); a request for page 2 of one book's reviews has nothing to do with which page of the books list the client is currently viewing, since these are two independent, differently-scoped lists.
Trade-offs and pitfalls. The mistake this design avoids is either the fully-flat alternative (a top-level /reviews collection with no book scoping, forcing every client to filter by book_id as a query parameter for what is fundamentally always a book-scoped operation) or the fully-embedded alternative (returning every review inline inside every book response, which explodes payload size the moment a popular book accumulates thousands of reviews); nesting the reviews endpoint under its book, while keeping it as its own independently-paginated resource, is the middle ground that fits how this data is actually used.
Design a JSON error response schema that both your internal teams and external clients will consume: what fields would you include (for example a machine-readable code, a human message, field-level validation detail, and a correlation id for tracing), what belongs in the client response versus only in your logs, and how would a client tell a retryable error from one it should not retry?
Sample Answer
Direct answer. A good error schema separates three concerns that a single "message" string conflates: a stable, machine-readable code a client can branch on programmatically, a human-readable message for logs and debugging, and enough structured detail (which field, what was wrong with it) for a UI to show something more useful than a generic failure.
A concrete shape.
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request failed validation.",
"retryable": false,
"request_id": "req_8f2a1c",
"details": [
{ "field": "email", "issue": "must be a valid email address" },
{ "field": "quantity", "issue": "must be greater than 0" }
]
}
}
code: a stable string a client's error-handling logic can switch on; unlike the HTTP status code alone, it can distinguish "insufficient funds" from "card declined" even though both might return the same 402.message: for humans (logs, a developer reading a support ticket), never the primary thing client CODE should branch on, since it is free to change wording without that being a breaking change.request_id: a correlation id the client can hand back to support, letting you find the exact server-side log line for this request instantly instead of searching by timestamp and endpoint.details: field-level validation information for a form UI to highlight the specific inputs that were wrong.
Client response vs. logs. The client response should NEVER include a stack trace, an internal service name, a raw database error message, or any other detail that reveals your system's internals; that information belongs only in your server-side logs, correlated by the same request_id, so an engineer investigating a support ticket can look up the full internal detail without ever exposing it to the caller.
Retryable vs. not. A retryable boolean (or deriving retryability from the error code via a documented mapping) tells the client whether blindly retrying the exact same request is safe and potentially successful (a transient 503) versus pointless or actively harmful (a 400 validation error, which will fail identically on every retry until the request itself changes). Without this signal, clients either retry everything (wasting calls on errors that can never succeed) or retry nothing (giving up on transient failures a simple retry would have fixed).
Trade-offs and pitfalls. The most common mistake is putting the human message where client code actually parses it, so a later, purely cosmetic wording change ("Invalid email" to "Please provide a valid email") silently breaks any client that was doing string-matching against the message instead of the code.
For a public partner API, an internal high-throughput service mesh, and a mobile client on unreliable networks, would you reach for REST, GraphQL, or gRPC? Compare the three on caching, client flexibility, network overhead, versioning, and developer ergonomics, and justify a different choice (or the same one) for each of the three scenarios.
Sample Answer
Direct answer. REST for the public partner API (broad client reach, cacheability, and simplicity matter most and you cannot control what partners' HTTP tooling supports); gRPC for the internal high-throughput service mesh (binary framing, streaming, and generated strongly-typed clients matter more than human-readability once every caller is a service you control); and REST, or GraphQL if the client genuinely needs to shape a nested response itself, for the unreliable mobile client, prioritizing tolerance of dropped connections and minimizing round trips over raw throughput.
Comparison across the five axes.
- Caching: REST's biggest structural advantage. A GET request maps naturally onto HTTP caching (Cache-Control, ETag, CDN edge caching) because the URL itself identifies the cacheable resource. GraphQL's single POST-based endpoint and gRPC's binary RPC calls both defeat generic HTTP caching by default; both need a purpose-built caching layer if they want it at all.
- Client flexibility: GraphQL wins outright, since the client specifies exactly which fields it wants in one round trip; REST either over-fetches (returns the whole resource) or requires bespoke
?fields=parameters per endpoint; gRPC returns whatever the fixed protobuf schema defines, no client-side shaping at all. - Network overhead: gRPC wins, using compact binary Protocol Buffers (a binary message format — smaller on the wire and faster for both sides to parse than text-based JSON, at the cost of not being readable directly in a browser's network tab) over HTTP/2 multiplexing (many requests and responses share one underlying connection at the same time, instead of the client opening a new connection per request); REST and GraphQL both typically ship JSON, which is larger and slower to parse, though GraphQL's field-selection can reduce payload size by avoiding over-fetch even with the same wire format.
- Versioning: REST has the most established conventions (URI/header/media-type, well understood by every client). GraphQL favors additive, non-breaking schema evolution with field deprecation over versioned endpoints entirely. gRPC's protobuf has strict, well-defined field-numbering rules for safe evolution, but breaking those rules (reusing a field number) is a hard, occasionally very difficult to detect production bug.
- Developer ergonomics: REST is the lowest common denominator, understood by literally every HTTP client, browser dev tool, and API testing tool with zero extra tooling. GraphQL and gRPC both need generated client code or specialized tooling to get real ergonomic benefit, which is friction for a public partner integration but a non-issue for an internal service mesh where every caller already runs your codegen pipeline (codegen — client-side code automatically generated from the service's schema, rather than hand-written by each team calling it).
Applying this to the three scenarios. A public partner API optimizes for the lowest integration cost for partners you do not control and for free CDN caching; REST wins on both. An internal high-throughput mesh optimizes for raw efficiency between services you fully control and can codegen against; gRPC wins. A mobile client on unreliable networks optimizes for minimizing round trips and tolerating partial failure; either a well-designed REST API with sparse fieldsets (a ?fields= query parameter letting the client name only the fields it actually needs back, so the server skips returning the rest of the resource), or GraphQL if the client's data needs are genuinely nested and variable screen-to-screen, both beat a chattier multi-call REST design, and gRPC's lack of native browser/mobile-friendly tooling (without a gRPC-Web proxy layer — an extra translation service that sits in front of gRPC, since a browser cannot speak raw HTTP/2 gRPC directly) makes it the weakest fit here despite its raw efficiency.
Trade-offs and pitfalls. The mistake to avoid is picking one paradigm company-wide "for consistency"; the three scenarios above genuinely optimize for different things, and a mesh forced into REST-for-consistency pays a real latency and serialization cost with no corresponding benefit, just as a public API built in gRPC for consistency loses the free caching and universal client support that made REST the right call there.
Unlock Full Question Bank
Get access to all 33 RESTful API Design interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.