System design / API design

API Design for System Design Interviews

How to design the API in a system design interview and in production: choosing REST, GraphQL or gRPC, modelling resources, status codes, pagination, idempotency keys, ETags, async jobs, versioning, auth and rate limits, with 19 diagrams and a 5-minute interview checklist.

On this page
  1. What an API is in a design interview
  2. Choosing a protocol: REST, GraphQL, gRPC or push
  3. REST, part 1: modelling resources
  4. REST, part 2: HTTP methods and what they promise
  5. REST, part 3: where each input belongs
  6. REST, part 4: status codes and response shapes
  7. Worked example: the food-delivery API
  8. GraphQL: one endpoint, client-shaped responses
  9. RPC and gRPC: fast, typed calls between services
  10. Design principles that keep an API usable
  11. Pagination in depth (members only)
  12. Filtering, sorting, field selection and search (members only)
  13. Idempotency keys: making POST safe to retry (members only)
  14. Concurrency control with ETags (members only)
  15. Long-running operations: 202 Accepted (members only)
  16. Error responses clients can act on (members only)
  17. Versioning and evolving an API (members only)
  18. Security: authentication, authorization and trust (members only)
  19. Rate limiting and throttling (members only)
  20. The 5-minute API step: checklist and whiteboard (members only)
  21. Interview questions (members only)

Every system design round has a moment, usually five or ten minutes in, where the interviewer says "Okay, what does the API look like?" It is a short step, but it sets the vocabulary for everything after it: the endpoints you name become the arrows on your diagram, the ids you choose become your partition keys, and the one detail you forget (pagination, idempotency, who is allowed to call what) is exactly where the follow-up questions land. This guide covers the whole surface: how to pick between REST, GraphQL and gRPC, how to model resources, which status codes matter, how pagination really breaks, idempotency keys, ETags, long-running jobs, versioning, auth and rate limits. We use one product the whole way through, a food-delivery app, so every idea lands on something concrete.

After reading this article you will be able to

  • Pick REST, GraphQL, gRPC or a push channel for a given client in under a minute, and defend the choice with one trade-off.
  • Turn a list of core entities into plural-noun resources, choose the right HTTP method for each operation and put every input in the right place (path, query, header or body).
  • Return the right status code, with the right headers, from 201 Created to 429 Too Many Requests, and a consistent error body.
  • Explain why offset pagination repeats and skips rows on live data, and design cursor (keyset) pagination with a stable sort.
  • Make a payment-style POST safe to retry with an idempotency key, and stop lost updates with ETags and If-Match.
  • Handle long-running operations with 202 Accepted, version an API without breaking clients, and secure it with tokens, roles and rate limits.
  • Present a complete API on the whiteboard in about five minutes, and answer the follow-ups interviewers ask most.

01What an API is in a design interview

API (application programming interface) Definition

The contract between a client and your system: which operations exist, what each one accepts, what it returns and what can go wrong. It says nothing about how the work is done behind it.

The best analogy I know is a restaurant menu. The menu lists what you can order, in what sizes and with which options. It does not tell you how the kitchen is laid out, how many cooks are on shift or where the rice is stored. The kitchen can be rebuilt overnight and, as long as the menu stays the same, no diner notices. An API is the menu; your services and databases are the kitchen.

That framing tells you what good API design optimises for. It should be easy for clients to use correctly, hard to use incorrectly, and stable while everything behind it changes. Notice that "mirrors my database tables" is not on that list. We'll come back to that.

Fig. 1

The API is the contract between clients and your system

The API is the contract between clients and your systemMobile, web and partner clients call an API gateway over HTTPS using a public REST or GraphQL API. The gateway authenticates, rate limits and routes each call to internal order, restaurant and payment services, which are reached over gRPC.ClientsOur backendHTTPSgRPCMobile appiOS, AndroidWeb appbrowser SPAPartnerserver to serverAPI gatewayauth, rate limitsrouting, TLSOrder serviceorders, checkoutRestaurant servicemenus, reviewsPayment servicecharges, refundsPublic API: REST (or GraphQL) over HTTPSInternal APIs: gRPC over HTTP/2
The public API is a boundary, not a box. Clients only ever see the contract exposed at the gateway. Behind it, the order, restaurant and payment services can split, merge or change databases without any client noticing, and they usually talk to each other with a different, internal protocol.

Where the API step sits in the interview

