COMP 4299|System Design

API Paradigms

Resource: Neetcode — System Design for Beginners, Background Video 8

These notes cover the three major paradigms for designing APIs — REST, GraphQL, and gRPC. Each paradigm sits on top of HTTP but makes very different trade-offs around flexibility, performance, and developer experience. Knowing when to reach for each one is a core system design skill.

Contents

  1. Preface — What Is an API?
  2. REST
  3. GraphQL
  4. gRPC
  5. Summary

0. Preface — What Is an API?

So far we've covered two ways of sending information between a client and a server:

  1. HTTP Requests — the standard request/response cycle
  2. WebSockets — persistent, bidirectional connections for real-time data

When clients need to access or modify data inside an application, they do so through an Application Programming Interface (API). An API is a defined contract that specifies what operations are available, what inputs they accept, and what outputs they return.

A familiar local example: the browser's localStorage API exposes localStorage.getItem(key) and localStorage.setItem(key, value). You don't need to know how the browser physically stores the data — the API abstracts that away entirely.

In distributed systems, "API" almost always refers to an external service running on another machine that your code talks to over a network. A database is a classic external service: it exposes an API (SQL queries, or a binary driver protocol) for reading and writing data. A web server is another — it exposes an API for handling HTTP requests from browsers and other services.

There are three dominant paradigms for designing these network APIs:

  1. RESTRepresentational State Transfer
  2. GraphQLGraph Query Language
  3. gRPCGoogle Remote Procedure Call
Rendering diagram…
An API layer sits between clients and backend resources — the paradigm (REST, GraphQL, gRPC) determines how requests and responses are structured

1. REST

Representational State Transfer

REST is by far the most widely used API paradigm. It is not a strict protocol — it is an architectural style built on top of HTTP. REST defines a loose set of conventions for how to expose resources over HTTP, rather than a rigid specification. That looseness is both its strength and its weakness.

Statelessness

The most important constraint of REST is that it is stateless: every request must contain all the information needed for the server to process it. The server stores no session state between requests.

Why this matters for scaling: if the server held per-user session state, a load balancer routing the same user to a different server would break that session. With stateless REST, any server can handle any request — no server "owns" a user's state. Anything that needs to persist between requests lives on the client (cookies, localStorage, URL query parameters).

Rendering diagram…
Stateless REST: pagination state lives in the URL query string, so any server behind the load balancer can handle the request identically

Resources & HTTP Verbs

REST organises everything around resources — the nouns of your system (users, posts, comments, orders). Each resource lives at a URL, and clients interact with it using standard HTTP verbs:

VerbActionExampleIdempotent?
GETRead a resourceGET /users/42✅ Yes
POSTCreate a new resourcePOST /users❌ No
PUTReplace a resource entirelyPUT /users/42✅ Yes
PATCHPartially update a resourcePATCH /users/42✅ Yes
DELETEDelete a resourceDELETE /users/42✅ Yes

Responses are almost always JSON — a lightweight, human-readable text format. The server also attaches an HTTP status code to signal the outcome of the request:

CodeMeaningWhen It's Returned
200OKSuccessful GET, PUT, PATCH, or DELETE
201CreatedSuccessful POST that created a new resource
400Bad RequestMalformed request or invalid input from the client
401UnauthorizedMissing or invalid authentication credentials
403ForbiddenAuthenticated but not permitted to access this resource
404Not FoundNo resource exists at this URL
500Internal Server ErrorUnexpected failure on the server side

REST Request / Response Flow

Rendering diagram…
REST request/response — the server returns a fixed shape per endpoint, which can include fields the client never uses (over-fetching)

Trade-offs

Pros:

  • Universally understood — every language and framework has HTTP client support
  • Stateless design makes horizontal scaling straightforward
  • GET responses are HTTP-cacheable by default (browsers, CDNs, and proxies all understand this)
  • Human-readable URLs and JSON make debugging easy without special tooling

