API Design: REST, GraphQL & gRPC
Compare REST, GraphQL, and gRPC: when to use each, versioning strategies, pagination, and API design best practices.
The Three Major API Paradigms
Modern distributed systems communicate primarily through APIs. Three paradigms dominate: REST (the web's default), GraphQL (flexible client-driven queries), and gRPC (high-performance binary RPC). Each reflects different trade-offs between simplicity, flexibility, and performance.
| Aspect | REST | GraphQL | gRPC |
|---|---|---|---|
| Protocol | HTTP/1.1 or HTTP/2 | HTTP/1.1 or HTTP/2 | HTTP/2 |
| Data format | JSON (usually), XML | JSON | Protocol Buffers (binary) |
| Schema | OpenAPI / informal | Strongly typed GraphQL schema | Strongly typed .proto files |
| Fetching | Fixed endpoints, may over/under-fetch | Client requests exactly what it needs | Fixed RPC methods in proto |
| Real-time | Polling or webhooks | Subscriptions (WebSocket) | Server streaming, bidirectional streaming |
| Tooling maturity | Excellent — universal support | Good — Apollo, Relay | Good — generated stubs in 10+ languages |
| Browser support | Native | Native | Requires gRPC-Web proxy |
| Best for | Public APIs, simple CRUD, external clients | Flexible client needs, multiple client types (mobile/web) | Internal microservices, low-latency, streaming |
REST: Representational State Transfer
REST is an architectural style built on HTTP conventions. Resources are identified by URLs, and HTTP verbs (`GET`, `POST`, `PUT`, `PATCH`, `DELETE`) represent operations. A well-designed REST API is self-describing, cacheable, and stateless.
# Create a user
POST /users
Content-Type: application/json
{"name": "Alice", "email": "alice@example.com"}
# Read a user
GET /users/123
# Update a user (partial)
PATCH /users/123
{"name": "Alice Smith"}
# List a user's orders (nested resource)
GET /users/123/orders?status=pending&page=2&limit=20
# Delete a user
DELETE /users/123- Stateless: Each request contains all information needed to process it. No server-side session state.
- Uniform interface: Consistent URL structure and HTTP verb semantics across all resources.
- Cacheable: GET responses can be cached. REST's alignment with HTTP makes CDN integration natural.
- Layered system: Clients don't need to know if they're talking to the server directly or via a proxy.
GraphQL
GraphQL was developed by Facebook to solve the over-fetching and under-fetching problems of REST. Clients send a query describing exactly what data they need, and the server returns precisely that — no more, no less.
# Client requests exactly the fields it needs
query {
user(id: "123") {
name
email
orders(status: PENDING) {
id
total
items {
productName
quantity
}
}
}
}
# Mutations change data
mutation {
createUser(input: { name: "Alice", email: "alice@example.com" }) {
id
name
}
}
# Subscriptions for real-time updates
subscription {
orderStatusChanged(userId: "123") {
orderId
newStatus
}
}GraphQL trade-offs
GraphQL solves over/under-fetching but introduces complexity: N+1 query problems (use DataLoader to batch), cache invalidation is harder (no URL-based caching), and file uploads require workarounds. It also exposes your full data model to clients, which can be a security concern for public APIs. Use GraphQL when you have diverse clients with varying data needs (mobile vs web vs third-party).
gRPC
gRPC (Google Remote Procedure Call) uses Protocol Buffers for serialization and HTTP/2 for transport. It generates client and server stubs in 10+ languages from a `.proto` schema file, making cross-language service communication type-safe and efficient.
// users.proto
syntax = "proto3";
service UserService {
rpc GetUser (GetUserRequest) returns (User);
rpc ListUsers (ListUsersRequest) returns (stream User); // server streaming
rpc CreateUser (CreateUserRequest) returns (User);
}
message GetUserRequest {
string user_id = 1;
}
message User {
string id = 1;
string name = 2;
string email = 3;
int64 created_at = 4;
}Protocol Buffers produce binary payloads 5-10x smaller than equivalent JSON. HTTP/2 multiplexing allows multiple RPC calls over a single TCP connection. gRPC natively supports four communication patterns: unary (request-response), server streaming, client streaming, and bidirectional streaming.
API Versioning Strategies
APIs must evolve without breaking existing clients. The main versioning strategies are:
| Strategy | Example | Pros | Cons |
|---|---|---|---|
| URL path versioning | `/v1/users`, `/v2/users` | Explicit, easy to see in logs and docs | URL is supposed to identify a resource, not a version |
| Query parameter | `/users?version=2` | Clean URLs | Easy to forget, not cacheable cleanly |
| Header versioning | `API-Version: 2` | Clean URLs, follows HTTP semantics | Harder to test in browser, less discoverable |
| Content negotiation | `Accept: application/vnd.example.v2+json` | Fully RESTful | Complex, poor tooling support |
| Proto backward compatibility | Add new fields only; never remove or renumber | No version needed for minor changes | Schema discipline required |
Pagination Patterns
When APIs return lists, pagination prevents responses from growing unbounded. Three main approaches exist:
| Pattern | How It Works | Use Case | Weakness |
|---|---|---|---|
| Offset/limit | `?page=3&limit=20` → `OFFSET 40 LIMIT 20` | Simple, works with any SQL DB | Performance degrades at high offsets; items can shift between pages |
| Cursor-based | `?cursor=<opaque-token>` (encodes last seen ID) | Real-time feeds (Twitter, Facebook) | Cannot jump to arbitrary pages |
| Keyset / seek | `?after_id=1234&limit=20` → `WHERE id > 1234` | High-performance; stable with inserts | Requires indexed sort column |
Prefer cursor-based pagination for feeds
Offset pagination breaks when new items are inserted — a user refreshing a page sees duplicates or skipped items as rows shift. Cursor-based pagination is stable: the cursor encodes the position in the result set, not a numeric page number. Use offset pagination only for slow-changing data where users need to jump to arbitrary pages.
Interview Tip
In API design interviews, always clarify the client context first: Who are the consumers? Internal microservices (gRPC is great), a mobile app with diverse data needs (GraphQL), or a public third-party API (REST with OpenAPI). Then discuss versioning and pagination. Bonus: mention idempotency keys for mutation safety — any non-idempotent operation (creating an order, charging a card) should accept a client-generated idempotency key so retries don't cause duplicates.