Most design rounds follow a similar arc: clarify requirements, list the core entities, define the API, draw the high-level design, then go deep on one or two hard parts. The API step comes right after the entities because it is the bridge between them: each functional requirement becomes one or two endpoints, and each endpoint touches one or two entities.

Here is the part many candidates get wrong in both directions. Most interviewers are not grading API perfection. They want a reasonable, consistent API in about five minutes, so you can get to the scaling and consistency problems where the real signal is. Spending 20 minutes debating PUT vs PATCH costs you the deep dive. Skipping the API entirely costs you the shared vocabulary.

When API design gets more weight

Two exceptions. For frontend, mobile, platform or product-engineering roles, the API is the job, so expect more time on it and sharper questions about payload shape, pagination and errors. For junior roles, interviewers expect less distributed-systems depth, so more of the hour often goes to the API and data model. Ask early: "How much time would you like me to spend on the API?"

For this guide we'll design the public API of a food-delivery app: customers browse restaurants and menus, place and track orders, pay, and leave reviews; restaurant owners edit menus; couriers update deliveries; support staff issue refunds. We'll cover the interview-sized version first in each section, then the production detail you can reach for when the interviewer pushes.

02Choosing a protocol: REST, GraphQL, gRPC or push

You are choosing between three request/response styles, plus one family of push channels for real-time features. Here is each in one breath.

  • REST models your system as resources (nouns) addressed by URLs and manipulated with standard HTTP methods. It rides on everything the web already has: caches, proxies, browsers, curl, status codes. It is the default for public and client-facing APIs.
  • GraphQL exposes one endpoint and a typed schema. The client sends a query naming exactly the fields it wants, across related objects, and gets exactly that back. It shines when many different clients need differently shaped data.
  • RPC, today mostly gRPC, models the system as procedures (verbs) you call like local functions: Quote(orderId), CheckPermission(user, resource). Messages are binary Protocol Buffers over HTTP/2, which makes it compact and fast. It is the usual choice between your own services.
  • WebSockets and Server-Sent Events (SSE) are not request/response APIs at all; they are long-lived connections that let the server push. You add them alongside one of the above for live order tracking, chat or notifications.

The decision rarely needs more than three questions. Before reading on, try answering them for our food app's courier-tracking screen.

Fig. 2

Picking a protocol in three questions

Picking a protocol in three questionsFlowchart. If only your own services call the API, use gRPC. For external clients, if the server must push updates, add WebSockets or Server-Sent Events. Otherwise, if many clients need very different data shapes, consider GraphQL; if not, use REST, the default.internalexternalyesnoyesnoWho calls it: our ownservices or clients?gRPCtyped, binary, fastMust the server pushupdates to the client?WebSocket / SSEalongside RESTDo clients need verydifferent data shapes?GraphQLclient picks fieldsRESTthe defaultMixing is normal: REST outside, gRPC inside, a WebSocket for the live parts
Three questions get you to a defensible answer. Internal service-to-service calls lean to gRPC. Anything the server must push gets a WebSocket or SSE channel next to the main API. For the rest, GraphQL earns its complexity only when clients genuinely need very different shapes of data; otherwise REST.

For courier tracking, the answers are: external client, yes the server must push, so the tracking screen opens an SSE stream (or a WebSocket) while everything else on that screen is REST. SSE is the simpler fit when only the server talks (location updates flowing one way); WebSockets earn their extra complexity when both sides send messages, as in chat.

AspectRESTGraphQLgRPC
Mental modelResources (nouns) + HTTP methodsA typed graph you queryRemote functions (verbs)
EndpointsMany URLsOne, usually POST /graphqlOne method per procedure
PayloadUsually JSON, human readableJSON, shaped by the queryBinary Protocol Buffers
TransportHTTP/1.1 or HTTP/2HTTP (WebSocket for subscriptions)HTTP/2 only; browsers need a gRPC-Web proxy
HTTP cachingExcellent (GET + ETag + CDNs)Hard (one POST URL); needs persisted queries or app-level cachesNone built in
Contract and typesOptional (OpenAPI)Built in (schema)Built in (.proto) with code generation
StreamingNo (pair it with SSE or WebSockets)SubscriptionsServer, client and bidirectional streams
Best fitPublic APIs, CRUD, web and mobileMany clients with different data needsInternal, high-volume service-to-service calls

My default, and the one I'd recommend you say out loud: "I'll use REST for the client-facing API, and gRPC between internal services if latency matters." REST is well understood by every interviewer, has tooling everywhere, and fits the large majority of product APIs. Reach for GraphQL or gRPC only when you can name the specific problem it solves. If you're unsure, say REST and move on; nobody fails a round for picking REST.