Cons:

  • Over-fetching — the response shape is fixed per endpoint. If a UI only needs a user's name and avatar but the endpoint returns 20 fields, all 20 are sent every time.
  • No built-in real-time support — the only way to get live updates over REST is polling, which wastes bandwidth and adds latency
  • No enforced schema — client and server can silently drift out of sync without a separate contract (like OpenAPI)

💡REST Is the Right Default

REST should be your starting point for almost any public-facing or internal CRUD API. Its ubiquity means excellent tooling (Swagger/OpenAPI), broad client support, and a mental model that every engineer already knows. Only reach for GraphQL or gRPC when REST's trade-offs are actively hurting you.

2. GraphQL

GraphQL is a query language for APIs developed by Facebook in 2012 and open-sourced in 2015. Rather than exposing a set of fixed endpoints that each return a fixed shape, GraphQL exposes a single endpoint and lets the client declare exactly what data it needs in every request.

Like REST, GraphQL runs over HTTP — but it only uses POST requests, with the query sent in the request body rather than encoded in the URL.

Solving Over-fetching

The core motivation for GraphQL was REST's over-fetching problem. Consider rendering a comment feed where each comment shows only the author's username and avatar:

  • With REST: GET /comments returns full user objects for every commenter — email, phone number, bio, account settings — none of which the UI needs.
  • With GraphQL: the client sends a query specifying author { username avatar } and only those fields are returned.
Rendering diagram…
GraphQL lets the client specify exactly which fields to return — the server fetches and responds with only those fields

Queries, Mutations & Subscriptions

GraphQL operations come in three types:

OperationREST EquivalentPurpose
QueryGETRead data — returns only the requested fields
MutationPOST / PUT / PATCH / DELETEWrite data — create, update, or delete resources
SubscriptionWebSocket / SSEPush real-time updates to the client when data changes

Trade-offs

Pros:

  • Eliminates over-fetching — clients receive exactly what they ask for, nothing more
  • A single request can fetch deeply nested, related data across many resource types (replacing multiple REST round-trips)
  • Strongly-typed schema serves as living documentation and enables tooling like query auto-complete and validation
  • Adding new fields to the schema is non-breaking for existing clients

Cons:

  • Under-fetching can still occur — if a component needs data it didn't originally request, an additional round-trip is required
  • HTTP caching is harder — because all requests are POST to a single URL, standard HTTP cache infrastructure (browsers, CDNs) doesn't apply without extra configuration
  • Higher server complexity — resolvers, DataLoader batching, and schema design require more upfront engineering
  • Overkill for simple services with only a few resource types

⚠️HTTP Caching Doesn't Work Out of the Box with GraphQL

REST GET requests are cacheable by URL — browsers, CDNs, and reverse proxies all handle this automatically. Because GraphQL uses POST for every operation (even reads), standard HTTP caching ignores those requests. Solutions exist (persisted queries, GET-based reads), but they add meaningful complexity. Factor this in before migrating from REST.

📝When GraphQL Shines

GraphQL is most valuable when you have multiple clients with different data requirements — for example, a mobile app that needs a compact summary and a web dashboard that needs rich detail from the same underlying data. Rather than building two REST endpoints or over-fetching on both clients, a single GraphQL schema serves both efficiently.

3. gRPC

gRPC (Google Remote Procedure Call) is a high-performance RPC framework open-sourced by Google in 2015. Where REST and GraphQL think in terms of resources and queries, gRPC thinks in terms of calling a function on a remote machine as if it were a local function call.

Built on HTTP/2

gRPC runs specifically on HTTP/2, which gives it capabilities unavailable to REST running on HTTP/1.1:

