API styles
0-day streakReview

Your progress

Level 1

0 XP100 XP to level 2
0day streak
0best streak
0topics started
0cards reviewed
    Review cards

    Progress is stored only in this browser. No account is required.

    Layer 4 · Full breakdown · 20 min · 5 figures

    REST vs GraphQL vs gRPC

    How REST, GraphQL and gRPC really work, what each costs you in round trips, caching, errors and tooling, and how to choose one for public, app-facing and internal APIs in a design interview.

    REST, GraphQL and gRPC are three ways for one program to ask another for data over a network. REST exposes resources at URLs and leans on HTTP’s methods, status codes and caching. GraphQL exposes one typed graph at a single endpoint and lets the client name the fields it wants. gRPC exposes typed procedures defined in Protocol Buffers, called over HTTP/2 with binary messages and streaming. None wins everywhere, so large systems use all three: REST at the public edge, GraphQL or a backend-for-frontend for apps, gRPC between internal services.

    Picture it first: a food court, a waiter and a kitchen ticket

    Picture a food court with separate counters for dosas, biryani and desserts.

    The counters are REST. Anyone can order “item 7” from any counter’s menu board, no training needed, and popular items sit pre-packed at the front. But a meal from three counters means three queues, and each counter hands over its full combo even when you only wanted the chutney.

    The waiter is GraphQL. You write everything you want, from every counter, on one slip; the waiter walks the kitchen and brings back one tray holding exactly that. But nothing can be pre-packed, because every slip differs, and a prankster can write “every dish, twice”, so slips need a size rule.

    The kitchen ticket is gRPC. Cooks don’t shout dish names; they pass KOTs (kitchen order tickets) with numbered codes from a shared code book, each stamped “out by 8:42”. When 8:42 passes, every station drops that ticket. Fast and unambiguous, but customers can’t read KOTs.

    Map it back: counters are resources, the menu board is an OpenAPI document, pre-packed items are HTTP caches and CDNs. The waiter is a GraphQL server running resolvers, the slip is a query, the slip-size rule is cost limiting. The KOT is a protobuf message, the code book is the .proto file, the stamp is a gRPC deadline, and unreadable KOTs are why browsers need gRPC-Web.

    REST: resources over HTTP’s uniform interface

    REST comes from Roy Fielding’s 2000 doctoral dissertation, which described the Web’s architecture as a set of constraints, each adopted for a property it gives the system:

    • Client-server. The user interface and data storage evolve independently.
    • Stateless. Every request carries everything needed to understand it. Any server can take any request, which makes scaling and recovery easy; the price is resending context, such as tokens, on every call.
    • Cacheable. Responses are labelled cacheable or not, so clients and intermediaries can reuse them, at the risk of staleness.
    • Uniform interface. Resources named by URLs, changed by sending representations (usually JSON), self-describing messages (method, media type, cache headers), and links in responses that drive the client’s next step.
    • Layered system. A client can’t tell the origin from a proxy, gateway or CDN, so layers can be added freely, each costing a little latency.
    • Code on demand (optional). The server may send scripts to extend the client.

    Fielding was explicit about the price of the uniform interface: data moves in a standard form rather than one shaped for each application, so it is less efficient for any particular client. That is exactly the over-fetching problem GraphQL was built to remove.

    What “RESTful” means in practice

    In daily use, “REST API” means something looser: nouns in URLs, HTTP methods as verbs, JSON bodies, meaningful status codes, no server session. For orders:

    GET    /v1/orders/9876                    → 200 OK with the order
    GET    /v1/orders?customer=c_51&status=ACTIVE
    POST   /v1/orders                         → 201 Created, Location: /v1/orders/9877
    PATCH  /v1/orders/9876                    → 200 OK (change the delivery note)
    DELETE /v1/orders/9876                    → 204 No Content (cancel)

    The methods carry meanings every proxy and library understands. RFC 9110 defines GET, HEAD, OPTIONS and TRACE as safe (read-only), and PUT, DELETE and the safe methods as idempotent (sending twice equals sending once). So a gateway may retry a timed-out GET on its own, but a POST needs an idempotency key.

    The contract is usually an OpenAPI document, a language-agnostic description of paths, parameters, schemas and errors that tools turn into docs, client SDKs, server stubs and tests. REST doesn’t require one; without it, the contract lives in a wiki and drifts.

    What you gain: HTTP’s machinery for free. A GET with Cache-Control: max-age=60 and an ETag can be served by the browser, a proxy or a CDN without touching your servers. curl is a debugger, and every partner already knows the model.

    What you pay: round trips and shape. The server shapes every response, so clients over-fetch (the whole restaurant when the screen shows its name) and under-fetch (the order holds only restaurant_id, so another call follows). Nested views become sequential round trips, painful on mobile; endpoints multiply per client; and breaking changes need versions such as /v1 and /v2, run side by side for years.

    GitHub hit exactly this. When it added a GraphQL API in 2016, its REST API caused over 60% of the requests reaching its database tier, a complete view could take two or three calls, and responses sent too much data yet missed what clients needed.

    GraphQL: one endpoint, client-shaped queries

    GraphQL, built at Facebook and open-sourced in 2015, is a query language plus a runtime. The server publishes a schema, a typed description of every object, field and relationship. Clients send a document naming the fields they want, usually as a POST to one URL such as /graphql, and the response mirrors the query’s shape.

    type Order {
      id: ID!
      status: OrderStatus!
      etaMinutes: Int
      items: [OrderItem!]!
      restaurant: Restaurant!
      deliveryPartner: DeliveryPartner   # null until a rider is assigned
    }
    type OrderItem { name: String!  quantity: Int! }
    type Restaurant { id: ID!  name: String!  area: String!  rating: Float }
    type DeliveryPartner { id: ID!  firstName: String!  maskedPhone: String! }
    enum OrderStatus { PLACED PREPARING OUT_FOR_DELIVERY DELIVERED CANCELLED }
    
    type Query        { order(id: ID!): Order }
    type Mutation     { cancelOrder(id: ID!, reason: String): Order }
    type Subscription { orderStatusChanged(id: ID!): Order }

    The specification defines three operations: a query is a read-only fetch; a mutation is a write followed by a fetch, so cancelOrder returns the updated order in the same round trip; a subscription is a long-lived request that pushes data as events happen, usually over WebSockets or server-sent events.

    Resolvers do the real work

    Every field has a resolver, a function that produces its value. Executing a query walks the tree: Query.order calls the Orders service, Order.restaurant calls Restaurants, Order.deliveryPartner calls Delivery. The schema is the contract, and resolvers map it onto the services behind it.

    Errors arrive inside a 200

    A response holds data and, if anything failed, an errors list. Failures are often partial: if Delivery is down, deliveryPartner comes back null with an error whose path names that field, and the rest of the screen renders. That is why fields are nullable by default.

    HTTP has no status for “partly worked”, so the GraphQL-over-HTTP guidance says to answer 2xx whenever data is present; only requests that fail before execution, such as validation errors, get a 4xx. GitHub returns 200 even when you exceed its primary rate limit.

    Evolving without versions

    Clients receive only the fields they name, so adding fields breaks nobody. To remove one, mark it @deprecated(reason: "use etaWindow"), wait until no operation asks for it, then delete it; the GraphQL project recommends this versionless evolution. The price: tracking field usage per client, and old app builds keep deprecated fields alive.

    A 200 doesn’t mean it worked

    GraphQL and gRPC both break alerting on HTTP 5xx: a GraphQL error usually rides inside a 200, and a gRPC error is a grpc-status trailer on an HTTP 200 stream. Count errors from the errors list per operation and from gRPC status codes per method, or your dashboard stays green through an outage.

    GraphQL in production: N+1, cost limits and caching

    The N+1 problem and DataLoader

    Resolvers are simple because each handles one field of one object, and that is the trap. Take an order-history screen:

    query History {
      me {
        orders(first: 20) { id status restaurant { name } }
      }
    }

    orders makes one call and returns 20 orders; then restaurant resolves once per order, often for the same few restaurants. That is 1 + 20 = 21 calls for one screen, and every nested list multiplies it. The SQL version of this disease is the N+1 query problem.

    The standard fix is batching, popularised by Facebook’s DataLoader. Resolvers call restaurantLoader.load(42) instead of the service. The loader waits for the current tick of the event loop to end, gathers every key requested in it, drops duplicates and calls your batch function once, which must return values in key order. Twenty orders from 12 restaurants become 1 + 1 = 2 calls.

    An order-history query for 20 orders. Naive resolvers call the Restaurants service once per order; DataLoader gathers the 20 loads made in one tick, removes duplicates and sends one batch.Notice The query text is identical on both sides. Only the data-loading layer changed, and 21 calls became 2.

    The loader also memoises within a request, so restaurant 42 is fetched once however often it is asked for.

    One loader per request

    Create loaders per request. Their cache is a per-request memo, not a shared cache like Redis; a loader shared across requests can leak one user’s data into another’s response.

    Depth, breadth and cost limits

    A REST endpoint does bounded work; a GraphQL query does whatever the client writes. So servers cap depth and breadth (fields, aliases, batched operations), require pagination arguments on lists, and estimate cost before running a query.

    GitHub documents its limits in detail:

    • Every connection needs first or last between 1 and 100, and one call may request at most 500,000 nodes.
    • A query’s point cost is the number of backend requests needed to fill every connection, assuming full lists, divided by 100 and rounded (minimum 1).
    • Users get 5,000 points per hour; a secondary limit allows 2,000 points per minute, and requests running over 10 seconds are cut off.

    Apply them to a dashboard asking for restaurants(first: 100) { orders(first: 100) { items(first: 50) { name } } }:

    • Requests: 1 + 100 (orders per restaurant) + 100 × 100 (items per order) = 10,101, so about 101 points.
    • Nodes: 100 + 10,000 + (100 × 100 × 50 = 500,000) = 510,100, over the cap, so it is rejected before running. With items(first: 40) it fits: 100 + 10,000 + 400,000 = 410,100.

    A request counter would call this “one request”, which is why rate limiting GraphQL is done in points.

    Caching is harder, not impossible

    GraphQL reads are usually POSTs to one URL, and RFC 9110 notes that most HTTP caches store only GET and HEAD responses, so a CDN has nothing to reuse. Teams win caching back in three layers:

    1. Client cache. Clients normalise responses by type and id (Restaurant:42), so the next screen showing restaurant 42 renders from memory.
    2. Persisted queries. The client sends the query’s SHA-256 hash instead of its text. With Apollo’s automatic persisted queries, an unknown hash returns PERSISTED_QUERY_NOT_FOUND and the client resends once with the full text. A hash fits in a URL, so queries can go out as GET and a CDN can cache them.
    3. Server-side caches behind resolvers, as with any API.

    A stricter variant, trusted documents, runs only operations registered at build time. It suits first-party apps, not public APIs.

    gRPC: typed procedures over HTTP/2

    gRPC is a remote procedure call framework that Google open-sourced (1.0 shipped in August 2016). You describe services and messages once in a .proto file, and the compiler generates client stubs and server interfaces for each language, so a remote call looks like a method call on a local, typed object.

    syntax = "proto3";
    package delivery.v1;
    
    service OrderService {
      rpc GetOrder(GetOrderRequest) returns (Order);                            // unary
    }
    service TrackingService {
      rpc TrackPartner(TrackRequest) returns (stream Location);                 // server streaming
      rpc UploadTrail(stream GpsPoint) returns (TrailSummary);                  // client streaming
      rpc DispatchChannel(stream DispatchEvent) returns (stream DispatchEvent); // bidirectional
    }
    
    message GetOrderRequest { int64 order_id = 1; }
    message Order {
      int64 order_id = 1;
      OrderStatus status = 2;
      int64 total_paise = 3;
      int64 restaurant_id = 4;
      repeated OrderItem items = 5;
    }
    // OrderStatus, OrderItem, Location and the other messages are omitted

    What travels on the wire

    Each call is an HTTP/2 request to a path such as /delivery.v1.OrderService/GetOrder, with content-type: application/grpc+proto and, if a deadline is set, a grpc-timeout header such as 750m (750 ms). Each message gets a 5-byte prefix (compression flag plus 4-byte length). The outcome is a grpc-status code in HTTP/2 trailers after the last message, so a stream can deliver 1,000 messages and still end in an error (see the protocol description).

    Protobuf is compact because its wire format carries field numbers instead of names and writes integers as “varints”, where small numbers take fewer bytes. Encode order_id = 9876, status = OUT_FOR_DELIVERY (enum value 4) and total_paise = 48900:

    • JSON: {"order_id":9876,"status":"OUT_FOR_DELIVERY","total_paise":48900} is 65 bytes.
    • Protobuf: a 1-byte tag plus 2-byte varint (3 bytes), a tag plus 1 byte (2), a tag plus 3-byte varint (4): 9 bytes, about 7× smaller, and parsing reads numbers instead of scanning text.

    The gap shrinks for string-heavy messages and gzipped JSON. The price is opacity: a captured payload is unreadable without its .proto file. Field-numbering rules live in serialization and schema evolution.

    Four call types

    HTTP/2 gives each call its own stream, so gRPC offers four shapes, preserving message order within each call.

    gRPC’s four call types, with a delivery-app example for each. Requests run left to right; responses run right to left.Notice Only unary looks like REST. The three streaming shapes need a long-lived HTTP/2 stream, which plain request-response APIs don’t have.

    In the delivery app, TrackPartner streams the rider’s location every few seconds from one request, UploadTrail sends a batch of GPS points for one summary, and DispatchChannel offers orders to a rider’s device and hears accepts and declines on the same call.

    Deadlines, cancellation and status codes

    gRPC has a real error vocabulary: 17 status codes, from OK (0) through INVALID_ARGUMENT, NOT_FOUND and RESOURCE_EXHAUSTED (quota ran out) to UNAVAILABLE, a transient failure worth retrying with backoff, though not blindly for non-idempotent calls.

    A deadline is the moment the caller stops waiting. gRPC sets none by default, so always set one. When it passes, the client gets DEADLINE_EXCEEDED and the server’s side is cancelled. Java and Go forward an incoming deadline to outgoing calls automatically (C++ needs it switched on). Two sharp edges: cancellation doesn’t roll back work already done, and DEADLINE_EXCEEDED can arrive after the server completed a write, so writes must be idempotent.

    Where gRPC hurts

    • Browsers. No browser API exposes raw HTTP/2 frames or trailers, so pages can’t speak native gRPC. gRPC-Web adds a translating proxy (commonly Envoy), and its official client supports unary and server-streaming calls only.
    • Load balancing. A client multiplexes many calls over one long-lived HTTP/2 connection, so a layer-4 balancer, which spreads connections, pins all of them to one backend. Balance per call at layer 7 or in the client (see load balancing: L4 vs L7).
    • Reach. Outside partners expect HTTP and JSON they can read.

    Dropbox’s migration shows why teams accept those costs internally: hundreds of services in several languages exchanging millions of requests per second. It chose gRPC mainly to keep its existing protobuf messages, liked HTTP/2 multiplexing and bidirectional streaming, and lets most services call each other directly, with no proxy, to keep latency down.

    One screen, three ways

    The worked example: a food delivery app’s order screen shows status and ETA, the items, the restaurant’s name and area, and the rider’s first name and masked phone. The data lives in three services: Orders, Restaurants and Delivery. Times are illustrative: about 100 ms per round trip on 4G, about 1 ms per data-centre hop.

    The same order screen fetched three ways. Plain REST makes the phone pay for two sequential round trips; GraphQL and a BFF move the fan-out into the data centre, where hops are cheap.Notice The expensive hops are left of the data-centre boundary. Each style decides how many of them the phone pays for.

    REST: two trips, or a custom endpoint

    GET /v1/orders/9876
    → 200 OK
    { "id": 9876, "status": "OUT_FOR_DELIVERY", "eta_minutes": 12,
      "restaurant_id": 42, "partner_id": 77,
      "items": [{ "name": "Masala dosa", "qty": 2 }, { "name": "Filter coffee", "qty": 1 }],
      "payment": { … }, "address": { … }, "invoice_url": "…", … }
    
    # only now do we know which restaurant and rider to ask for; these two run in parallel
    GET /v1/restaurants/42          → name and area, plus hours, cuisines, ratings, licence …
    GET /v1/delivery-partners/77    → name and phone, plus vehicle, shift, payout details …

    The second pair needs ids from the first, so the phone waits about 2 × 100 ms = 200 ms in network time alone; HTTP/2 can run the last two in parallel but can’t remove the dependency. Each reply is a whole resource, mostly unused here. The usual REST fixes are ?expand=restaurant,partner, sparse fieldsets (?fields=name,area), or a screen endpoint such as GET /v1/order-screens/9876, which is really a BFF.

    GraphQL: one query

    query OrderScreen($id: ID!) {
      order(id: $id) {
        status
        etaMinutes
        items { name quantity }
        restaurant { name area }
        deliveryPartner { firstName maskedPhone }
      }
    }
    {
      "data": {
        "order": {
          "status": "OUT_FOR_DELIVERY",
          "etaMinutes": 12,
          "items": [{ "name": "Masala dosa", "quantity": 2 }, { "name": "Filter coffee", "quantity": 1 }],
          "restaurant": { "name": "Udupi Corner", "area": "Indiranagar" },
          "deliveryPartner": { "firstName": "Ravi", "maskedPhone": "+91 98xxxxxx21" }
        }
      }
    }

    One round trip, about 100 ms plus a few milliseconds of fan-out inside the data centre, carrying exactly the eight leaf fields the screen renders. Showing the restaurant’s rating later is a change to the app’s query, with no server deploy.

    gRPC behind a BFF

    The phone doesn’t speak gRPC. A backend-for-frontend owned by the app team exposes one JSON route per screen and makes typed gRPC calls inside:

    func (s *BFF) OrderScreen(ctx context.Context, orderID int64) (*Screen, error) {
    	ctx, cancel := context.WithTimeout(ctx, 800*time.Millisecond) // budget for the whole screen
    	defer cancel()
    
    	order, err := s.orders.GetOrder(ctx, &orderpb.GetOrderRequest{OrderId: orderID})
    	if err != nil {
    		return nil, err // codes.NotFound becomes HTTP 404 for the app
    	}
    	var rest *restpb.Restaurant
    	var rider *deliverypb.Assignment
    	g, gctx := errgroup.WithContext(ctx) // the two lookups run in parallel
    	g.Go(func() (err error) {
    		rest, err = s.restaurants.GetRestaurant(gctx, &restpb.GetRestaurantRequest{Id: order.RestaurantId})
    		return err
    	})
    	g.Go(func() (err error) {
    		rider, err = s.delivery.GetAssignment(gctx, &deliverypb.GetAssignmentRequest{OrderId: orderID})
    		return err
    	})
    	if err := g.Wait(); err != nil {
    		return nil, err
    	}
    	return buildScreen(order, rest, rider), nil
    }

    The 800 ms budget on ctx travels with every gRPC call as grpc-timeout. A production BFF would degrade, not fail, when only Delivery is down. The three approaches for this screen:

    Plain REST GraphQL BFF + gRPC
    Phone round trips 2 sequential (3 requests) 1 1
    Who shapes the response Each resource’s owner The client, per query The app team, per screen
    Unused fields sent Most of each resource None None
    Cacheable at a CDN Yes, per resource URL Only as persisted GET queries Yes, if the screen route is a GET

    Side by side

    REST GraphQL gRPC
    Core idea Resources (nouns) at URLs, HTTP methods as verbs One typed graph; the client selects fields Typed procedures (verbs) on services
    Transport Any HTTP version HTTP, usually POST to one endpoint; WebSockets or SSE for subscriptions HTTP/2 only; browsers need gRPC-Web and a proxy
    Payload Usually JSON; any media type JSON Protobuf binary (JSON mapping exists)
    Contract Optional: OpenAPI Required: the schema, which clients can introspect Required: .proto files, with generated code
    Response shape Server decides, per endpoint Client decides, per query Server decides, per method
    HTTP caching Native: GET, ETag, Cache-Control, CDN Hard: needs persisted queries sent as GET; clients cache by id None at the HTTP layer; cache inside services
    Streaming Not built in; add SSE or WebSockets Subscriptions Native: server, client and bidirectional
    Errors HTTP status codes 200 with an errors list and partial data 17 status codes in trailers
    Versioning /v1, /v2 or a header Add fields, deprecate old ones, no versions Add fields with new numbers; a new package for breaking changes
    Debugging curl, browser, readable logs Explorers built on introspection Needs the .proto file or server reflection
    Best fit Public and partner APIs, simple CRUD, cacheable reads Product UIs with many screens and several clients Internal service-to-service calls, low latency, streaming
    REST, GraphQL and gRPC compared, as each is most commonly deployed.

    Choosing a style

    Start from who calls the API, not from fashion.

    • Public and partner APIs: REST with OpenAPI. Partners use any language, sit behind proxies you don’t control, expect curl to work, benefit from HTTP caching, and upgrade on their own schedule, so explicit versions suit them.
    • Product UIs with many screens and clients: GraphQL or a BFF. When Android, iOS and web each want different slices of the same data and ship weekly, client-shaped queries remove most “please add this field” tickets. Large organisations stitch many teams’ schemas into one graph with federation (see API composition, BFF and GraphQL federation); a small team with five screens may be happier with a REST BFF.
    • Internal service-to-service calls: gRPC. Many languages, high call rates, strict contracts, streaming and deadlines. The more services you run (see monolith vs microservices), the more typed contracts pay off.
    • Telling someone something happened: neither. Use events and webhooks (see “Going deeper”).
    Where each style usually sits in a delivery platform: REST for partners, GraphQL or a BFF for the apps, gRPC inside, and webhooks pushing events back out.Notice The app edge and the partner edge both turn into the same gRPC calls one hop in. The styles split by audience, not by service.

    In the interview

    Name the boundary, not your favourite tool: “Partners get versioned REST with OpenAPI. Apps go through a GraphQL layer, one round trip per screen. Behind it, services talk gRPC with deadlines, and partners hear about events through webhooks.”

    Going deeper

    GraphQL’s attack surface

    A GraphQL endpoint runs small programs, not fixed requests, which gives attackers more to work with.

    • Introspection. Anyone can ask the schema to describe itself: a map of every type and field. First-party APIs often disable it in production, but the GraphQL security guide calls that obscurity, not protection, since error suggestions (“did you mean …?”) leak names too. Real defences are trusted documents and field-level authorisation inside resolvers.
    • Deep and cyclic queries. If orders link to restaurants and restaurants to orders, a client can nest order → restaurant → orders → restaurant → … until the server falls over. Cap depth, and list nesting tighter.
    • Aliases. One HTTP request can ask for the same expensive field a thousand times under different names:
    query {
      a1: order(id: "9001") { restaurant { name } }
      a2: order(id: "9002") { restaurant { name } }
      # … a3 to a1000
    }

    A request counter sees one request; the server does 1,000 lookups. Aimed at an OTP mutation, it becomes 1,000 guesses per request. Limit aliases and batched operations, and charge by cost.

    Deadlines across hops

    With deadline propagation, the first caller sets one budget, every hop forwards the time left, and when it runs out every hop stops together.

    The BFF gives the order screen an 800 ms budget. Each hop forwards the time left; when it runs out, the whole chain stops together.Notice Pricing, two hops from the BFF, learns the deadline from the grpc-timeout header alone. Without propagation it would keep working on a request nobody is waiting for.

    The arithmetic is subtraction. The BFF spends 50 ms, so Orders receives 800 − 50 = 750 ms; Orders spends 150 ms, so Pricing receives 750 − 150 = 600 ms. Sending time left rather than a wall-clock time sidesteps clock skew.

    Dropbox reported that forcing engineers to declare deadlines in service definitions fixed “whole classes” of reliability problems; it even writes the deadline into SQL queries as a comment so a proxy can kill queries nobody is waiting for. Two rules: set the budget from the user’s patience at the edge and let it shrink, instead of giving each hop its own generous timeout; and fit retries inside what remains, or one slow dependency becomes a retry storm. More in timeouts, retries and backoff.

    HATEOAS: the constraint almost nobody follows

    Hypermedia as the engine of application state says a client needs only an entry URL and its media types; every next step comes from links in responses. In a 2008 post Fielding said bluntly that an HTTP API not driven by hypertext is RPC, not REST.

    By that standard, almost every “REST API” is RPC with nice URLs, which is usually fine: real clients are coded against documented URLs. GitHub, which did build hypermedia into its REST API, found the *_url hints bloated responses, and following links meant repeated round trips. What survives is small: a Location header after a create, and next-page links for pagination.

    The fourth shape: webhooks and events

    All three styles are request-response: the client asks. Integrations often need the opposite, “tell me when the order is delivered”, and polling wastes calls; GitHub’s rate-limit guide tells integrators to use webhooks instead. The answers:

    • Webhooks for partners: POST a signed event to a URL they registered, and retry on failure (webhooks).
    • Events on a log or queue between your services, so producers don’t wait for consumers (event-driven architecture).
    • Live connections for users: GraphQL subscriptions, gRPC server streams, or SSE and WebSockets.

    Cheat sheet

    Topic Remember it as
    REST Resources at URLs plus HTTP’s methods, status codes and caching; contract in OpenAPI
    Fielding’s constraints Client-server, stateless, cacheable, uniform interface, layered, optional code on demand
    REST’s cost Server-shaped responses: over-fetching, under-fetching, sequential round trips, /v2
    GraphQL One endpoint, typed schema, client-shaped queries; query, mutation, subscription
    GraphQL errors 200 with data plus an errors list; failed fields become null
    N+1 fix DataLoader: collect keys for one tick, dedupe, one batch call; one loader per request
    GraphQL limits Depth, aliases, required pagination, cost points (GitHub: 5,000 points/hour, 500,000 nodes)
    GraphQL caching POST defeats CDNs; persisted query hashes sent as GET bring them back
    gRPC Protobuf contract, generated stubs, HTTP/2, binary frames, status in trailers
    Deadlines No default; set at the edge, forwarded as time left, every hop cancels together
    gRPC caveats Browsers need gRPC-Web plus a proxy; balance per call at L7, not per connection
    Default picks Partners: REST. Apps: GraphQL or a BFF. Internal: gRPC. Push: webhooks and events

    How to say it in the interview

    “I choose per audience. Public and partner APIs are REST with OpenAPI: every client speaks HTTP, GETs cache at the CDN, and explicit versions suit outside developers. Our apps talk to a GraphQL layer, so each screen is one round trip with only the fields it shows; resolvers batch with DataLoader and queries are charged by cost. Behind that, services use gRPC: protobuf contracts, generated clients, streaming where needed, and an edge deadline every hop forwards. Partners hear about changes through webhooks. Three styles in one system is normal.”

    Why not use GraphQL for everything, including service-to-service calls? Internally the caller is code you control, so flexible queries buy little and cost parsing and cost analysis on every hop; gRPC gives tighter contracts, smaller messages, streaming and deadlines. GraphQL earns its keep at the edge, where many clients want many shapes.

    How would you cache a GraphQL API? Client caches keyed by type and id, persisted queries sent as GET so a CDN can cache them, and ordinary caches behind resolvers.

    What is the N+1 problem in GraphQL, and how do you fix it? A list returns N items and a child field resolves once per item: 1 + N backend calls. A per-request DataLoader batches them, so 20 orders become 2 calls instead of 21.

    Why is gRPC faster than REST with JSON, and when isn’t it? Smaller binary messages (9 bytes against 65 for our order summary), no text parsing, and many calls multiplexed on one HTTP/2 connection. The edge shrinks for string-heavy or gzipped payloads and vanishes when the time goes to a slow database query, so measure first.

    How does each one handle breaking changes? REST runs /v1 and /v2 side by side. GraphQL adds fields, deprecates old ones and removes them at zero usage. gRPC adds fields with new numbers, never reuses one, and puts breaking changes in a new package such as delivery.v2.

    Where to go next

    References and further reading

    1. Architectural Styles and the Design of Network-based Software Architectures, Chapter 5: REST (Fielding, 2000)ics.uci.eduThe REST constraints and what each one buys and costs, including the efficiency price of a uniform interface.
    2. RFC 9110: HTTP Semanticsrfc-editor.orgSafe and idempotent methods, and the note that most caches only store GET and HEAD responses.
    3. GraphQL Specification (September 2025 edition)spec.graphql.orgThe three operation types, the response format with data and errors, and how execution errors become nulls.
    4. GraphQL Learn: Performancegraphql.orgThe N+1 problem, batching with DataLoader, GET requests for queries and hashed persisted queries for CDN caching.
    5. GraphQL Learn: Securitygraphql.orgTrusted documents, depth, breadth and alias limits, query complexity analysis and the limits of disabling introspection.
    6. GitHub Docs: Rate limits and query limits for the GraphQL APIdocs.github.comPoint-based primary limits, the 500,000-node cap, first/last between 1 and 100, secondary limits and the 10-second timeout.
    7. The GitHub GraphQL API (GitHub Blog, 2016)github.blogWhy GitHub added GraphQL: REST drove over 60% of database-tier requests, and views needed two or three calls.
    8. gRPC: Core concepts, architecture and lifecyclegrpc.ioProtobuf service definitions, generated stubs, the four RPC types, per-call ordering, metadata and cancellation.
    9. gRPC: Deadlinesgrpc.ioNo default deadline, server-side cancellation, and deadline propagation converted to a timeout to avoid clock skew.
    10. Courier: Dropbox migration to gRPC (Dropbox Tech Blog)dropbox.techA real adoption: why Dropbox chose gRPC, direct service-to-service calls, and mandatory deadlines that travel across boundaries.