Many candidates pick GraphQL to sound modern

Then the interviewer asks how they'll cache the restaurant page at the CDN, or stop a client from requesting a query nested ten levels deep, and the answer is not ready. Pick the tool whose costs you can explain. GraphQL is a great answer when you can say "we have a web dashboard, two mobile apps and a partner widget, all needing different slices of the same data".

03REST, part 1: modelling resources

REST design starts from your core entities, not from your screens or your tables. A resource is any thing a client can name and act on: a restaurant, a menu item, an order, a review. Resources come in two shapes: a collection (/restaurants) and an item inside it (/restaurants/81).

Four naming rules cover almost every interview.

  1. Nouns, not verbs. The HTTP method is the verb. POST /orders, not POST /createOrder; GET /orders/42, not GET /getOrder?id=42.
  2. Plural collections. /restaurants and /restaurants/81 read as "the restaurants collection" and "restaurant 81 in it". Mixing /restaurant/81 and /orders/42 is a small inconsistency clients trip over forever.
  3. Ids in the path identify; everything else goes elsewhere. We'll make this precise in a moment.
  4. Nest only to show ownership, and at most two levels. /restaurants/81/menu-items says "the menu of restaurant 81". Deeper paths get brittle fast.

Here's the resource map I'd sketch for the food app.

Fig. 3

Resources for a food-delivery API

Resources for a food-delivery APICollections such as /restaurants and /orders contain items such as /restaurants/{id}. Items own sub-collections such as menu-items and reviews, one level deep. A path that nests five levels deep is replaced by a flat /reviews/{id} resource.flatten/restaurantscollection/restaurants/{id}one restaurant/restaurants/{id}/menu-itemsits menu/restaurants/{id}/reviewsits reviews/orderscollection/orders/{id}one order/orders/{id}/refundsrefunds of that order/restaurants/{id}/menu-items/{id}/reviews/{id}five levels deep/reviews/{id}flat; links by id fieldsCollectionItemSub-collection (max depth 2)Plural nouns, ids in the path, no verbs, at most two levels deep
Collections, items and one level of sub-collections. Sub-collections express ownership (a menu belongs to a restaurant). Once a path needs three or more ids, flatten it: a review has its own id, so /reviews/{id} works on its own and carries restaurantId and menuItemId as fields.

Nested or flat?

Nesting reads nicely and makes ownership checks obvious in routing, but it forces the client to know every parent id. If a support agent has a review id from a complaint ticket, they should not have to look up its restaurant and menu item first to build the URL. A good compromise is nest for creation and listing, flat for the item itself: POST /restaurants/81/reviews and GET /restaurants/81/reviews, but GET /reviews/9013 and DELETE /reviews/9013.

Your API is not your schema

A tempting shortcut is one endpoint per table: /order_headers, /order_lines, /order_status_history. It leaks your storage design into every client, so you can never refactor the tables without breaking someone, and it makes clients chatty: rendering one order takes three calls. Model resources around what clients think about. An order resource can include its line items and current status even if those live in three tables, or even three services.

The opposite failure is also real: one giant resource that returns everything about a restaurant (200 menu items, 3,000 reviews) because "it saves round trips". Aim for the middle: resources the size of one screen's main object, with related collections reachable by their own paginated endpoint.

Actions that aren't CRUD

Sooner or later you need "cancel this order" or "reorder my last meal". Three reasonable options, in the order I'd suggest them:

  • Change state with PATCH when the action is a field change: PATCH /orders/42 with {"status": "cancelled"}. Works when the server can validate the transition.
  • Create a noun for the action when it has its own data or lifecycle: POST /orders/42/cancellation with a reason, returning a cancellation you can later GET. Refunds work the same way: POST /orders/42/refunds.
  • A custom verb as a last resort: POST /orders/42:cancel. Some large API style guides allow this colon form. It's fine in an interview if you say why the first two didn't fit.

04REST, part 2: HTTP methods and what they promise

Each method makes a promise to every cache, proxy, retry library and client between you and the user. Two properties matter most.

  • Safe: the request does not change server state. Crawlers, prefetchers and caches may send safe requests freely.
  • Idempotent: sending the request N times leaves the server in the same state as sending it once. The response may differ (a second DELETE might return 404), but the state does not. Clients and proxies may retry idempotent requests after a timeout without asking.
