REST, GraphQL & gRPC
The Richardson maturity model, and where real APIs sit
The model grades how RESTful an HTTP API is. Level 0 tunnels everything through one endpoint and one verb. Level 1 gives each resource its own URI. Level 2 uses HTTP methods by their real meaning (GET is safe and cacheable, POST/PUT/PATCH/DELETE for writes) and returns honest status codes like 201, 404, and 409. Level 3 adds hypermedia: responses carry links that tell the client what it can do next.
Wrong "It only counts as REST if it implements HATEOAS." Most public APIs, Stripe and GitHub included, stop at Level 2. Level 3 hypermedia is rarely fully implemented, because clients hard-code URI templates instead of following links and the extra payload earns little. Right Level 2 is the working definition teams ship against, and Level 3 is an ideal you can cite. One method rule to keep straight: PUT is idempotent, POST is not, and GET and HEAD are safe and cacheable on top of that.
Why moving to GraphQL drops free HTTP caching
REST caches almost for free: a GET response caches by its URL, and Cache-Control with ETag let CDNs and browsers revalidate. GraphQL sends every operation as a POST to a single /graphql endpoint with the query in the body, so the URL never varies and a URL-keyed cache has nothing to distinguish. You trade cheap edge caching for a client-side normalized cache. The usual mitigation is Automatic Persisted Queries: the client sends a SHA-256 hash of the query, and serving hashed queries over GET lets a CDN cache the response by that hash.
The N+1 problem and DataLoader
GraphQL resolves each field with its own resolver. Load 100 posts, resolve author once per post, and you fire 1 query for the list plus 100 for the authors: 101 round trips. This is the N+1 problem. The fix is DataLoader: the author resolver calls loader.load(authorId), and DataLoader coalesces every .load made in the same tick of the event loop into one batched query. Create a new loader per request and hang it on the context, because its cache is a per-request memoizer and a shared instance would leak one user's data into another's response.
Calling gRPC from a browser
You cannot call a gRPC service directly from browser JavaScript, because the browser has no access to the raw HTTP/2 frames and trailers gRPC needs. You generate a gRPC-Web client and route it through a proxy that translates gRPC-Web to gRPC, with Envoy as the default. Even then only unary and server-streaming calls work from the browser; client-streaming and bidirectional streaming are not supported.
HTTP/2 multiplexing and the head-of-line blocking it does not fix
gRPC runs on HTTP/2, which carries many concurrent RPCs as interleaved streams over one TCP connection with binary framing. That removes the application-layer head-of-line blocking of HTTP/1.1, where one slow response blocked everything queued behind it.
Wrong "HTTP/2 multiplexing eliminates head-of-line blocking." It removes it only at the application layer. All streams share one TCP connection, and TCP delivers bytes in order, so one dropped packet stalls every multiplexed stream until it is retransmitted. Right only HTTP/3 over QUIC gives each stream independent loss recovery. gRPC over HTTP/3 is still experimental across implementations, so most gRPC today carries TCP-level head-of-line blocking under packet loss.
DataLoader: batching, memoization, and the per-request rule
DataLoader does two jobs. Batching coalesces every .load(key) made within one tick of the event loop into a single call to your batch function. Memoization returns the same promise for a repeated key inside a request. The batch function has a hard contract: given keys, it returns a promise of a values array of the same length and order as keys, filling a miss with null or an Error. Return a map, or rows in the database's order, and you silently hand the wrong record to the wrong parent.
That cache is a request-scoped memoizer, not an application cache. Build a new instance per request on the GraphQL context. One shared instance leaks a user's loaded rows into another user's response and serves stale reads, so it never replaces Redis.
GraphQL query cost, depth, and complexity
A single /graphql request can be arbitrarily expensive, so counting requests does not bound load. Depth limiting rejects queries nested past a maximum. Complexity analysis assigns each field a cost and rejects a query whose total exceeds a budget, scored statically before execution, with the actual cost measured during execution refunding the difference. Shopify's model is concrete: a 1,000-point bucket refilling at 50 points per second, where an object costs 1, a scalar 0, a connection 2 plus the objects it returns, and a mutation 10.
Versioning across REST, GraphQL, and gRPC
REST usually versions in the path (/v1/orders), which stays cache-friendly because each version is a distinct URL. Check the exemplars before copying them, though: several APIs that look path-versioned freeze that prefix and version elsewhere. Stripe's /v1 has never been bumped for a breaking change; it pins a version per account through the date-based Stripe-Version header. GitHub versions through the X-GitHub-Api-Version date header too, not the path. A genuine path bump looks like the Twitter/X API moving from 1.1 to 2. GraphQL evolves the schema in place: additive fields break nothing, and you retire a field by marking it @deprecated(reason:), then removing it once no client reads it. Wrong "GraphQL removes versioning." Renaming or removing a field still breaks clients, so the work moves from a URL bump to field-level deprecation. gRPC leans on protobuf, where fields are keyed by number on the wire: never reuse or renumber a field, reserved the numbers you drop, and bump the package (myapi.v1 to myapi.v2) for a breaking service change.
beta
The interviewer part is in the works.
The diagnostic, personal maps, and AI mock interviews are being finished right now. The notes stay free either way. Leave an email and you'll get the first-cohort invite, plus a month of Pro when it opens.