What are API Paradigms?
APIs (Application Programming Interfaces) enable communication between services. Three dominant paradigms have emerged: REST, GraphQL, and gRPC. Each solves different problems.
Key question: How should services communicate?
Quick Comparison
| Feature | REST | GraphQL | gRPC |
|---|---|---|---|
| Protocol | HTTP/1.1 | HTTP/1.1 | HTTP/2 |
| Format | JSON | JSON | Protobuf (Binary) |
| Philosophy | Resources | Queries | RPC |
| Performance | Medium | Medium | High |
| Caching | Built-in | Complex | Manual |
| Browser Support | ✅ | ✅ | ❌ (needs proxy) |
| Learning Curve | Low | Medium | High |
| Best For | Public APIs | Mobile/Complex UIs | Microservices |
REST (Representational State Transfer)
Philosophy
Everything is a resource with a unique URL. Use standard HTTP methods to manipulate resources.
GET /users/123 → Read user POST /users → Create user PUT /users/123 → Update user DELETE /users/123 → Delete user
Example API
// REST endpoint
GET /api/users/123
Response:
{
"id": 123,
"name": "Alice",
"email": "alice@example.com",
"posts": [
{
"id": 1,
"title": "Hello World",
"comments": [...] // All comments included!
},
...
]
}
Problem: Over-fetching - you get ALL data even if you only need the name.
REST Implementation
// Express.js REST API
const express = require('express');
const app = express();
app.use(express.json());
// GET /users/:id
app.get('/users/:id', async (req, res) => {
const user = await db.users.findById(req.params.id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
});
// POST /users
app.post('/users', async (req, res) => {
const { name, email } = req.body;
const user = await db.users.create({ name, email });
res.status(201).json(user);
});
// PUT /users/:id
app.put('/users/:id', async (req, res) => {
const user = await db.users.update(req.params.id, req.body);
res.json(user);
});
// DELETE /users/:id
app.delete('/users/:id', async (req, res) => {
await db.users.delete(req.params.id);
res.status(204).send();
});
app.listen(3000);
REST Best Practices
// Versioning GET /api/v1/users/123 GET /api/v2/users/123 // Filtering & Pagination GET /api/users?role=admin&page=2&limit=20 // Nested resources GET /api/users/123/posts GET /api/users/123/posts/456/comments // HTTP Status Codes 200 OK // Success 201 Created // Resource created 204 No Content // Deleted successfully 400 Bad Request // Invalid input 401 Unauthorized // Not authenticated 403 Forbidden // Not authorized 404 Not Found // Resource doesn't exist 500 Internal Error // Server error
Pros & Cons
✅ Pros:
- Simple, well-understood
- Excellent caching (HTTP caching works out of the box)
- Stateless, scalable
- Great for public APIs
- Tooling everywhere
❌ Cons:
- Over-fetching (getting too much data)
- Under-fetching (need multiple requests)
- Versioning challenges
- No type safety
GraphQL
Philosophy
Client dictates exactly what data it needs. Single endpoint, flexible queries.
# Client query
{
user(id: 123) {
name
email
# Only get first 5 posts with titles
posts(limit: 5) {
title
}
}
}
# Server response (exactly what was asked)
{
"data": {
"user": {
"name": "Alice",
"email": "alice@example.com",
"posts": [
{ "title": "Hello World" },
{ "title": "GraphQL Guide" }
]
}
}
}
Schema Definition
# schema.graphql
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
comments: [Comment!]!
}
type Comment {
id: ID!
text: String!
author: User!
}
type Query {
user(id: ID!): User
users(limit: Int, offset: Int): [User!]!
post(id: ID!): Post
}
type Mutation {
createUser(name: String!, email: String!): User!
updateUser(id: ID!, name: String, email: String): User!
deleteUser(id: ID!): Boolean!
}
GraphQL Implementation
// Apollo Server
const { ApolloServer, gql } = require('apollo-server');
const typeDefs = gql`
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
content: String!
}
type Query {
user(id: ID!): User
users: [User!]!
}
type Mutation {
createUser(name: String!, email: String!): User!
}
`;
const resolvers = {
Query: {
user: async (parent, { id }, context) => {
return await context.db.users.findById(id);
},
users: async (parent, args, context) => {
return await context.db.users.findAll();
}
},
Mutation: {
createUser: async (parent, { name, email }, context) => {
return await context.db.users.create({ name, email });
}
},
User: {
// Resolver for User.posts field
posts: async (parent, args, context) => {
return await context.db.posts.findByUserId(parent.id);
}
}
};
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => ({
db: database,
user: req.user // From auth middleware
})
});
server.listen().then(({ url }) => {
console.log(`Server ready at ${url}`);
});
Advanced Features
Fragments (Reusable queries):
fragment UserInfo on User {
id
name
email
}
query GetUsers {
users {
...UserInfo
posts {
title
}
}
}
Variables:
query GetUser($userId: ID!) {
user(id: $userId) {
name
email
}
}
# Variables
{
"userId": "123"
}
Subscriptions (Real-time):
subscription OnNewPost {
postAdded {
id
title
author {
name
}
}
}
// Server-side subscription
const resolvers = {
Subscription: {
postAdded: {
subscribe: () => pubsub.asyncIterator(['POST_ADDED'])
}
}
};
// Publish event
pubsub.publish('POST_ADDED', {
postAdded: newPost
});
Pros & Cons
✅ Pros:
- No over/under-fetching
- Single request for related data
- Strong typing (schema)
- Perfect for mobile (minimize bandwidth)
- Introspection (self-documenting)
❌ Cons:
- Complex caching (can't use HTTP caching easily)
- N+1 query problem (requires DataLoader)
- Security risks (deeply nested queries can DoS server)
- Harder to version
- Files/uploads awkward
gRPC (Google Remote Procedure Call)
Philosophy
High-performance RPC using binary protocol. Call remote functions like local functions.
Client calls: getUser(123) ↓ gRPC converts to binary → Sent over HTTP/2 ↓ Server executes: getUser(123) ↓ Returns binary response → Converted to object
Protocol Buffers (Protobuf)
// user.proto
syntax = "proto3";
package users;
service UserService {
rpc GetUser(GetUserRequest) returns (User);
rpc ListUsers(ListUsersRequest) returns (ListUsersResponse);
rpc CreateUser(CreateUserRequest) returns (User);
rpc StreamUsers(StreamUsersRequest) returns (stream User);
}
message User {
int32 id = 1;
string name = 2;
string email = 3;
repeated Post posts = 4;
}
message Post {
int32 id = 1;
string title = 2;
string content = 3;
}
message GetUserRequest {
int32 id = 1;
}
message ListUsersRequest {
int32 page = 1;
int32 page_size = 2;
}
message ListUsersResponse {
repeated User users = 1;
int32 total = 2;
}
message CreateUserRequest {
string name = 1;
string email = 2;
}
message StreamUsersRequest {
int32 min_id = 1;
}
gRPC Implementation
Server (Node.js):
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
// Load proto file
const packageDefinition = protoLoader.loadSync('user.proto');
const userProto = grpc.loadPackageDefinition(packageDefinition).users;
// Implement service methods
const server = new grpc.Server();
server.addService(userProto.UserService.service, {
getUser: async (call, callback) => {
const userId = call.request.id;
const user = await db.users.findById(userId);
if (!user) {
return callback({
code: grpc.status.NOT_FOUND,
message: 'User not found'
});
}
callback(null, user);
},
listUsers: async (call, callback) => {
const { page, page_size } = call.request;
const users = await db.users.findAll({ page, page_size });
callback(null, { users, total: users.length });
},
createUser: async (call, callback) => {
const { name, email } = call.request;
const user = await db.users.create({ name, email });
callback(null, user);
},
// Server streaming
streamUsers: (call) => {
const stream = db.users.streamFrom(call.request.min_id);
stream.on('data', (user) => {
call.write(user);
});
stream.on('end', () => {
call.end();
});
}
});
server.bindAsync(
'0.0.0.0:50051',
grpc.ServerCredentials.createInsecure(),
() => server.start()
);
Client:
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const packageDefinition = protoLoader.loadSync('user.proto');
const userProto = grpc.loadPackageDefinition(packageDefinition).users;
const client = new userProto.UserService(
'localhost:50051',
grpc.credentials.createInsecure()
);
// Unary call
client.getUser({ id: 123 }, (error, user) => {
if (error) {
console.error(error);
} else {
console.log('User:', user);
}
});
// Server streaming
const call = client.streamUsers({ min_id: 100 });
call.on('data', (user) => {
console.log('Received user:', user);
});
call.on('end', () => {
console.log('Stream ended');
});
Four Communication Patterns
service UserService {
// 1. Unary: Single request → Single response
rpc GetUser(GetUserRequest) returns (User);
// 2. Server streaming: Single request → Stream of responses
rpc ListUsers(ListUsersRequest) returns (stream User);
// 3. Client streaming: Stream of requests → Single response
rpc UploadUsers(stream User) returns (UploadResponse);
// 4. Bidirectional streaming: Stream ←→ Stream
rpc Chat(stream Message) returns (stream Message);
}
Pros & Cons
✅ Pros:
- Extremely fast (binary, HTTP/2, multiplexing)
- Type-safe (generated code)
- Streaming support (built-in)
- Multiple languages (code generation)
- Compact payload (50-70% smaller than JSON)
❌ Cons:
- No browser support (needs envoy/grpc-web proxy)
- Harder to debug (binary, not human-readable)
- Learning curve (Protobuf, code generation)
- Limited middleware ecosystem
Performance Comparison
Payload Size
Same data (1000 users): JSON (REST/GraphQL): 120 KB Protobuf (gRPC): 42 KB Savings: 65%
Latency Benchmark
100,000 requests: REST: 450ms average GraphQL: 420ms average gRPC: 180ms average gRPC is 2.5x faster!
HTTP/2 Multiplexing
gRPC uses HTTP/2: - Single TCP connection - Multiple concurrent requests - Header compression - Server push REST typically uses HTTP/1.1: - One request per connection - Head-of-line blocking
Real-World Usage
REST: Stripe API
curl https://api.stripe.com/v1/charges \ -u sk_test_xxx: \ -d amount=2000 \ -d currency=usd \ -d source=tok_visa
GraphQL: GitHub API
query {
viewer {
repositories(first: 10) {
nodes {
name
stargazers {
totalCount
}
}
}
}
}
gRPC: Google Cloud APIs
// Google Cloud Pub/Sub
service Publisher {
rpc Publish(PublishRequest) returns (PublishResponse);
}
Companies using gRPC:
- Netflix (internal microservices)
- Uber (inter-service communication)
- Square (payment processing)
When to Use What?
Use REST when:
- Building public APIs
- Need simple HTTP caching
- Wide audience (mobile, web, third-party)
- Team unfamiliar with GraphQL/gRPC
- Examples: Twitter API, Stripe API, AWS API
Use GraphQL when:
- Mobile app (minimize round trips)
- Complex, nested data requirements
- Rapid frontend iteration
- Multiple client types with different needs
- Examples: Facebook, GitHub, Shopify, Airbnb
Use gRPC when:
- Internal microservices
- Performance critical (low latency, high throughput)
- Polyglot services (multiple languages)
- Streaming data required
- Examples: Netflix microservices, Uber backend, Google Cloud
Migration Strategies
REST → GraphQL
// Wrap existing REST endpoints
const resolvers = {
Query: {
user: async (parent, { id }) => {
// Call existing REST API
const response = await fetch(`/api/users/${id}`);
return response.json();
}
}
};
REST → gRPC (Dual Support)
// Expose both REST and gRPC
app.get('/users/:id', restHandler); // REST
server.addService(userService, grpcHandler); // gRPC
// Use envoy proxy for gRPC-web (browser support)
Interview Tips 💡
When discussing API design in system design interviews:
- Identify audience: "For public API, use REST for simplicity and caching..."
- Performance needs: "Internal services need low latency, so gRPC..."
- Client requirements: "Mobile app with complex screens, GraphQL prevents over-fetching..."
- Trade-offs: "gRPC is fastest but doesn't work in browsers without proxy..."
- Real examples: "Netflix uses gRPC for microservices, GitHub uses GraphQL for flexible queries..."
- Hybrid approach: "Could use REST for public API, gRPC for internal services..."
Related Concepts
- HTTP/2 — Protocol powering gRPC
- API Gateway — Central entry point for APIs
- Load Balancing — Distributing API requests
- Caching Strategies — Performance optimization
- Microservices — Architecture style using these APIs
About ScaleWiki
ScaleWiki is an interactive educational platform dedicated to demystifying distributed systems, software architecture, and system design. Our mission is to provide high-quality, technically accurate resources for software engineers preparing for interviews or solving complex scaling challenges in production.
Read more about our Editorial Guidelines & Authorship.
Educational Disclaimer: The architectural patterns and system designs discussed in this article are based on common industry practices, technical whitepapers, and public engineering blogs. Actual implementations in enterprise environments may vary significantly based on specific product requirements, legacy constraints, and evolving technologies.
Related Articles
Service Discovery
Complete guide to microservice discovery in dynamic cloud environments, covering client-side vs server-side patterns, health checks, DNS-based discovery, and production implementations using Consul, etcd, Eureka, and Kubernetes services.
BitTorrent Protocol (P2P File Sharing)
Complete guide to peer-to-peer file sharing using BitTorrent protocol, covering torrent structure, piece exchange, tit-for-tat algorithm, DHT for decentralization, and real-world implementations powering massive file distribution networks.
DNS Architecture
The phonebook of the internet. How Domain Name System works, the hierarchy of Route 53, and recursive vs iterative resolution strategies.