REST API Design
Introduction to System Design
Fundamentals, why it matters, key concepts
Scalability Fundamentals
Horizontal vs vertical scaling, stateless design
Load Balancing & Caching
Algorithms, Redis, CDN patterns
Database Design & Sharding
SQL vs NoSQL, replication, partitioning
Microservices Architecture
Service decomposition, API gateways, sagas
6
API Design & REST/GraphQL
RESTful principles, GraphQL, gRPC
You Are Here
7
Message Queues & Event-Driven
Kafka, RabbitMQ, event sourcing
8
CAP Theorem & Consistency
Distributed trade-offs, eventual consistency
9
Rate Limiting & Security
Throttling algorithms, DDoS protection
10
Monitoring & Observability
Logging, metrics, distributed tracing
11
Real-World Case Studies
URL shortener, chat, feed, video streaming
12
Low-Level Design Patterns
SOLID, OOP patterns, data modeling
13
Distributed Systems Deep Dive
Consensus, Paxos, Raft, coordination
14
Authentication & Security
OAuth, JWT, zero trust, compliance
15
Interview Preparation
4-step framework, estimation, strategies
REST (Representational State Transfer) is an architectural style for designing networked applications. It relies on a stateless, client-server protocol—almost always HTTP—to provide standard operations on resources.
Key Insight: Good API design is about making the common cases easy and the complex cases possible. Think like a consumer of your API.
RESTful Principles
- Client-Server: Separation of concerns—UI from data storage
- Stateless: Each request contains all information needed
- Cacheable: Responses can be cached to improve performance
- Uniform Interface: Standardized methods and URI structure
- Layered System: Client can't tell if connected to end server
Resource Naming Conventions
# Good REST URLs - Resources as nouns, plural
GET /api/v1/users # List all users
GET /api/v1/users/123 # Get specific user
POST /api/v1/users # Create new user
PUT /api/v1/users/123 # Update entire user
PATCH /api/v1/users/123 # Partial update
DELETE /api/v1/users/123 # Delete user
# Nested resources
GET /api/v1/users/123/orders # User's orders
GET /api/v1/users/123/orders/456 # Specific order
POST /api/v1/users/123/orders # Create order for user
# Bad REST URLs - Avoid verbs, actions
GET /api/v1/getUsers # ? Verb in URL
POST /api/v1/createUser # ? Verb in URL
GET /api/v1/user # ? Singular noun
HTTP Methods
Each HTTP method has specific semantics and properties:
| Method |
Purpose |
Idempotent |
Safe |
Request Body |
| GET |
Retrieve resource |
? Yes |
? Yes |
No |
| POST |
Create resource |
? No |
? No |
Yes |
| PUT |
Replace resource |
? Yes |
? No |
Yes |
| PATCH |
Partial update |
? No* |
? No |
Yes |
| DELETE |
Remove resource |
? Yes |
? No |
Optional |
| HEAD |
Get headers only |
? Yes |
? Yes |
No |
| OPTIONS |
Get allowed methods |
? Yes |
? Yes |
No |
*PATCH can be idempotent if designed carefully
Status Codes
# HTTP Status Codes - Use them correctly!
# 2xx Success
200 OK # General success, return data
201 Created # Resource created (return location header)
204 No Content # Success, no response body (DELETE)
# 3xx Redirection
301 Moved Permanently # Resource URL changed
304 Not Modified # Cached version is current
# 4xx Client Errors
400 Bad Request # Invalid syntax, validation error
401 Unauthorized # Authentication required/failed
403 Forbidden # Authenticated but not authorized
404 Not Found # Resource doesn't exist
405 Method Not Allowed # HTTP method not supported
409 Conflict # State conflict (duplicate, version)
422 Unprocessable Entity # Valid syntax, semantic error
429 Too Many Requests # Rate limit exceeded
# 5xx Server Errors
500 Internal Server Error # Unexpected server error
502 Bad Gateway # Upstream server error
503 Service Unavailable # Maintenance or overload
504 Gateway Timeout # Upstream timeout
Response Format
// Successful response
{
"data": {
"id": 123,
"name": "John Doe",
"email": "john@example.com",
"created_at": "2024-01-15T10:30:00Z"
},
"meta": {
"request_id": "abc123",
"response_time_ms": 45
}
}
// Error response
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid email format",
"details": [
{
"field": "email",
"message": "Must be a valid email address"
}
]
},
"meta": {
"request_id": "def456"
}
}
Pagination
# Cursor-based pagination (recommended for large datasets)
GET /api/v1/users?cursor=eyJpZCI6MTAwfQ&limit=20
# Response
{
"data": [...],
"pagination": {
"next_cursor": "eyJpZCI6MTIwfQ",
"has_more": true
}
}
# Offset-based pagination (simpler but less efficient)
GET /api/v1/users?page=5&per_page=20
# Response
{
"data": [...],
"pagination": {
"total": 458,
"page": 5,
"per_page": 20,
"total_pages": 23
}
}
GraphQL
GraphQL is a query language for APIs developed by Facebook. It lets clients request exactly the data they need, solving over-fetching and under-fetching problems common in REST.
Schema Definition
# GraphQL Schema (SDL)
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
followers: [User!]!
createdAt: DateTime!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
comments: [Comment!]!
likes: Int!
publishedAt: DateTime
}
type Comment {
id: ID!
text: String!
author: User!
createdAt: DateTime!
}
type Query {
user(id: ID!): User
users(limit: Int, offset: Int): [User!]!
post(id: ID!): Post
posts(authorId: ID, published: Boolean): [Post!]!
}
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
createPost(input: CreatePostInput!): Post!
deletePost(id: ID!): Boolean!
}
input CreateUserInput {
name: String!
email: String!
}
input CreatePostInput {
title: String!
content: String!
}
Queries
# GraphQL Query - Client requests exactly what it needs
query GetUserWithPosts($userId: ID!) {
user(id: $userId) {
name
email
posts {
id
title
likes
comments {
text
author {
name
}
}
}
}
}
# Variables
{
"userId": "123"
}
# Response - Only requested fields
{
"data": {
"user": {
"name": "John Doe",
"email": "john@example.com",
"posts": [
{
"id": "post_1",
"title": "My First Post",
"likes": 42,
"comments": [
{
"text": "Great post!",
"author": { "name": "Jane Smith" }
}
]
}
]
}
}
}
Mutations
# GraphQL Mutation - Create/Update data
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
id
title
content
publishedAt
author {
name
}
}
}
# Variables
{
"input": {
"title": "GraphQL Best Practices",
"content": "Here's what I learned..."
}
}
Subscriptions (Real-time)
# GraphQL Subscription - Real-time updates
subscription OnNewComment($postId: ID!) {
commentAdded(postId: $postId) {
id
text
author {
name
avatar
}
createdAt
}
}
# Server pushes updates when new comments arrive
REST vs GraphQL
| Aspect |
REST |
GraphQL |
| Data Fetching |
Multiple endpoints, fixed responses |
Single endpoint, flexible queries |
| Over/Under-fetching |
Common problem |
Request exactly what you need |
| Versioning |
URL versioning (/v1/, /v2/) |
Evolve schema, deprecate fields |
| Caching |
HTTP caching built-in |
More complex, client-side caching |
| Learning Curve |
Familiar HTTP semantics |
New query language, tooling |
| Documentation |
OpenAPI/Swagger |
Self-documenting schema |
| Best For |
Simple CRUD, public APIs, caching-heavy |
Complex data relationships, mobile apps |
Choosing Between Them: Use REST for simple, resource-based APIs with good caching needs. Use GraphQL when clients have diverse data needs or you're building mobile apps where bandwidth matters.
Best Practices
1. Versioning
# URL Path Versioning (most common)
GET /api/v1/users
GET /api/v2/users
# Header Versioning
GET /api/users
Accept: application/vnd.myapi.v2+json
# Query Parameter Versioning
GET /api/users?version=2
2. Error Handling
# Consistent error format
from flask import Flask, jsonify
app = Flask(__name__)
class APIError(Exception):
def __init__(self, code, message, details=None, status_code=400):
self.code = code
self.message = message
self.details = details or []
self.status_code = status_code
@app.errorhandler(APIError)
def handle_api_error(error):
return jsonify({
"error": {
"code": error.code,
"message": error.message,
"details": error.details
}
}), error.status_code
# Usage
raise APIError(
code="VALIDATION_ERROR",
message="Invalid input data",
details=[{"field": "email", "message": "Invalid format"}],
status_code=422
)
3. Request Validation
# Using Pydantic for validation
from pydantic import BaseModel, EmailStr, validator
from typing import Optional
class CreateUserRequest(BaseModel):
name: str
email: EmailStr
age: Optional[int] = None
@validator('name')
def name_must_not_be_empty(cls, v):
if not v.strip():
raise ValueError('Name cannot be empty')
return v.strip()
@validator('age')
def age_must_be_positive(cls, v):
if v is not None and v < 0:
raise ValueError('Age must be positive')
return v
# Automatically validates and returns 422 on error
4. Filtering, Sorting, and Field Selection
# Flexible querying
GET /api/v1/products?
category=electronics&
price_min=100&
price_max=500&
sort=-created_at,name& # Descending created_at, then name
fields=id,name,price # Only return these fields
# Implementation
def get_products(request):
query = Product.query
# Filtering
if category := request.args.get('category'):
query = query.filter(Product.category == category)
if price_min := request.args.get('price_min'):
query = query.filter(Product.price >= float(price_min))
# Sorting
sort_fields = request.args.get('sort', 'id').split(',')
for field in sort_fields:
if field.startswith('-'):
query = query.order_by(desc(getattr(Product, field[1:])))
else:
query = query.order_by(getattr(Product, field))
# Field selection
fields = request.args.get('fields', '').split(',')
return query.all()
Networking & Communication Protocols
Key Insight: Understanding the underlying networking protocols is crucial for designing efficient APIs. HTTP sits on top of TCP/IP, and knowing when to use alternatives like WebSockets or gRPC can significantly improve system performance.
HTTP/2 vs HTTP/1.1
| Feature |
HTTP/1.1 |
HTTP/2 |
| Multiplexing |
Sequential requests (head-of-line blocking) |
Multiple streams over single connection |
| Header Compression |
None (repeated headers) |
HPACK compression |
| Server Push |
No |
Yes (proactively send resources) |
| Binary Protocol |
Text-based |
Binary framing |
WebSockets & Real-Time Communication
WebSockets provide full-duplex communication channels over a single TCP connection, ideal for real-time applications.
WebSocket Implementation
# Server-side (Python with websockets library)
import asyncio
import websockets
import json
connected_clients = set()
async def handler(websocket, path):
# Register client
connected_clients.add(websocket)
try:
async for message in websocket:
data = json.loads(message)
if data["type"] == "chat_message":
# Broadcast to all connected clients
await broadcast({
"type": "new_message",
"user": data["user"],
"message": data["message"],
"timestamp": datetime.utcnow().isoformat()
})
finally:
connected_clients.remove(websocket)
async def broadcast(message):
if connected_clients:
await asyncio.gather(
*[client.send(json.dumps(message)) for client in connected_clients]
)
# Start server
asyncio.run(websockets.serve(handler, "localhost", 8765))
// Client-side (JavaScript)
const socket = new WebSocket('wss://example.com/ws');
socket.onopen = () => {
console.log('Connected');
socket.send(JSON.stringify({
type: 'chat_message',
user: 'John',
message: 'Hello!'
}));
};
socket.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Received:', data);
};
socket.onclose = () => console.log('Disconnected');
socket.onerror = (error) => console.error('Error:', error);
Real-Time
Bidirectional
When to Use WebSockets
- ? Chat applications
- ? Live notifications
- ? Real-time dashboards
- ? Multiplayer games
- ? Collaborative editing
- ? Simple request-response (use REST)
- ? Occasional updates (use polling or SSE)
gRPC & Protocol Buffers
gRPC is a high-performance RPC framework using Protocol Buffers for serialization. It's ideal for microservices communication.
Protocol Buffer Definition
# user.proto - Define service and messages
syntax = "proto3";
package user;
service UserService {
// Unary RPC
rpc GetUser(GetUserRequest) returns (User);
// Server streaming
rpc ListUsers(ListUsersRequest) returns (stream User);
// Client streaming
rpc CreateUsers(stream CreateUserRequest) returns (CreateUsersResponse);
// Bidirectional streaming
rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}
message User {
string id = 1;
string name = 2;
string email = 3;
int32 age = 4;
}
message GetUserRequest {
string id = 1;
}
message ListUsersRequest {
int32 page_size = 1;
string page_token = 2;
}
gRPC Server Implementation
# Python gRPC Server
import grpc
from concurrent import futures
import user_pb2
import user_pb2_grpc
class UserService(user_pb2_grpc.UserServiceServicer):
def GetUser(self, request, context):
# Fetch user from database
user = db.get_user(request.id)
if not user:
context.set_code(grpc.StatusCode.NOT_FOUND)
context.set_details(f"User {request.id} not found")
return user_pb2.User()
return user_pb2.User(
id=user.id,
name=user.name,
email=user.email,
age=user.age
)
def ListUsers(self, request, context):
# Server streaming - yield multiple responses
users = db.list_users(page_size=request.page_size)
for user in users:
yield user_pb2.User(
id=user.id,
name=user.name,
email=user.email
)
# Start server
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
user_pb2_grpc.add_UserServiceServicer_to_server(UserService(), server)
server.add_insecure_port('[::]:50051')
server.start()
server.wait_for_termination()
gRPC vs REST
| Aspect |
REST |
gRPC |
| Protocol |
HTTP/1.1 (usually) |
HTTP/2 |
| Data Format |
JSON (text) |
Protocol Buffers (binary) |
| Performance |
Good |
Excellent (10x faster serialization) |
| Streaming |
Limited |
Built-in (all 4 types) |
| Browser Support |
Native |
Requires grpc-web proxy |
| Best For |
Public APIs, web clients |
Internal microservices, mobile apps |
Performance Comparison: In benchmarks, gRPC is typically 7-10x faster than REST+JSON for serialization/deserialization, and 2-3x faster in end-to-end latency due to HTTP/2 multiplexing.
Next Steps
Continue the Series
Part 5: Microservices Architecture
Review microservices patterns and service mesh architectures.
Read Article
Part 7: Message Queues & Event-Driven
Master asynchronous communication with message queues and event-driven architecture.
Read Article
Part 8: CAP Theorem & Consistency
Understand distributed systems trade-offs with CAP theorem and consistency models.
Read Article