MethodUse it toSafeIdempotentTypical successFood app example
GETRead a resource or listYesYes200 (304 if unchanged)GET /restaurants/81/menu-items
POSTCreate in a collection, or trigger a processNoNo201 + Location, or 202POST /orders
PUTReplace a resource completely (or create at a known URL)NoYes200 or 204 (201 if created)PUT /users/me/address
PATCHChange some fieldsNoNot guaranteed200 or 204PATCH /menu-items/7
DELETERemove a resourceNoYes204 (404 on a repeat is fine)DELETE /reviews/9013
HEAD / OPTIONSHeaders only / capabilities and CORS preflightYesYes200 / 204Browser preflight before a cross-origin PATCH

The table is easier to remember as a story about retries. Imagine the network drops right after each request reaches the server, and the client sends it again.

Fig. 4

What happens when a retry sends the request twice

What happens when a retry sends the request twiceFive requests are each sent twice. GET leaves state untouched. PUT sets the price to 250 both times. DELETE removes the item once and the second call finds nothing to remove. A JSON Patch that appends to an array appends twice. POST creates two orders.x2x2x2x2x2GET /orders/42read onlyNothing changedsafe and idempotentPUT /menu-items/7{"price": 250}price = 250same end state: idempotentDELETE /menu-items/7second call gets 404Item gone, oncesame end state: idempotentPATCH /menu-items/7[{"op":"add","path":"/tags/-",...}]Tag appended twicedepends on the patchPOST /orders{"items": [...]}Two orders, two chargesnot idempotentRequest (a retry sends it twice)Server state afterwards
Idempotency is about the end state after a retry. GET, PUT and DELETE leave the server where one call would have. A PATCH depends on what it says: "set price to 250" is idempotent, "append a tag" is not. POST is the dangerous one: a blind retry creates a second order and a second charge, which is why it needs the idempotency keys we'll build later.

PUT vs PATCH, and the two PATCH formats

PUT sends the whole new representation; any field you leave out is reset or removed. That's why it's idempotent, and also why it's awkward for big objects edited by different people. PATCH sends only the changes, and there are two standard formats worth naming:

FormatContent-TypeBody looks likeGood atWatch out for
JSON Merge Patch (RFC 7396)application/merge-patch+json{"price": 260, "description": null}Simple field updates; looks like the resourcenull means "delete the field"; arrays are replaced whole
JSON Patch (RFC 6902)application/json-patch+json[{"op": "add", "path": "/tags/-", "value": "vegan"}]Array edits, conditional test ops, atomic multi-step changesMore verbose; add to an array end is not idempotent

In an interview, "PATCH with a merge-patch body" is plenty. If a client sends a Content-Type you don't support, answer 415 Unsupported Media Type.

GET must never change state

Many candidates expose something like GET /orders/42/cancel because it's easy to test in a browser. Link prefetchers, crawlers and caches all assume GET is safe, so one preview of that link can cancel a real order. Anything that changes state is POST, PUT, PATCH or DELETE.

05REST, part 3: where each input belongs

A request has four places to carry data, and putting an input in the wrong one is a small mistake interviewers notice immediately. Here's one request with all four.

Fig. 5

Where each input belongs in an HTTP request

Where each input belongs in an HTTP requestAn annotated POST request. The path identifies the resource, the query string carries optional modifiers, headers carry metadata about the call such as auth and idempotency keys, and the body carries the data being created.Pathwhich resource; required idsQuery stringoptional: filter, sort, pageHeadersabout the call: auth, idempotency,format, versionBodythe data being created or changedPOST /restaurants/81/reviews?notify=false HTTP/1.1Host: api.example.comAuthorization: Bearer eyJhbGciOiJSUzI1NiJ9...Idempotency-Key: 8c2e7a1f-41d0-4f7e-9b1aContent-Type: application/json{"rating": 4, "text": "Great biryani"}
Path says which, query says how, headers say about, body says what. The path holds the identity of the resource. The query string holds optional modifiers. Headers hold metadata about the call itself (who is calling, retry safety, formats, versions). The body holds the data being created or changed.
PlacePut hereFood app examplesDon't put here
PathRequired identifiers of the resource/restaurants/81, /orders/42Filters, options, secrets
Query stringOptional filters, sorting, pagination, field selection?cuisine=thai&sort=-rating&limit=20Tokens or personal data (URLs end up in logs, history and referrers)
HeadersMetadata about the call: auth, idempotency, content type, caching, versionAuthorization, Idempotency-Key, If-Match, AcceptBusiness data the resource owns
BodyThe representation being created or changed; anything large or structured{"items": [...], "deliveryAddress": {...}}Bodies on GET (many proxies drop them)