FeatureHTTP/1.1 (REST)HTTP/2 (gRPC)
Multiplexing❌ One request per connection at a time✅ Many concurrent streams per connection
Header compression❌ Headers re-sent as plain text every request✅ Headers compressed with HPACK
Streaming❌ Response-only (chunked transfer)✅ Bidirectional streaming built in
Browser support✅ Native everywhere⚠️ Requires a gRPC-Web proxy layer

📝gRPC and WebSockets

Because HTTP/2 supports bidirectional streaming natively, gRPC can cover the same real-time use cases as WebSockets — without a separate protocol upgrade handshake. This is one reason gRPC is preferred for server-to-server communication even in latency-sensitive, streaming scenarios.

Protocol Buffers (Protobuf)

Instead of JSON, gRPC uses Protocol Buffers (Protobuf) — a compact binary serialization format defined in a .proto schema file. The schema specifies the structure of every request and response, and the gRPC toolchain auto-generates type-safe client and server code from it in dozens of languages.

proto
// user.proto
syntax = "proto3";

service UserService {
  rpc GetUser (GetUserRequest) returns (User);
}

message GetUserRequest {
  int32 id = 1;
}

message User {
  int32  id    = 1;
  string name  = 2;
  string email = 3;
}

Once the .proto file is defined, calling the remote service looks identical to calling a local function in the generated client:

python
# Python — calling a remote gRPC function like a local one
user = stub.GetUser(GetUserRequest(id=42))
print(user.name)  # "Alice"

gRPC Request / Response Flow

Rendering diagram…
gRPC uses auto-generated, type-safe clients and compact binary Protobuf encoding over HTTP/2 — significantly faster than JSON over HTTP/1.1

Trade-offs

Pros:

  • Significantly faster than REST — Protobuf binary encoding is ~5–10× smaller and faster to parse than JSON
  • Auto-generated, type-safe clients eliminate an entire class of integration bugs
  • Bidirectional streaming is a first-class feature via HTTP/2
  • Strict .proto contract is enforced at code-generation time — breaking changes are caught before deployment

Cons:

  • No native browser support — gRPC requires low-level HTTP/2 control that browsers don't expose. A gRPC-Web proxy is required for any browser client.
  • Binary encoding is not human-readable — debugging requires dedicated tooling (grpcurl, Postman gRPC mode, etc.)
  • Limited built-in error codes — gRPC's status codes are coarser than HTTP's, and richer error detail requires custom metadata
  • Higher setup cost — .proto files and code-generation tooling must be integrated into the build pipeline

💡gRPC Is the Default for Internal Microservices

When you control both sides of the connection — as with internal microservices — gRPC is almost always a better choice than REST. You get a stricter contract, better performance, bidirectional streaming, and auto-generated clients. The lack of native browser support is irrelevant for server-to-server communication.

🔑API Paradigm Decisions Are Hard to Reverse

Switching paradigms mid-project is painful. REST → GraphQL requires rewriting resolvers and client queries. REST → gRPC requires introducing Protobuf schemas and regenerating all clients. Like database choices, API paradigm decisions made early are expensive to undo once thousands of clients depend on the existing interface — get it right before launch.

Summary

ConceptKey Takeaway
RESTStateless, resource-based, JSON over HTTP — the universal default for public APIs
StatelessnessEvery request is self-contained — enables horizontal scaling without sticky sessions
HTTP VerbsGET / POST / PUT / PATCH / DELETE map to read, create, replace, update, delete
HTTP Status Codes2xx = success, 4xx = client error, 5xx = server error
Over-fetchingREST returns fixed shapes — clients receive fields they don't need
GraphQLSingle endpoint, client-specified queries — eliminates over-fetching, harder to cache
GraphQL CachingUses POST for all requests — HTTP-level caching doesn't apply by default
gRPCBinary Protobuf over HTTP/2 — fastest option, best for internal service-to-service calls
ProtobufBinary schema format — ~5–10× smaller than JSON, auto-generates type-safe clients
gRPC Browser LimitationCannot be used natively in browsers — requires a gRPC-Web proxy layer