Metadata
| Field | Value |
|---|---|
| Slug | rest-api-design |
| Difficulty | Beginner |
| Estimated Reading Time | 10 min |
| Estimated Coding Time | 15 min |
| Tier | 0 — Networking Fundamentals |
| Implementation Language | Python |
| SEO Description | Master REST API design principles — resource modeling, HTTP method semantics, status codes, versioning, pagination, and idempotency — with a complete Python implementation. |
1. Overview
What problem does it solve?
HTTP gives you raw request-response communication. But it doesn't tell you how to structure your API. Without conventions, every API becomes a snowflake:
# Without REST — chaos
POST /createUser
GET /fetchAllUsers
POST /deleteUserById
GET /getUserOrders?userId=42
POST /updateUserAddress
REST (Representational State Transfer) imposes constraints that make APIs predictable, cacheable, and evolvable:
# With REST — uniform interface
POST /users → Create user
GET /users → List users
GET /users/42 → Get user 42
PUT /users/42 → Replace user 42
PATCH /users/42 → Update user 42
DELETE /users/42 → Delete user 42
GET /users/42/orders → Get user 42's orders
What breaks without it?
- API consumers guess how to interact with your service
- Caching breaks (cache servers don't know which responses are safe to cache)
- Load balancers can't make routing decisions based on method semantics
- Client libraries become tightly coupled to server implementation
- API versioning and evolution become nightmares
2. Motivation
Why was REST invented?
In 2000, Roy Fielding (co-author of HTTP/1.1) defined REST in his PhD dissertation. He observed that the web already worked at massive scale — billions of documents, millions of servers, zero central coordination. He extracted six architectural constraints that made this possible and formalized them as REST.
The key insight: if you model your API as operations on resources (nouns) instead of remote procedure calls (verbs), you inherit all of HTTP's infrastructure — caches, proxies, load balancers, browser support — for free.
The six REST constraints
| # | Constraint | What it means | Why it matters |
|---|---|---|---|
| 1 | Client-Server | Client and server evolve independently | Deploy backend without updating apps |
| 2 | Stateless | Each request contains all info needed | Any server can handle any request → horizontal scaling |
| 3 | Cacheable | Responses declare if they're cacheable | Reduce server load, improve latency |
| 4 | Uniform Interface | Resources identified by URLs; manipulated via representations | Predictable API shape |
| 5 | Layered System | Client doesn't know if it's talking to origin or intermediary | CDNs, load balancers, API gateways work transparently |
| 6 | Code-on-Demand (optional) | Server can send executable code | JavaScript in browsers |
3. Real-World Usage
| System | REST API Example |
|---|---|
| TinyURL | POST /urls to create, GET /urls/:code to redirect |
| Twitter/X | GET /2/tweets/:id, POST /2/tweets |
| GitHub | GET /repos/:owner/:repo/pulls |
| Stripe | POST /v1/charges, GET /v1/customers/:id |
| Uber | POST /v1.2/requests (request a ride) |
GET /me/media (user's photos) | |
| Kubernetes | GET /api/v1/namespaces/default/pods |
| Google Drive | POST /upload/drive/v3/files |
| Dropbox | POST /2/files/upload |
4. Prerequisites
| Concept | Block |
|---|---|
| HTTP methods, status codes, headers | 001 HTTP & TCP/IP Fundamentals |
5. Visual Explanation
REST Resource Model
/api/v1
│
┌────────┼────────┐
│ │ │
/users /orders /products
│ │ │
┌────┼────┐ │ ┌───┼───┐
│ │ │ │ │ │
/users /users /users │ /products /products
/:id /:id /:id │ /:id /:id
│ │ │ │
/orders /addresses /reviews
Request Flow Through a REST API
sequenceDiagram
participant C as Client
participant GW as API Gateway
participant LB as Load Balancer
participant S1 as Server 1
participant DB as Database
C->>GW: POST /api/v1/users<br/>Authorization: Bearer token<br/>{"name": "Sourav", "email": "..."}
GW->>GW: Authenticate token
GW->>GW: Rate limit check
GW->>LB: Forward request
LB->>S1: Route to server
S1->>S1: Validate request body
S1->>DB: INSERT INTO users
DB-->>S1: Row created (id=42)
S1-->>LB: 201 Created<br/>Location: /api/v1/users/42<br/>{"id": 42, "name": "Sourav"}
LB-->>GW: Forward response
GW-->>C: 201 Created
HTTP Method Semantics Decision Tree
Need to do something with a resource?
│
┌─────────┼──────────────────┐
│ │ │
READ WRITE DELETE
│ │ │
GET ┌───┼───┐ DELETE
│ │
CREATE UPDATE
│ │
POST ┌───┼───┐
│ │
REPLACE PARTIAL
│ │
PUT PATCH
6. Internal Working
6.1 Resource Design
A resource is any concept you can name — a user, an order, a photo, a transaction. Each resource gets a unique URL (Uniform Resource Locator).
Rules for URL design:
✅ Use nouns, not verbs: /users, /orders, /photos
❌ Don't use verbs: /getUsers, /createOrder, /deletePhoto
✅ Use plural nouns: /users/42 (user 42 from the users collection)
❌ Don't use singular: /user/42
✅ Use hierarchy for nesting: /users/42/orders (orders belonging to user 42)
❌ Don't flatten everything: /getUserOrders?userId=42
✅ Use query params for filtering: /users?role=admin&sort=created_at
❌ Don't encode filters in path: /users/role/admin/sort/created_at
6.2 Method-to-Operation Mapping
| Operation | HTTP Method | URL | Request Body | Response | Idempotent? |
|---|---|---|---|---|---|
| Create | POST | /users | {"name": "Sourav"} | 201 + Location header | ❌ |
| List | GET | /users | — | 200 + array | ✅ |
| Read | GET | /users/42 | — | 200 + object | ✅ |
| Replace | PUT | /users/42 | Full object | 200 + updated object | ✅ |
| Partial Update | PATCH | /users/42 | Partial object | 200 + updated object | ❌ |
| Delete | DELETE | /users/42 | — | 204 No Content | ✅ |
6.3 Pagination
For large collections, never return everything. Three common patterns:
# Offset-based (simple but slow on large datasets)
GET /users?offset=100&limit=25
# Cursor-based (fast, used by Twitter, Facebook)
GET /users?cursor=eyJ0IjoxNjk...&limit=25
# Page-based (simple, used by GitHub)
GET /users?page=5&per_page=25
6.4 Versioning
# URL versioning (most common, explicit)
GET /api/v1/users
GET /api/v2/users
# Header versioning (cleaner URLs but harder to test)
GET /api/users
Accept: application/vnd.myapp.v2+json
# Query param versioning
GET /api/users?version=2
6.5 Error Responses
Always return structured errors:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Email is required",
"details": [
{
"field": "email",
"constraint": "required",
"message": "The email field must not be empty"
}
]
},
"request_id": "req_abc123"
}
7. Implementation
Why Python? Builds directly on the Block 001 HTTP server — you can see REST as a layer of conventions on top of raw HTTP, not a separate technology.
"""
002 - RESTful API Server from Scratch
Builds on 001-HTTP to add REST conventions:
- Resource-based routing
- Proper HTTP method handling
- JSON request/response
- Status codes and Location headers
- Pagination
"""
import socket
import threading
import json
from datetime import datetime, timezone
from typing import Optional
# ── In-Memory Database ──
users_db: dict[int, dict] = {
1: {"id": 1, "name": "Sourav", "email": "sourav@example.com", "role": "admin"},
2: {"id": 2, "name": "Alice", "email": "alice@example.com", "role": "user"},
3: {"id": 3, "name": "Bob", "email": "bob@example.com", "role": "user"},
}
next_id = 4
# ── HTTP Parsing (reused from Block 001) ──
def parse_request(raw: bytes) -> dict:
text = raw.decode("utf-8", errors="replace")
lines = text.split("\r\n")
parts = lines[0].split(" ")
method, path = parts[0], parts[1] if len(parts) > 1 else "/"
headers = {}
body_start = 0
for i, line in enumerate(lines[1:], 1):
if line == "":
body_start = i + 1
break
if ":" in line:
k, v = line.split(":", 1)
headers[k.strip().lower()] = v.strip()
body = "\r\n".join(lines[body_start:]).strip()
# Parse query string
query = {}
if "?" in path:
path, qs = path.split("?", 1)
for param in qs.split("&"):
if "=" in param:
k, v = param.split("=", 1)
query[k] = v
return {"method": method, "path": path, "headers": headers,
"body": body, "query": query}
def json_response(status: int, body: dict, headers: Optional[dict] = None) -> bytes:
reasons = {200: "OK", 201: "Created", 204: "No Content",
400: "Bad Request", 404: "Not Found", 405: "Method Not Allowed"}
body_bytes = json.dumps(body, indent=2).encode("utf-8") if status != 204 else b""
now = datetime.now(timezone.utc).strftime("%a, %d %b %Y %H:%M:%S GMT")
resp_headers = {
"Content-Type": "application/json",
"Content-Length": str(len(body_bytes)),
"Date": now,
"Connection": "close",
}
if headers:
resp_headers.update(headers)
header_str = "\r\n".join(f"{k}: {v}" for k, v in resp_headers.items())
return f"HTTP/1.1 {status} {reasons.get(status, 'Unknown')}\r\n{header_str}\r\n\r\n".encode() + body_bytes
# ── REST Router ──
def route(req: dict) -> bytes:
global next_id
method, path, query = req["method"], req["path"], req["query"]
# ── GET /users — List (with pagination) ──
if method == "GET" and path == "/users":
page = int(query.get("page", "1"))
per_page = int(query.get("per_page", "10"))
all_users = list(users_db.values())
# Filter by role if provided
role = query.get("role")
if role:
all_users = [u for u in all_users if u["role"] == role]
start = (page - 1) * per_page
end = start + per_page
page_users = all_users[start:end]
return json_response(200, {
"data": page_users,
"pagination": {
"page": page, "per_page": per_page,
"total": len(all_users),
"total_pages": max(1, -(-len(all_users) // per_page)),
}
})
# ── GET /users/:id — Read ──
if method == "GET" and path.startswith("/users/"):
user_id = int(path.split("/")[2])
user = users_db.get(user_id)
if not user:
return json_response(404, {"error": {"code": "NOT_FOUND", "message": f"User {user_id} not found"}})
return json_response(200, {"data": user})
# ── POST /users — Create ──
if method == "POST" and path == "/users":
try:
body = json.loads(req["body"]) if req["body"] else {}
except json.JSONDecodeError:
return json_response(400, {"error": {"code": "INVALID_JSON", "message": "Request body is not valid JSON"}})
if "name" not in body or "email" not in body:
return json_response(400, {"error": {"code": "VALIDATION_ERROR", "message": "name and email are required"}})
user = {"id": next_id, "name": body["name"], "email": body["email"], "role": body.get("role", "user")}
users_db[next_id] = user
next_id += 1
return json_response(201, {"data": user}, {"Location": f"/users/{user['id']}"})
# ── PUT /users/:id — Replace ──
if method == "PUT" and path.startswith("/users/"):
user_id = int(path.split("/")[2])
if user_id not in users_db:
return json_response(404, {"error": {"code": "NOT_FOUND", "message": f"User {user_id} not found"}})
try:
body = json.loads(req["body"]) if req["body"] else {}
except json.JSONDecodeError:
return json_response(400, {"error": {"code": "INVALID_JSON", "message": "Invalid JSON"}})
users_db[user_id] = {"id": user_id, "name": body.get("name", ""), "email": body.get("email", ""), "role": body.get("role", "user")}
return json_response(200, {"data": users_db[user_id]})
# ── DELETE /users/:id — Delete ──
if method == "DELETE" and path.startswith("/users/"):
user_id = int(path.split("/")[2])
if user_id not in users_db:
return json_response(404, {"error": {"code": "NOT_FOUND", "message": f"User {user_id} not found"}})
del users_db[user_id]
return json_response(204, {})
# ── Method not allowed ──
if path.startswith("/users"):
return json_response(405, {"error": {"code": "METHOD_NOT_ALLOWED", "message": f"{method} not supported on {path}"}})
return json_response(404, {"error": {"code": "NOT_FOUND", "message": f"No resource at {path}"}})
# ── Server (from Block 001) ──
def handle_client(sock: socket.socket, addr: tuple):
try:
data = sock.recv(8192)
if not data:
return
req = parse_request(data)
print(f"[{addr[0]}] {req['method']} {req['path']}")
sock.sendall(route(req))
except Exception as e:
sock.sendall(json_response(500, {"error": {"code": "INTERNAL_ERROR", "message": str(e)}}))
finally:
sock.close()
def main():
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(("127.0.0.1", 8080))
server.listen(128)
print("REST API on http://127.0.0.1:8080")
print("Try:")
print(" curl http://127.0.0.1:8080/users")
print(" curl http://127.0.0.1:8080/users/1")
print(' curl -X POST http://127.0.0.1:8080/users -d \'{"name":"Eve","email":"eve@x.com"}\'')
print(" curl -X DELETE http://127.0.0.1:8080/users/1")
print(" curl http://127.0.0.1:8080/users?role=admin")
try:
while True:
client, addr = server.accept()
threading.Thread(target=handle_client, args=(client, addr), daemon=True).start()
except KeyboardInterrupt:
print("\nShutting down.")
finally:
server.close()
if __name__ == "__main__":
main()
Sample Output
$ curl http://127.0.0.1:8080/users | python -m json.tool
{
"data": [
{"id": 1, "name": "Sourav", "email": "sourav@example.com", "role": "admin"},
{"id": 2, "name": "Alice", "email": "alice@example.com", "role": "user"},
{"id": 3, "name": "Bob", "email": "bob@example.com", "role": "user"}
],
"pagination": {"page": 1, "per_page": 10, "total": 3, "total_pages": 1}
}
$ curl -X POST http://127.0.0.1:8080/users \
-d '{"name": "Eve", "email": "eve@example.com"}' | python -m json.tool
{
"data": {"id": 4, "name": "Eve", "email": "eve@example.com", "role": "user"}
}
# Response includes: Location: /users/4
$ curl http://127.0.0.1:8080/users?role=admin | python -m json.tool
{
"data": [
{"id": 1, "name": "Sourav", "email": "sourav@example.com", "role": "admin"}
],
"pagination": {"page": 1, "per_page": 10, "total": 1, "total_pages": 1}
}
$ curl -X DELETE http://127.0.0.1:8080/users/1
# 204 No Content
$ curl http://127.0.0.1:8080/users/999
{"error": {"code": "NOT_FOUND", "message": "User 999 not found"}}
8. Complexity
| Metric | Value | Notes |
|---|---|---|
| Route matching | O(n) per request | n = number of routes. Production routers use radix trees for O(k) where k = path depth |
| JSON serialization | O(n) | n = size of response object |
| Pagination scan | O(n) offset-based | Cursor-based pagination is O(1) seek |
| Memory | O(n) | n = number of stored resources |
Scalability Characteristics
- Statelessness enables horizontal scaling — add more servers behind a load balancer
- Cacheability of GET requests reduces database load
- Idempotency of GET/PUT/DELETE enables safe retries
- Rate limiting (Block 009) protects against abuse
9. Trade-offs
| REST | gRPC | GraphQL | |
|---|---|---|---|
| Protocol | HTTP/1.1+ (text) | HTTP/2 (binary) | HTTP (text) |
| Schema | OpenAPI/Swagger (optional) | Protobuf (required) | GraphQL Schema (required) |
| Overhead | Higher (JSON text) | Lower (binary encoding) | Variable |
| Caching | ✅ HTTP caching works | ❌ Requires custom caching | ❌ POST-only, no HTTP caching |
| Browser support | ✅ Native | ❌ Requires grpc-web | ✅ With client library |
| Over-fetching | ✅ Common problem | ❌ Fixed schema | ❌ Client picks fields |
| Learning curve | Low | Medium | Medium-High |
| Best for | Public APIs, CRUD | Service-to-service, streaming | Mobile apps, complex UIs |
When NOT to use REST
- Real-time bidirectional: Use WebSockets (Block 003) — REST requires client to poll
- High-throughput inter-service: Use gRPC — binary encoding saves bandwidth
- Complex nested queries: Use GraphQL — avoids N+1 REST calls
- File streaming: Use chunked transfer or direct TCP
- Event-driven: Use message queues (Block 023) — REST is request-response
10. Production Evolution
| Concern | This Implementation | Production (Spring Boot / Express / Gin) |
|---|---|---|
| Routing | String matching | Radix tree router with path params and regex |
| Validation | Manual if checks | Schema validation (JSON Schema, Zod, Pydantic) |
| Authentication | None | JWT/OAuth2 middleware |
| Serialization | json.dumps() | Auto-serialization with field filtering |
| Error handling | Manual try/catch | Global error handler with stack traces |
| Documentation | None | OpenAPI/Swagger auto-generated from code |
| Versioning | None | URL prefix routing (/v1/, /v2/) |
| CORS | None | CORS middleware for browser clients |
| Rate limiting | None | Middleware (Block 009) |
| Request logging | print() | Structured JSON logs with request IDs |
11. Common Bugs
| Bug | What happens | Fix |
|---|---|---|
| Using verbs in URLs | /createUser, /deleteItem — not RESTful | Use nouns: POST /users, DELETE /items/:id |
| Returning 200 for errors | Client can't distinguish success from failure | Use proper status codes: 400, 404, 422 |
| No pagination | GET /users returns 10M rows | Always paginate collections |
| PUT for partial update | Missing fields get set to null | Use PATCH for partial updates, PUT for full replace |
| Not returning Location header | Client doesn't know the URL of the created resource | POST should return 201 + Location: /resource/:id |
| Inconsistent error format | Some errors return strings, some objects | Use a standard error envelope everywhere |
| Sequential IDs exposed | Competitors can enumerate your data | Use UUIDs (Block 006) |
| No rate limiting | One client can DDoS your API | Add rate limiting middleware (Block 009) |
12. Interview Questions
-
Design a RESTful API for a URL shortening service. Hint: Two resources —
POST /urlsto create,GET /:codefor redirect. Consider what the response body and status codes should be. -
What's the difference between PUT and PATCH? Give an example where using the wrong one causes a bug. Hint: PUT replaces the entire resource. If you PUT with only
{"name": "X"}, all other fields become null. -
How would you design pagination for a feed with real-time inserts (like Twitter)? Hint: Offset-based breaks when new items are inserted. Cursor-based pagination uses a pointer to the last seen item.
-
Your REST API is called by 50 microservices. How do you evolve the API without breaking clients? Hint: API versioning + backward compatibility. Add new fields but never remove or rename existing ones.
-
Should you use REST or gRPC for inter-service communication? What are the tradeoffs? Hint: gRPC is faster (binary + HTTP/2) but REST is simpler to debug (curl-friendly) and has better tooling.
13. Used By (Downstream Blocks)
- 008 API Gateway — routes and aggregates REST calls
- 009 Rate Limiter — limits requests per REST endpoint
- 023 Message Queues — REST producer APIs
- 028 Circuit Breaker — wraps REST calls to failing services
- 029 Retry & Backoff — retries failed REST requests
- 030 Idempotency — ensures safe retries of POST/PATCH
- 035 Observability — traces REST request flows
14. Used In (Case Studies)
| System | REST API Pattern |
|---|---|
| TinyURL | POST /urls → create, GET /:code → 301 redirect |
| REST for auth, contacts, media upload | |
| Uber | POST /rides, GET /rides/:id, GET /drivers/nearby |
| Netflix | REST for catalog, user profiles, recommendations |
| YouTube | REST for video metadata, comments, subscriptions |
| Twitter/X | GET /2/tweets/:id, POST /2/tweets, streaming REST |
| REST for feed, stories, profile, media upload | |
| Google Drive | REST + resumable uploads for large files |
| Dropbox | REST for file metadata, content endpoints |
| Kubernetes | Fully RESTful API server — GET /api/v1/pods |
15. Related Blocks
| Relationship | Block |
|---|---|
| Previous | 001 HTTP & TCP/IP |
| Parallel | 003 WebSockets (alternative communication model) |
| Next | 008 API Gateway |
| Next | 009 Rate Limiter |
| Next | 030 Idempotency |
| Alternative | gRPC, GraphQL, SOAP, JSON-RPC |
16. Try It Yourself
Exercise 1: Add PATCH Support
Implement PATCH /users/:id that updates only the fields provided in the request body, leaving other fields unchanged. Compare the behavior with your existing PUT handler.
Exercise 2: Cursor-Based Pagination
Replace offset-based pagination with cursor-based pagination. The cursor should be an opaque string encoding the last seen id. The API should return a next_cursor field that the client passes back in the next request.
Website Metadata
| Field | Value |
|---|---|
| Hero Title | REST API Design |
| Hero Subtitle | The universal interface — model resources, use HTTP semantics, scale horizontally |
| Breadcrumb | System Design → Building Blocks → REST API Design |
| Sidebar Category | Tier 0 — Networking Fundamentals |
| Search Keywords | rest api, api design, http methods, status codes, pagination, versioning, restful, crud, resource modeling |
| Internal Links | ← 001 HTTP & TCP/IP · → 008 API Gateway · → 009 Rate Limiter |
| Suggested Illustration | A filing cabinet with labeled drawers (/users, /orders, /products), with HTTP methods (GET, POST, PUT, DELETE) as different colored keys that open the drawers |
| Suggested Animation | Animated CRUD cycle: POST creates a card → GET retrieves it → PUT replaces it → DELETE removes it with a fade-out |