A handy test for path vs query: if removing the value makes the request mean a different resource, it's path; if it only narrows or reshapes the same collection, it's query. Removing 81 from /restaurants/81 changes what you're talking about. Removing cuisine=thai still returns restaurants, only more of them.

Never take the caller's identity from the request body

POST /orders with {"userId": "u-7", ...} lets anyone order on someone else's account by changing one field. The user id comes from the verified token on the server side. If you need "my" resources, use /users/me/orders or plain /orders scoped to the token. We return to this in the security section, because it is the most common real-world API vulnerability.

06REST, part 4: status codes and response shapes

The status code is the first thing every client, retry library, load balancer and dashboard reads. Getting the family right matters more than getting the exact code right: 2xx means it worked, 4xx means the caller must change something, 5xx means we failed and a retry might help.

Fig. 6

Status codes worth knowing, by family

Status codes worth knowing, by familyFour columns. 2xx success: 200, 201, 202, 204. 3xx redirection: 301, 303, 304. 4xx client errors: 400, 401, 403, 404, 409, 412, 422, 429. 5xx server errors: 500, 502, 503, 504. Notes explain which ones are worth retrying.2xx success3xx redirect4xx client error5xx server error200 OKread or update worked201 Created+ Location of new item202 Acceptedqueued; poll a status204 No Contentworked, empty body301 Movedpermanent new URL303 See OtherGET the result there304 Not Modifiedyour copy is fresh400 Bad Requestmalformed input401 Unauthorizedmissing or bad credentials403 Forbiddenknown caller, not allowed404 Not Foundno such resource409 Conflictclashes with current state412 Precondition FailedIf-Match ETag is stale422 Unprocessablevalid JSON, invalid values429 Too Many Requestsslow down; Retry-After500 Internal Errorbug or unhandled case502 Bad Gatewayupstream answered badly503 Unavailableoverloaded; retry later504 Gateway Timeoutupstream too slowIt workedLook elsewhereFix the requestOur faultShould the client retry?5xx, 429: yes, with backoff and jitter4xx: no, the same request fails againPOST retries need an idempotency keyAnti-pattern200 OK with an errorin the body breakscaches and alerts
Learn about 20 codes, by family. The 4xx column is where precision pays off: 401 vs 403 tells a client whether to log in again or give up, 409 vs 422 tells it whether the conflict is with current state or with the input itself, and 429 tells it to back off. Never return 200 with an error inside the body; it hides failures from caches, retries and alerting.

A few pairs come up again and again in interviews:

  • 401 vs 403. 401 Unauthorized really means unauthenticated: no token, expired token, bad signature. Log in and retry. 403 Forbidden means we know who you are and you still may not do this. Some APIs return 404 instead of 403 for resources the caller can't see, so they don't confirm the resource exists.
  • 400 vs 422. 400 for requests you can't parse (broken JSON, wrong types). 422 Unprocessable Content for well-formed requests that break a rule ("quantity must be between 1 and 20"). Plenty of APIs use 400 for both; pick one convention and keep it.
  • 409 vs 412. 409 Conflict: the request clashes with current state (ordering from a restaurant that closed a minute ago, a duplicate username). 412 Precondition Failed: the client's If-Match condition no longer holds, which we'll use for optimistic locking.
  • 502, 503, 504. Bad gateway (an upstream sent nonsense), unavailable (overloaded or in maintenance; add Retry-After), gateway timeout (an upstream was too slow). All are retryable with backoff.

Headers that belong on responses

Status codes come with companions. 201 Created should carry Location: /orders/42 so the client knows the new resource's URL. 202 Accepted carries a Location for a status resource. 204 No Content has no body at all. 429 and 503 carry Retry-After. Every cacheable GET carries an ETag and a Cache-Control policy.

Envelopes or bare resources?

For a single resource, return the resource itself. For lists, wrap the items, because you need room for pagination metadata and you can never add it later to a bare JSON array without breaking clients.

