Chapter 11 Interview: API Design
1. How would you design a REST API for a marketplace?
Alright, let me think through the core resources first. A marketplace has sellers, buyers, products, orders, and reviews. Each of these maps naturally to a REST resource with its own collection endpoint.
I'd structure it around nouns: /products, /orders, /sellers/{id}/products for scoping. Standard HTTP verbs — GET for listing/reading, POST for creating, PUT/PATCH for updates, DELETE for removal. Pagination is essential from day one — cursor-based, not offset-based, because offset breaks when items are added or removed mid-pagination.
Search and filtering go on the products collection: GET /products?category=electronics&minPrice=50&sort=relevance. I'd keep the query params flat and predictable. For nested resources, you've got a design choice — /orders/{id}/items vs. /order-items?orderId=123. I prefer shallow nesting (max one level deep) because deep nesting makes URLs fragile.
Authentication via Bearer tokens, rate limiting per API key, and I'd return hypermedia links (at least a "self" link and pagination cursors) so clients aren't hardcoding URL construction. Oh, and idempotency keys on POST /orders — if a payment request gets retried due to a timeout, you don't want to charge someone twice.
- Resource modeling: nouns for URLs, HTTP verbs for actions
- Pagination: cursor-based from the start, not offset
- Shallow nesting: max one level to keep URLs maintainable
- Idempotency: keys on state-changing operations to prevent duplicates
2. When would you choose GraphQL over REST?
GraphQL shines in specific scenarios, and the common thread is client flexibility. If you have multiple clients — web, mobile, smart TV — each needing different slices of the same data, REST forces you into either over-fetching (send everything, client ignores what it doesn't need) or maintaining multiple endpoints per client. GraphQL lets each client request exactly what it needs in one round trip.
The other big win is deeply nested or connected data. Think a social feed: post → author → mutual friends → their recent posts. In REST that's 4-5 sequential requests or one bloated endpoint. In GraphQL it's one query that specifies the exact shape.
But I'd stick with REST when: the API is simple CRUD with predictable access patterns, you need aggressive HTTP caching (GraphQL's single POST endpoint kills cache-ability), you're building a public API where simplicity and discoverability matter, or your team doesn't have GraphQL operational experience — the N+1 query problem in resolvers can destroy your database if you're not careful.
Honestly, for most backend-to-backend communication, REST or gRPC wins. GraphQL is a frontend-facing tool for teams with diverse client needs.
- Choose GraphQL: multiple clients, varying data needs, deeply nested data
- Stick with REST: simple CRUD, public APIs, heavy caching needs
- Watch out: N+1 resolver queries, cache complexity, operational learning curve
- Sweet spot: frontend-facing APIs with diverse client form factors
3. How do you handle API versioning?
There are three main approaches and honestly none are perfect — you're choosing which trade-off you can live with.
URL path versioning — /v1/products, /v2/products. It's the most visible and explicit. Clients know exactly what they're hitting. Downside: you're now maintaining multiple complete API versions, and it encourages big-bang version bumps instead of incremental evolution. But it's simple, debuggable, and works great with caching.
Header versioning — Accept: application/vnd.myapi.v2+json. Cleaner URLs, but harder to test (can't just paste a URL in a browser), harder for clients to discover, and often gets lost in documentation. I've seen teams struggle with this operationally.
My preferred approach: version sparingly by evolving the API in backward-compatible ways for as long as possible. Add fields, don't remove them. New endpoints for new capabilities. Only bump the version when you absolutely must make a breaking change. When you do, URL path versioning — it's the least surprising for consumers.
Whatever you pick, have a deprecation policy. Announce v1 sunset 6+ months ahead. Provide migration guides. Monitor who's still on the old version. And never maintain more than 2 active versions — the operational burden compounds fast.
- URL path: /v1/ — most explicit, great for caching, encourages big bumps
- Header: cleaner URLs but harder to discover and test
- Best practice: evolve compatibly, version only for breaking changes
- Deprecation: 6+ month sunset window, max 2 active versions
4. What makes a good error response?
A good error response answers three questions for the developer: what went wrong, why it went wrong, and what they can do about it. Most APIs only answer the first one — "400 Bad Request" — and leave you guessing.
Structure-wise, I'd include: an HTTP status code that's semantically correct (don't return 200 with an error body — that breaks every HTTP-aware tool), a machine-readable error code (like "INSUFFICIENT_FUNDS" not just "error"), a human-readable message explaining the issue, and a pointer to which field or parameter caused it.
For validation errors, return all the problems at once — don't make the client fix one field, resubmit, discover another issue, repeat. Something like: errors array with field path, code, and message for each.
What I'd avoid: leaking internal details (stack traces, SQL errors, internal service names) — that's a security issue. Also avoid generic messages that don't help anyone. "Something went wrong" tells me nothing. "The 'email' field must be a valid email address" tells me exactly what to fix.
Include a request ID in every error response so when a customer says "it's broken," support can trace exactly what happened in the logs.
- Structure: status code + error code + message + field pointer
- Batch validation: return all errors at once, not one at a time
- Actionable: tell the developer what to fix, not just what broke
- Security: never leak internals; always include a request/trace ID
5. How do you ensure backward compatibility?
Backward compatibility means existing clients keep working when you change the API. It sounds simple but it's surprisingly easy to break things in subtle ways.
The golden rules: adding fields is safe — existing clients just ignore what they don't know. Removing fields breaks clients. Changing a field's type breaks clients. Making an optional field required breaks clients. Even changing the meaning of a value (status "active" now means something different) is a silent break.
Practically, I enforce this with contract tests. Every consumer writes tests describing what they expect from your API. You run those tests in your CI pipeline — if your change breaks any consumer's expectations, the build fails. This catches things code review misses.
For the schema itself: new fields get defaults so old clients that don't send them still work. Deprecated fields stay in the response but get marked in docs. I'd also use feature flags or expansion parameters — clients explicitly opt into new behavior with a query param or header, so the default behavior never changes.
And honestly? The best tool is a changelog and a breaking-change review process. Every PR that touches the API contract gets extra scrutiny from someone who thinks like a consumer.
- Safe changes: add fields, add endpoints, add optional params
- Breaking changes: remove/rename fields, change types, change semantics
- Contract tests: consumers define expectations, run in provider's CI
- Process: breaking-change review, deprecation period, changelogs