GET /restaurants/81/reviews?limit=2
{
  "data": [
    { "id": "rv_9013", "rating": 4, "text": "Great biryani", "createdAt": "2026-10-17T09:12:00Z" },
    { "id": "rv_9008", "rating": 5, "text": "Fast delivery", "createdAt": "2026-10-17T08:40:00Z" }
  ],
  "page": { "nextCursor": "eyJ0IjoiMjAyNi0xMC0xN1QwODo0MDowMFoiLCJpZCI6OTAwOH0", "hasMore": true }
}

Content negotiation lets one resource have several representations. The client says what it accepts (Accept: application/json), the server answers with a matching Content-Type, or 406 Not Acceptable if it can't. In practice, most APIs speak JSON only, and that's a fine interview answer.

07Worked example: the food-delivery API

Let's put parts 1 to 4 together. Our functional requirements were: browse restaurants near me, view a menu, place and pay for an order, track it, review the restaurant, and let owners edit their menu. Before reading on, try writing the endpoints yourself, then compare.

Food-delivery public API (v1)
# Discovery
GET    /v1/restaurants?near=12.97,77.59&cuisine=thai&sort=-rating&limit=20   -> 200, paginated
GET    /v1/restaurants/{restaurantId}                                     -> 200
GET    /v1/restaurants/{restaurantId}/menu-items                          -> 200, paginated

# Ordering (customer; user comes from the token)
POST   /v1/orders               Idempotency-Key: <uuid>                   -> 201 + Location
GET    /v1/orders/{orderId}                                               -> 200 (403 if not yours)
GET    /v1/orders?status=active&cursor=...                               -> 200, my orders only
POST   /v1/orders/{orderId}/cancellation                                  -> 201 or 409 if already picked up
GET    /v1/orders/{orderId}/tracking     (SSE stream of courier location) -> 200 text/event-stream

# Reviews
POST   /v1/restaurants/{restaurantId}/reviews                             -> 201 + Location
GET    /v1/restaurants/{restaurantId}/reviews?limit=20&cursor=...          -> 200, paginated
DELETE /v1/reviews/{reviewId}                                             -> 204

# Restaurant owners
PATCH  /v1/menu-items/{menuItemId}       If-Match: "v5"                    -> 200 or 412

# Reports
POST   /v1/exports                                                        -> 202 + Location: /v1/jobs/{jobId}
GET    /v1/jobs/{jobId}                                                   -> 200 or 303 to the result

Notice a few choices. The order body contains restaurantId, the items and the delivery address, but never the user id. List endpoints all paginate. The two writes that must not happen twice (placing an order, and implicitly paying for it) take an idempotency key. The owner's menu edit is protected against concurrent edits. And the one slow operation, the export, returns immediately with 202. We'll build each of those in its own section.

Where payment lives

You could expose POST /orders/{id}/payments as its own resource, and real systems often do, because payments have their own lifecycle (authorised, captured, refunded). In a 45-minute interview, I'd fold payment into order creation (the order body carries a payment-method token from the client-side payment SDK) and mention the split as a refinement. Never put raw card numbers in your API; the client tokenises them with the payment provider first.

08GraphQL: one endpoint, client-shaped responses

GraphQL exists to fix two REST pains that show up when many clients share one API. Over-fetching: the mobile home screen needs a restaurant's name, rating and five dishes, but GET /restaurants/81 returns 40 fields and GET /menu-items returns all 120 items. Under-fetching: the screen needs data from three resources, so it makes three sequential calls, each paying a full mobile round trip.

With GraphQL, the server publishes a typed schema, and the client sends one query naming exactly what it wants:

Schema (excerpt) and the home-screen query
type Restaurant {
  id: ID!
  name: String!
  rating: Float
  menuItems(first: Int = 20, after: String): MenuItemConnection!
  reviews(first: Int = 10, after: String): ReviewConnection!
}

type Query {
  restaurant(id: ID!): Restaurant
}

type Mutation {
  placeOrder(input: PlaceOrderInput!, idempotencyKey: String!): Order!
}

# The client sends this as POST /graphql
query HomeScreen {
  restaurant(id: "81") {
    name
    rating
    menuItems(first: 5) { edges { node { name price } } }
    reviews(first: 3)   { edges { node { rating text } } }
  }
}

On the server, every field has a resolver: a function that knows how to fetch that field for its parent object. The engine walks the query, calls the resolvers it needs (in parallel where it can), and assembles a JSON response with exactly the requested shape. Reads are queries, writes are mutations, and server push is a subscription, usually over a WebSocket.

Fig. 7

Three REST round trips vs one GraphQL query

Three REST round trips vs one GraphQL queryLeft: a mobile home screen calls three REST endpoints in turn and each returns more fields than it needs. Right: the same screen sends one GraphQL query to a single endpoint, and the server runs three resolvers and returns only the requested fields.REST: one endpoint per resourceGraphQL: client picks the fieldsPOST /graphqlMobile home screenneeds name, 5 dishes, 3 reviewsGET /restaurants/81~40 fields back; screen uses 31GET /restaurants/81/menu-items120 items back; screen shows 52GET /restaurants/81/reviewsa third round trip3Mobile home screenone query names the exact fieldsGraphQL serverparse, validate, executerestaurantrestaurant DBmenuItemsmenu servicereviewsreview storeone resolver per field, run in parallel3 sequential round trips on a mobile network1 round trip, only the fields asked for
Same screen, one round trip instead of three. With REST the phone pays three sequential round trips and throws most of each response away. With GraphQL it sends one query; the server fans out to the restaurant, menu and review resolvers and returns only the fields the screen named.

The costs you must be ready to talk about

The flexibility moves work and risk to the server. Interviewers who mention GraphQL usually probe at least one of these.

  • The N+1 problem. A query for 20 restaurants and each one's owner runs one query for the list, then the owner resolver runs once per restaurant: 21 database calls. The fix is a DataLoader: resolvers ask the loader for owner ids, it waits until the end of the current tick, then loads all of them in one IN (...) query, and caches results for the rest of the request.
  • Caching. Every query is a POST to the same URL, so CDNs and browser caches can't help by default. Teams use persisted queries (the client sends a hash of a pre-registered query, which can be a cacheable GET) plus caching inside the resolvers or a normalised client-side cache.
  • Abuse and cost. One query can ask for restaurants, their reviews, each reviewer's orders, each order's restaurant... Limit query depth, compute a cost score per query and reject expensive ones, and rate limit by cost rather than by request count.
  • Errors. GraphQL commonly returns HTTP 200 with an errors array next to partial data. Monitoring that only watches status codes will miss failures.
  • Authorization per field. Because any field can appear in any query, access checks must live in the resolvers (or the business layer they call), not at the endpoint.
Fig. 8

The N+1 problem and the DataLoader fix

The N+1 problem and the DataLoader fixTop: listing 20 restaurants with their owners runs one query for the list and then one query per restaurant for its owner, 21 queries. Bottom: a DataLoader collects the 20 owner ids and loads them in one IN query, 2 queries in total.Naive resolversWith a DataLoaderper rowbatch 20 idsrestaurants(first: 20)1 query for the list1SELECT * FROM users WHERE id = ?owner of one restaurantrestaurants(first: 20)1 query for the list1SELECT * FROM users WHERE id IN (...)all 20 owners at once221 queries2 queries
Batching turns 21 queries into 2. Naive resolvers fetch each restaurant's owner separately. A DataLoader collects the 20 ids requested during one execution step and issues a single IN query, which is the standard first answer to "what's the N+1 problem in GraphQL?".

When I'd choose it in an interview: several client types (web dashboard, two mobile apps, a partner widget) with genuinely different data needs, a product team that ships screens faster than a backend team can add endpoints, or an aggregation layer over many microservices. When I'd avoid it: a simple CRUD product, public APIs where HTTP caching matters, or file uploads and downloads.

09RPC and gRPC: fast, typed calls between services

RPC lets one service call a function on another as if it were local. Instead of "create an order resource", you think "call Pricing.Quote(orderId)". gRPC is the dominant modern implementation, and it brings three things.

  • A contract first. You write a .proto file describing services and messages. The protoc compiler generates client stubs and server base classes in Java, Go, Python, C++ and more, so both sides share one source of truth and type errors show up at compile time.
  • Compact binary messages. Protocol Buffers encode fields by number, not by name, so a message is typically several times smaller than the same JSON and much cheaper to parse.
  • HTTP/2. One long-lived connection carries many concurrent calls (multiplexed streams), headers are compressed, and calls can stream in either direction.
Fig. 9

From a .proto file to a call over HTTP/2

From a .proto file to a call over HTTP/2A .proto file defines the Pricing service and its messages. The protoc compiler generates a client stub and a server base class. The order service calls the stub, which sends a binary protobuf message over a long-lived HTTP/2 connection to the pricing service. gRPC supports four call shapes: unary, server streaming, client streaming and bidirectional.protocstubbase classQuoteReqQuoteResGenerated codeclient stub + server basefor Java, Go, Python...Order servicestub.quote(req)HTTP/2 connectionbinary protobuf frames, manystreams on one TCP connectionPricing serviceQuote() implpricing.protoservice Pricing { rpc Quote(QuoteReq) returns (QuoteRes);}message QuoteReq { string order_id = 1;}On the wirefield numbers, not namesrename a field: safereuse a number: breaksFour call shapesUnary: 1 in, 1 outServer stream: 1 in, many outClient stream: many in, 1 outBidirectional: many both ways
The .proto file is the API. Code generation gives the order service a typed stub and the pricing service a base class to implement; on the wire, only field numbers and binary values travel over a shared HTTP/2 connection. Because names never travel, renaming a field is safe, but reusing a field number corrupts old clients.
pricing.proto
syntax = "proto3";

service Pricing {
  rpc Quote(QuoteRequest) returns (QuoteResponse);
  rpc WatchSurge(SurgeRequest) returns (stream SurgeUpdate);  // server streaming
}

message QuoteRequest {
  string order_id = 1;
  string city = 2;
  reserved 3;                  // removed field: never reuse the number
}

message QuoteResponse {
  int64 subtotal_paise = 1;    // money as integer minor units, never float
  int64 delivery_fee_paise = 2;
  int64 total_paise = 3;
}

gRPC has its own status codes (NOT_FOUND, INVALID_ARGUMENT, UNAVAILABLE, DEADLINE_EXCEEDED...) and a feature worth mentioning in any interview about microservices: deadlines. The caller says "I'll wait 300 ms", that budget propagates down the call chain, and every service stops work the moment the original caller has given up.

When I'd choose it: internal, high-volume service-to-service calls, polyglot teams that want generated clients, and streaming between services. When I'd avoid it: public APIs for third parties (they expect REST and JSON), and direct browser clients, which can't speak native gRPC and need a gRPC-Web proxy. The common production shape is the one in Fig. 1: REST at the edge, gRPC inside.

How I'd say it

"Clients talk REST to the gateway. Behind it, the order service calls pricing and inventory over gRPC because those calls happen on every checkout, at a few thousand per second, and we want typed contracts and deadlines between teams."

10Design principles that keep an API usable

Protocols aside, the APIs people enjoy using share a handful of habits. These are the ones I listen for.

  • Consistency beats cleverness. One naming style for paths (kebab-case), one for JSON fields (camelCase or snake_case, never both), one pagination scheme, one error format, one timestamp format (ISO 8601 in UTC). A client that has learned one endpoint should be able to guess the next.
  • Least surprise. GET never changes anything, DELETE deletes, a 2xx means success. Money is integer minor units (paise, cents) with a currency code, never a float. Ids are opaque strings, so you can change their format later.
  • Design for the client, not the tables. Avoid chatty APIs where one screen needs ten calls, and avoid mirroring storage. Aggregate where it's natural, for example an order that includes its line items.
  • Backwards compatibility by default. Adding an optional field or a new endpoint is safe. Removing or renaming a field, changing a type, tightening validation or changing a default is breaking. Clients must ignore unknown fields; that rule alone makes most evolution painless.
  • Predictable limits. Every list is paginated with a capped page size, every request has a maximum body size, every caller has a rate limit, and all of it is documented.
  • Self-describing where it's cheap. Returning links such as Location, next page URLs and status-resource URLs is the useful part of HATEOAS (hypermedia as the engine of application state). Full hypermedia-driven clients are rare; don't spend interview time on them.

One more for multi-tenant products (say, the restaurant-owner dashboard used by thousands of restaurant chains): decide where the tenant comes from. A subdomain (chainA.api.example.com), a path segment (/tenants/{id}/...) or a header all work, but the safest source is the authenticated token, so a caller can never reach another tenant by editing a URL.

Members-only article

Keep reading with the Article Pass

You've read the free part. The other 11 sections of this guide, with every diagram and worked example, are for members. One Article Pass unlocks every members-only article on Engineering Atlas: the ones live today and every one I publish while your pass is active.

  • Pagination in depth
  • Filtering, sorting, field selection and search
  • Idempotency keys: making POST safe to retry
  • Concurrency control with ETags
  • Long-running operations: 202 Accepted
  • Error responses clients can act on
  • Versioning and evolving an API
  • Security: authentication, authorization and trust
  • Rate limiting and throttling
  • The 5-minute API step: checklist and whiteboard
  • Interview questions

From ₹49 · one-time payment, no auto-renewal · separate from course passes