
Rest Api Design Patterns
- 388 installs
- 61 repo stars
- Updated June 13, 2026
- manutej/luxor-claude-marketplace
rest-api-design-patterns is a Luxor Claude marketplace skill that models REST resources, versioning, pagination, error envelopes, authentication, and OpenAPI contracts for developers building public or internal HTTP APIs
About
rest-api-design-patterns is a tier-1 luxor-backend-toolkit skill in manutej/luxor-claude-marketplace—a comprehensive REST design guide spanning Richardson maturity levels 0–3, plural noun resource URIs, collection versus item routes, nested resources capped at 2–3 levels, and proper GET/POST/PUT/PATCH/DELETE semantics. It covers versioning strategies, cursor and offset pagination, filtering and sorting query parameters, consistent error response schemas, authentication and authorization patterns, HATEOAS _links, caching headers, and OpenAPI/Swagger specifications with FastAPI and Express.js examples across an ~1800-line SKILL.md. Use it when designing new microservice boundaries, refactoring inconsistent endpoints, or publishing contracts for mobile and web consumers that need predictable HTTP behavior and evolvable versioning.
- Resource naming and verbs
- Pagination and filtering
- Consistent error envelopes
- Versioning strategies
- Auth and rate limiting
Rest Api Design Patterns by the numbers
- 388 all-time installs (skills.sh)
- +19 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #1,113 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/manutej/luxor-claude-marketplace --skill rest-api-design-patternsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 388 |
|---|---|
| repo stars | ★ 61 |
| Last updated | June 13, 2026 |
| Repository | manutej/luxor-claude-marketplace ↗ |
How do you design versioned REST APIs with pagination and errors?
Model REST resources, versioning, pagination, errors, auth, and contracts for public or internal HTTP APIs consumed by web and mobile clients.
Who is it for?
Backend developers designing or refactoring HTTP APIs who need consistent resource modeling, HTTP semantics, and documented contracts for multi-client consumption.
Skip if: gRPC or GraphQL-only services, frontend component styling, or teams that already enforce a finalized organization-wide API standard with no design decisions left.
When should I use this skill?
The user designs a new REST API, adds versioning or pagination, standardizes error responses, or documents HTTP contracts with OpenAPI for web or mobile clients.
What you get
REST resource maps, URI conventions, pagination and filtering contracts, standardized error envelopes, auth patterns, and OpenAPI specification outlines.
- REST resource URI map
- Pagination and error contract spec
- OpenAPI/Swagger outline
By the numbers
- Tier-1 luxor-backend-toolkit skill with Richardson maturity levels 0 through 3
- SKILL.md spans roughly 1800 lines of REST design patterns and examples
Files
REST API Design Patterns
A comprehensive skill for designing, implementing, and maintaining RESTful APIs. Master resource modeling, HTTP methods, versioning strategies, pagination, filtering, error handling, and best practices for building scalable, maintainable APIs using FastAPI, Express.js, and modern frameworks.
When to Use This Skill
Use this skill when:
- Designing a new RESTful API from scratch
- Building microservices with HTTP/REST interfaces
- Refactoring existing APIs for better design and consistency
- Implementing CRUD operations with proper HTTP semantics
- Adding versioning to an existing API
- Designing resource relationships and nested endpoints
- Implementing pagination, filtering, and sorting
- Handling errors and validation consistently
- Building hypermedia-driven APIs (HATEOAS)
- Optimizing API performance with caching and compression
- Documenting APIs with OpenAPI/Swagger specifications
- Ensuring API security with authentication and authorization patterns
Core REST Principles
What is REST?
REST (Representational State Transfer) is an architectural style for distributed systems that emphasizes:
1. Resource-Based: Everything is a resource with a unique identifier (URI) 2. Standard Methods: Use standard HTTP methods (GET, POST, PUT, DELETE, PATCH) 3. Stateless: Each request contains all information needed to process it 4. Client-Server: Clear separation between client and server 5. Cacheable: Responses can be cached for performance 6. Uniform Interface: Consistent patterns across the API
REST Maturity Model (Richardson Maturity Model)
Level 0 - The Swamp of POX: Single URI, single HTTP method (usually POST)
- Example:
/apiwith all operations in POST body
Level 1 - Resources: Multiple URIs, each representing a resource
- Example:
/users,/posts,/products
Level 2 - HTTP Verbs: Proper use of HTTP methods
- Example: GET
/users/123, POST/users, PUT/users/123
Level 3 - Hypermedia Controls (HATEOAS): API responses include links to related resources
- Example: Response includes
"_links": {"self": "/users/123", "posts": "/users/123/posts"}
Resource Modeling
Resource Naming Conventions
1. Use Nouns, Not Verbs
Good:
GET /users
GET /products
POST /orders
Bad:
GET /getUsers
GET /getAllProducts
POST /createOrder2. Use Plural Nouns for Collections
Good:
GET /users # Collection
GET /users/123 # Individual resource
Bad:
GET /user
GET /user/1233. Use Lowercase and Hyphens
Good:
/user-profiles
/order-items
/payment-methods
Bad:
/userProfiles
/OrderItems
/payment_methods4. Hierarchy for Related Resources
Good:
/users/123/posts
/users/123/posts/456
/users/123/posts/456/comments
Avoid Deep Nesting (max 2-3 levels):
/organizations/1/departments/2/teams/3/members/4/tasks/5 # Too deep!Resource Design Patterns
Pattern 1: Collection and Item Resources
Collection Resource:
GET /products # List all products
POST /products # Create new product
Item Resource:
GET /products/123 # Get specific product
PUT /products/123 # Replace product (full update)
PATCH /products/123 # Partial update
DELETE /products/123 # Delete productPattern 2: Nested Resources (Parent-Child Relationships)
# Comments belong to posts
GET /posts/42/comments # List comments for post 42
POST /posts/42/comments # Create comment on post 42
GET /posts/42/comments/7 # Get specific comment
DELETE /posts/42/comments/7 # Delete specific comment
# Alternative for accessing comments directly
GET /comments/7 # Get comment by ID (if you have it)Pattern 3: Filtering Collections (Query Parameters)
GET /products?category=electronics
GET /products?price_min=100&price_max=500
GET /products?sort=price&order=desc
GET /users?status=active&role=admin
GET /posts?author=123&published=truePattern 4: Actions on Resources (Controllers)
For operations that don't fit standard CRUD:
POST /users/123/activate # Activate user account
POST /orders/456/cancel # Cancel order
POST /payments/789/refund # Refund payment
POST /documents/321/publish # Publish document
POST /subscriptions/654/renew # Renew subscriptionPattern 5: Bulk Operations
POST /users/bulk-create # Create multiple users
PATCH /products/bulk-update # Update multiple products
DELETE /orders/bulk-delete # Delete multiple orders
# Or using query parameters
DELETE /orders?ids=1,2,3,4,5HTTP Methods Deep Dive
GET - Retrieve Resources
Characteristics:
- Safe: No side effects
- Idempotent: Multiple identical requests have the same effect
- Cacheable: Responses can be cached
FastAPI Example:
from fastapi import FastAPI, HTTPException
from typing import List, Optional
app = FastAPI()
# Collection endpoint
@app.get("/items/")
async def list_items(
skip: int = 0,
limit: int = 10,
category: Optional[str] = None
) -> List[dict]:
"""List items with pagination and filtering."""
# Filter and paginate
items = get_items_from_db(skip=skip, limit=limit, category=category)
return items
# Individual resource endpoint
@app.get("/items/{item_id}")
async def get_item(item_id: int) -> dict:
"""Get a specific item by ID."""
item = get_item_from_db(item_id)
if not item:
raise HTTPException(status_code=404, detail="Item not found")
return itemExpress.js Example:
const express = require('express');
const app = express();
// Collection endpoint
app.get('/items', async (req, res) => {
try {
const { skip = 0, limit = 10, category } = req.query;
const items = await getItemsFromDB({ skip, limit, category });
res.json(items);
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});
// Individual resource endpoint
app.get('/items/:id', async (req, res) => {
try {
const item = await getItemFromDB(req.params.id);
if (!item) {
return res.status(404).json({ error: 'Item not found' });
}
res.json(item);
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});POST - Create Resources
Characteristics:
- Not safe: Has side effects (creates resource)
- Not idempotent: Multiple requests create multiple resources
- Response should include
Locationheader with new resource URI
FastAPI Example:
from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel
class ItemCreate(BaseModel):
name: str
price: float
category: str
description: Optional[str] = None
@app.post("/items/", status_code=status.HTTP_201_CREATED)
async def create_item(item: ItemCreate, response: Response) -> dict:
"""Create a new item."""
# Validate and create
new_item = create_item_in_db(item)
# Set Location header
response.headers["Location"] = f"/items/{new_item.id}"
return new_itemExpress.js Example:
app.use(express.json());
app.post('/items', async (req, res) => {
try {
const { name, price, category, description } = req.body;
// Validate
if (!name || !price || !category) {
return res.status(400).json({
error: 'Missing required fields: name, price, category'
});
}
// Create resource
const newItem = await createItemInDB({ name, price, category, description });
// Set Location header and return 201
res.location(`/items/${newItem.id}`)
.status(201)
.json(newItem);
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});PUT - Replace Resource
Characteristics:
- Not safe: Has side effects
- Idempotent: Multiple identical requests have the same effect
- Replaces entire resource (all fields required)
FastAPI Example:
class ItemUpdate(BaseModel):
name: str
price: float
category: str
description: str
@app.put("/items/{item_id}")
async def replace_item(item_id: int, item: ItemUpdate) -> dict:
"""Replace an entire item (all fields required)."""
existing_item = get_item_from_db(item_id)
if not existing_item:
raise HTTPException(status_code=404, detail="Item not found")
# Replace entire resource
updated_item = replace_item_in_db(item_id, item)
return updated_itemExpress.js Example:
app.put('/items/:id', async (req, res) => {
try {
const { name, price, category, description } = req.body;
// All fields required for PUT
if (!name || !price || !category || description === undefined) {
return res.status(400).json({
error: 'PUT requires all fields: name, price, category, description'
});
}
const existingItem = await getItemFromDB(req.params.id);
if (!existingItem) {
return res.status(404).json({ error: 'Item not found' });
}
// Replace entire resource
const updatedItem = await replaceItemInDB(req.params.id, req.body);
res.json(updatedItem);
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});PATCH - Partial Update
Characteristics:
- Not safe: Has side effects
- Idempotent: Multiple identical requests have the same effect
- Updates only specified fields (partial update)
FastAPI Example:
class ItemPatch(BaseModel):
name: Optional[str] = None
price: Optional[float] = None
category: Optional[str] = None
description: Optional[str] = None
@app.patch("/items/{item_id}")
async def update_item(item_id: int, item: ItemPatch) -> dict:
"""Partially update an item (only provided fields)."""
existing_item = get_item_from_db(item_id)
if not existing_item:
raise HTTPException(status_code=404, detail="Item not found")
# Update only provided fields
update_data = item.model_dump(exclude_unset=True)
updated_item = update_item_in_db(item_id, update_data)
return updated_itemExpress.js Example:
app.patch('/items/:id', async (req, res) => {
try {
const existingItem = await getItemFromDB(req.params.id);
if (!existingItem) {
return res.status(404).json({ error: 'Item not found' });
}
// Update only provided fields
const updatedItem = await updateItemInDB(req.params.id, req.body);
res.json(updatedItem);
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});DELETE - Remove Resource
Characteristics:
- Not safe: Has side effects
- Idempotent: Multiple identical requests have the same effect
- Returns 204 No Content or 200 OK with response body
FastAPI Example:
@app.delete("/items/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_item(item_id: int):
"""Delete an item."""
existing_item = get_item_from_db(item_id)
if not existing_item:
raise HTTPException(status_code=404, detail="Item not found")
delete_item_from_db(item_id)
return None # 204 No Content
# Alternative: Return deleted resource
@app.delete("/items/{item_id}")
async def delete_item_with_response(item_id: int) -> dict:
"""Delete an item and return it."""
existing_item = get_item_from_db(item_id)
if not existing_item:
raise HTTPException(status_code=404, detail="Item not found")
delete_item_from_db(item_id)
return existing_item # 200 OK with bodyExpress.js Example:
// 204 No Content approach
app.delete('/items/:id', async (req, res) => {
try {
const existingItem = await getItemFromDB(req.params.id);
if (!existingItem) {
return res.status(404).json({ error: 'Item not found' });
}
await deleteItemFromDB(req.params.id);
res.status(204).send();
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});
// 200 OK with response body approach
app.delete('/items/:id', async (req, res) => {
try {
const existingItem = await getItemFromDB(req.params.id);
if (!existingItem) {
return res.status(404).json({ error: 'Item not found' });
}
await deleteItemFromDB(req.params.id);
res.json({ message: 'Item deleted successfully', item: existingItem });
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});API Versioning Strategies
Strategy 1: URI Versioning (Most Common)
Version in the URI path - clear, explicit, easy to understand.
Pros:
- Explicit and visible
- Easy to route to different code versions
- Browser-friendly
- Simple for documentation
Cons:
- Creates multiple endpoints
- Can lead to code duplication
- URLs change between versions
FastAPI Implementation:
from fastapi import FastAPI, APIRouter
app = FastAPI()
# Version 1 router
v1_router = APIRouter(prefix="/api/v1")
@v1_router.get("/users")
async def get_users_v1():
return {"users": ["user1", "user2"], "version": "1.0"}
@v1_router.get("/users/{user_id}")
async def get_user_v1(user_id: int):
return {"id": user_id, "name": "John", "version": "1.0"}
# Version 2 router
v2_router = APIRouter(prefix="/api/v2")
@v2_router.get("/users")
async def get_users_v2(limit: int = 10, offset: int = 0):
"""V2 adds pagination"""
return {
"users": ["user1", "user2"],
"pagination": {"limit": limit, "offset": offset},
"version": "2.0"
}
@v2_router.get("/users/{user_id}")
async def get_user_v2(user_id: int):
"""V2 returns more fields"""
return {
"id": user_id,
"name": "John",
"email": "john@example.com",
"created_at": "2024-01-01",
"version": "2.0"
}
app.include_router(v1_router)
app.include_router(v2_router)Express.js Implementation:
const express = require('express');
const app = express();
// Version 1 routes
const v1Router = express.Router();
v1Router.get('/users', (req, res) => {
res.json({ users: ['user1', 'user2'], version: '1.0' });
});
v1Router.get('/users/:id', (req, res) => {
res.json({ id: req.params.id, name: 'John', version: '1.0' });
});
// Version 2 routes
const v2Router = express.Router();
v2Router.get('/users', (req, res) => {
const { limit = 10, offset = 0 } = req.query;
res.json({
users: ['user1', 'user2'],
pagination: { limit, offset },
version: '2.0'
});
});
v2Router.get('/users/:id', (req, res) => {
res.json({
id: req.params.id,
name: 'John',
email: 'john@example.com',
created_at: '2024-01-01',
version: '2.0'
});
});
app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);Strategy 2: Header Versioning
Version specified in custom header or Accept header.
Pros:
- Clean URIs
- No URL pollution
- More "RESTful" (resources have single URI)
Cons:
- Less visible
- Harder to test in browser
- More complex routing logic
FastAPI Implementation:
from fastapi import FastAPI, Header, HTTPException
app = FastAPI()
@app.get("/users")
async def get_users(api_version: str = Header(default="1.0", alias="X-API-Version")):
"""Handle multiple versions based on header"""
if api_version == "1.0":
return {"users": ["user1", "user2"], "version": "1.0"}
elif api_version == "2.0":
return {
"users": ["user1", "user2"],
"pagination": {"limit": 10, "offset": 0},
"version": "2.0"
}
else:
raise HTTPException(
status_code=400,
detail=f"Unsupported API version: {api_version}"
)
@app.get("/users/{user_id}")
async def get_user(
user_id: int,
api_version: str = Header(default="1.0", alias="X-API-Version")
):
"""User endpoint with version handling"""
if api_version == "1.0":
return {"id": user_id, "name": "John", "version": "1.0"}
elif api_version == "2.0":
return {
"id": user_id,
"name": "John",
"email": "john@example.com",
"created_at": "2024-01-01",
"version": "2.0"
}
else:
raise HTTPException(
status_code=400,
detail=f"Unsupported API version: {api_version}"
)Express.js Implementation:
app.get('/users', (req, res) => {
const version = req.get('X-API-Version') || '1.0';
if (version === '1.0') {
res.json({ users: ['user1', 'user2'], version: '1.0' });
} else if (version === '2.0') {
res.json({
users: ['user1', 'user2'],
pagination: { limit: 10, offset: 0 },
version: '2.0'
});
} else {
res.status(400).json({ error: `Unsupported API version: ${version}` });
}
});
app.get('/users/:id', (req, res) => {
const version = req.get('X-API-Version') || '1.0';
if (version === '1.0') {
res.json({ id: req.params.id, name: 'John', version: '1.0' });
} else if (version === '2.0') {
res.json({
id: req.params.id,
name: 'John',
email: 'john@example.com',
created_at: '2024-01-01',
version: '2.0'
});
} else {
res.status(400).json({ error: `Unsupported API version: ${version}` });
}
});Strategy 3: Content Negotiation (Accept Header)
Version specified in Accept header with custom media types.
FastAPI Implementation:
from fastapi import FastAPI, Request, HTTPException
app = FastAPI()
@app.get("/users")
async def get_users(request: Request):
"""Handle versioning via Accept header"""
accept = request.headers.get("accept", "application/vnd.api.v1+json")
if "vnd.api.v1+json" in accept:
return {"users": ["user1", "user2"], "version": "1.0"}
elif "vnd.api.v2+json" in accept:
return {
"users": ["user1", "user2"],
"pagination": {"limit": 10, "offset": 0},
"version": "2.0"
}
else:
raise HTTPException(
status_code=406,
detail="Not Acceptable: Unsupported media type"
)Express.js Implementation:
app.get('/users', (req, res) => {
const accept = req.get('Accept') || 'application/vnd.api.v1+json';
if (accept.includes('vnd.api.v1+json')) {
res.type('application/vnd.api.v1+json')
.json({ users: ['user1', 'user2'], version: '1.0' });
} else if (accept.includes('vnd.api.v2+json')) {
res.type('application/vnd.api.v2+json')
.json({
users: ['user1', 'user2'],
pagination: { limit: 10, offset: 0 },
version: '2.0'
});
} else {
res.status(406).json({ error: 'Not Acceptable: Unsupported media type' });
}
});Strategy 4: Query Parameter Versioning
Version as query parameter (least recommended).
GET /users?version=2.0
GET /users/123?v=2Cons:
- Mixes versioning with filtering
- Harder to cache
- Less clear separation of concerns
Pagination Patterns
Pattern 1: Offset-Based Pagination (Traditional)
Simple but can have performance issues with large datasets.
FastAPI Implementation:
from fastapi import FastAPI, Query
from typing import List
from pydantic import BaseModel
class PaginatedResponse(BaseModel):
items: List[dict]
total: int
limit: int
offset: int
has_more: bool
@app.get("/items", response_model=PaginatedResponse)
async def list_items(
limit: int = Query(default=10, ge=1, le=100),
offset: int = Query(default=0, ge=0)
):
"""Offset-based pagination"""
# Get total count
total = count_items_in_db()
# Get paginated items
items = get_items_from_db(limit=limit, offset=offset)
# Check if there are more items
has_more = (offset + limit) < total
return {
"items": items,
"total": total,
"limit": limit,
"offset": offset,
"has_more": has_more
}Express.js Implementation:
app.get('/items', async (req, res) => {
try {
const limit = Math.min(parseInt(req.query.limit) || 10, 100);
const offset = parseInt(req.query.offset) || 0;
const total = await countItemsInDB();
const items = await getItemsFromDB({ limit, offset });
const hasMore = (offset + limit) < total;
res.json({
items,
total,
limit,
offset,
has_more: hasMore
});
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});Pattern 2: Cursor-Based Pagination (Recommended for Large Datasets)
More efficient for large datasets, prevents issues with data changes during pagination.
FastAPI Implementation:
from typing import Optional
class CursorPaginatedResponse(BaseModel):
items: List[dict]
next_cursor: Optional[str] = None
has_more: bool
@app.get("/items", response_model=CursorPaginatedResponse)
async def list_items_cursor(
limit: int = Query(default=10, ge=1, le=100),
cursor: Optional[str] = None
):
"""Cursor-based pagination"""
# Get items after cursor
items = get_items_after_cursor(cursor=cursor, limit=limit + 1)
# Check if there are more items
has_more = len(items) > limit
# Get next cursor from last item
next_cursor = None
if has_more:
items = items[:limit] # Remove extra item
next_cursor = items[-1]["id"] # Use last item ID as cursor
return {
"items": items,
"next_cursor": next_cursor,
"has_more": has_more
}Express.js Implementation:
app.get('/items', async (req, res) => {
try {
const limit = Math.min(parseInt(req.query.limit) || 10, 100);
const cursor = req.query.cursor || null;
// Get one extra item to check if there are more
const items = await getItemsAfterCursor({ cursor, limit: limit + 1 });
const hasMore = items.length > limit;
let nextCursor = null;
if (hasMore) {
items.pop(); // Remove extra item
nextCursor = items[items.length - 1].id; // Last item ID as cursor
}
res.json({
items,
next_cursor: nextCursor,
has_more: hasMore
});
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});Pattern 3: Page-Based Pagination
User-friendly for UIs with page numbers.
FastAPI Implementation:
class PagePaginatedResponse(BaseModel):
items: List[dict]
page: int
page_size: int
total_pages: int
total_items: int
@app.get("/items", response_model=PagePaginatedResponse)
async def list_items_pages(
page: int = Query(default=1, ge=1),
page_size: int = Query(default=10, ge=1, le=100)
):
"""Page-based pagination"""
# Calculate offset
offset = (page - 1) * page_size
# Get total count and items
total_items = count_items_in_db()
items = get_items_from_db(limit=page_size, offset=offset)
# Calculate total pages
total_pages = (total_items + page_size - 1) // page_size
return {
"items": items,
"page": page,
"page_size": page_size,
"total_pages": total_pages,
"total_items": total_items
}Express.js Implementation:
app.get('/items', async (req, res) => {
try {
const page = Math.max(parseInt(req.query.page) || 1, 1);
const pageSize = Math.min(parseInt(req.query.page_size) || 10, 100);
const offset = (page - 1) * pageSize;
const totalItems = await countItemsInDB();
const items = await getItemsFromDB({ limit: pageSize, offset });
const totalPages = Math.ceil(totalItems / pageSize);
res.json({
items,
page,
page_size: pageSize,
total_pages: totalPages,
total_items: totalItems
});
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});Filtering and Sorting
Advanced Filtering
FastAPI Implementation:
from enum import Enum
from typing import Optional, List
class SortOrder(str, Enum):
asc = "asc"
desc = "desc"
@app.get("/products")
async def list_products(
# Filtering
category: Optional[str] = None,
min_price: Optional[float] = None,
max_price: Optional[float] = None,
in_stock: Optional[bool] = None,
tags: Optional[List[str]] = Query(None),
search: Optional[str] = None,
# Sorting
sort_by: Optional[str] = Query(default="created_at"),
order: SortOrder = SortOrder.desc,
# Pagination
limit: int = Query(default=10, ge=1, le=100),
offset: int = Query(default=0, ge=0)
):
"""Advanced filtering and sorting"""
filters = {
"category": category,
"min_price": min_price,
"max_price": max_price,
"in_stock": in_stock,
"tags": tags,
"search": search
}
# Remove None values
filters = {k: v for k, v in filters.items() if v is not None}
# Query database
products = query_products(
filters=filters,
sort_by=sort_by,
order=order.value,
limit=limit,
offset=offset
)
return {
"products": products,
"filters": filters,
"sort": {"by": sort_by, "order": order.value},
"pagination": {"limit": limit, "offset": offset}
}Express.js Implementation:
app.get('/products', async (req, res) => {
try {
const {
category,
min_price,
max_price,
in_stock,
tags,
search,
sort_by = 'created_at',
order = 'desc',
limit = 10,
offset = 0
} = req.query;
// Build filters
const filters = {};
if (category) filters.category = category;
if (min_price) filters.min_price = parseFloat(min_price);
if (max_price) filters.max_price = parseFloat(max_price);
if (in_stock !== undefined) filters.in_stock = in_stock === 'true';
if (tags) filters.tags = Array.isArray(tags) ? tags : [tags];
if (search) filters.search = search;
// Query database
const products = await queryProducts({
filters,
sortBy: sort_by,
order,
limit: Math.min(parseInt(limit), 100),
offset: parseInt(offset)
});
res.json({
products,
filters,
sort: { by: sort_by, order },
pagination: { limit, offset }
});
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});Error Handling Best Practices
Consistent Error Response Format
Standard Error Response Structure:
{
"error": {
"code": "RESOURCE_NOT_FOUND",
"message": "The requested user was not found",
"details": {
"user_id": "123",
"resource": "user"
},
"timestamp": "2024-01-15T10:30:00Z"
}
}HTTP Status Codes
Success Codes:
200 OK: Successful GET, PUT, PATCH, DELETE with response201 Created: Successful POST creating a resource202 Accepted: Request accepted for async processing204 No Content: Successful DELETE with no response body
Client Error Codes:
400 Bad Request: Invalid request syntax or validation error401 Unauthorized: Authentication required403 Forbidden: Authenticated but not authorized404 Not Found: Resource doesn't exist405 Method Not Allowed: HTTP method not supported409 Conflict: Request conflicts with current state422 Unprocessable Entity: Validation failed429 Too Many Requests: Rate limit exceeded
Server Error Codes:
500 Internal Server Error: Generic server error502 Bad Gateway: Invalid response from upstream server503 Service Unavailable: Server temporarily unavailable504 Gateway Timeout: Upstream server timeout
FastAPI Error Handling
from fastapi import FastAPI, HTTPException, status, Request
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
from datetime import datetime
app = FastAPI()
# Custom exception
class ResourceNotFoundError(Exception):
def __init__(self, resource: str, resource_id: str):
self.resource = resource
self.resource_id = resource_id
# Global exception handler
@app.exception_handler(ResourceNotFoundError)
async def resource_not_found_handler(request: Request, exc: ResourceNotFoundError):
return JSONResponse(
status_code=404,
content={
"error": {
"code": "RESOURCE_NOT_FOUND",
"message": f"The requested {exc.resource} was not found",
"details": {
"resource": exc.resource,
"resource_id": exc.resource_id
},
"timestamp": datetime.utcnow().isoformat() + "Z"
}
}
)
# Validation error handler
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
return JSONResponse(
status_code=422,
content={
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": exc.errors(),
"timestamp": datetime.utcnow().isoformat() + "Z"
}
}
)
# Using exceptions
@app.get("/users/{user_id}")
async def get_user(user_id: int):
user = get_user_from_db(user_id)
if not user:
raise ResourceNotFoundError("user", str(user_id))
return user
# Manual error responses
@app.post("/users")
async def create_user(email: str):
if user_exists(email):
raise HTTPException(
status_code=409,
detail={
"code": "DUPLICATE_EMAIL",
"message": "A user with this email already exists",
"details": {"email": email}
}
)
return create_user_in_db(email)Express.js Error Handling
const express = require('express');
const app = express();
app.use(express.json());
// Custom error class
class ResourceNotFoundError extends Error {
constructor(resource, resourceId) {
super(`${resource} not found`);
this.name = 'ResourceNotFoundError';
this.resource = resource;
this.resourceId = resourceId;
this.statusCode = 404;
}
}
class ValidationError extends Error {
constructor(message, details) {
super(message);
this.name = 'ValidationError';
this.details = details;
this.statusCode = 422;
}
}
// Routes
app.get('/users/:id', async (req, res, next) => {
try {
const user = await getUserFromDB(req.params.id);
if (!user) {
throw new ResourceNotFoundError('user', req.params.id);
}
res.json(user);
} catch (error) {
next(error);
}
});
app.post('/users', async (req, res, next) => {
try {
const { email } = req.body;
if (!email) {
throw new ValidationError('Validation failed', {
field: 'email',
message: 'Email is required'
});
}
const userExists = await checkUserExists(email);
if (userExists) {
const error = new Error('Duplicate email');
error.statusCode = 409;
error.code = 'DUPLICATE_EMAIL';
error.details = { email };
throw error;
}
const newUser = await createUserInDB(email);
res.status(201).json(newUser);
} catch (error) {
next(error);
}
});
// Global error handler (must be last)
app.use((err, req, res, next) => {
const statusCode = err.statusCode || 500;
const code = err.code || err.name || 'INTERNAL_SERVER_ERROR';
const errorResponse = {
error: {
code,
message: err.message,
timestamp: new Date().toISOString()
}
};
// Add details if available
if (err.details) {
errorResponse.error.details = err.details;
} else if (err.resource && err.resourceId) {
errorResponse.error.details = {
resource: err.resource,
resource_id: err.resourceId
};
}
// Log error for debugging (don't expose in production)
if (process.env.NODE_ENV !== 'production') {
errorResponse.error.stack = err.stack;
}
res.status(statusCode).json(errorResponse);
});HATEOAS and Hypermedia
What is HATEOAS?
HATEOAS (Hypermedia as the Engine of Application State) means including links to related resources in API responses.
Benefits:
- Self-documenting API
- Client doesn't need to construct URLs
- Easier API evolution
- Better discoverability
FastAPI HATEOAS Implementation
from fastapi import FastAPI
from pydantic import BaseModel, HttpUrl
from typing import List, Optional
class Link(BaseModel):
rel: str
href: str
method: str = "GET"
class UserResponse(BaseModel):
id: int
name: str
email: str
links: List[Link]
class UserListResponse(BaseModel):
users: List[UserResponse]
links: List[Link]
app = FastAPI()
def build_user_links(user_id: int, base_url: str = "http://api.example.com") -> List[Link]:
"""Build HATEOAS links for a user"""
return [
Link(rel="self", href=f"{base_url}/users/{user_id}", method="GET"),
Link(rel="update", href=f"{base_url}/users/{user_id}", method="PUT"),
Link(rel="delete", href=f"{base_url}/users/{user_id}", method="DELETE"),
Link(rel="posts", href=f"{base_url}/users/{user_id}/posts", method="GET"),
Link(rel="create_post", href=f"{base_url}/users/{user_id}/posts", method="POST")
]
@app.get("/users/{user_id}", response_model=UserResponse)
async def get_user(user_id: int):
"""Get user with HATEOAS links"""
user = get_user_from_db(user_id)
return {
"id": user.id,
"name": user.name,
"email": user.email,
"links": build_user_links(user.id)
}
@app.get("/users", response_model=UserListResponse)
async def list_users():
"""List users with HATEOAS links"""
users = get_users_from_db()
users_with_links = [
{
"id": user.id,
"name": user.name,
"email": user.email,
"links": build_user_links(user.id)
}
for user in users
]
collection_links = [
Link(rel="self", href="http://api.example.com/users", method="GET"),
Link(rel="create", href="http://api.example.com/users", method="POST")
]
return {
"users": users_with_links,
"links": collection_links
}Express.js HATEOAS Implementation
app.get('/users/:id', async (req, res) => {
try {
const user = await getUserFromDB(req.params.id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
const baseUrl = `${req.protocol}://${req.get('host')}`;
res.json({
id: user.id,
name: user.name,
email: user.email,
_links: {
self: { href: `${baseUrl}/users/${user.id}`, method: 'GET' },
update: { href: `${baseUrl}/users/${user.id}`, method: 'PUT' },
delete: { href: `${baseUrl}/users/${user.id}`, method: 'DELETE' },
posts: { href: `${baseUrl}/users/${user.id}/posts`, method: 'GET' },
create_post: { href: `${baseUrl}/users/${user.id}/posts`, method: 'POST' }
}
});
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});
app.get('/users', async (req, res) => {
try {
const users = await getUsersFromDB();
const baseUrl = `${req.protocol}://${req.get('host')}`;
const usersWithLinks = users.map(user => ({
id: user.id,
name: user.name,
email: user.email,
_links: {
self: { href: `${baseUrl}/users/${user.id}`, method: 'GET' },
update: { href: `${baseUrl}/users/${user.id}`, method: 'PUT' },
delete: { href: `${baseUrl}/users/${user.id}`, method: 'DELETE' }
}
}));
res.json({
users: usersWithLinks,
_links: {
self: { href: `${baseUrl}/users`, method: 'GET' },
create: { href: `${baseUrl}/users`, method: 'POST' }
}
});
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});Performance Optimization
Caching with ETags
FastAPI Implementation:
from fastapi import FastAPI, Request, Response
from fastapi.responses import JSONResponse
import hashlib
app = FastAPI()
def generate_etag(data: dict) -> str:
"""Generate ETag from response data"""
content = str(data).encode('utf-8')
return hashlib.md5(content).hexdigest()
@app.get("/users/{user_id}")
async def get_user_cached(user_id: int, request: Request):
"""Get user with ETag caching"""
user = get_user_from_db(user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
# Generate ETag
etag = generate_etag(user)
# Check If-None-Match header
if_none_match = request.headers.get("if-none-match")
if if_none_match == etag:
return Response(status_code=304) # Not Modified
# Return with ETag header
return JSONResponse(
content=user,
headers={"ETag": etag, "Cache-Control": "max-age=300"}
)Express.js Implementation:
const crypto = require('crypto');
function generateETag(data) {
const content = JSON.stringify(data);
return crypto.createHash('md5').update(content).digest('hex');
}
app.get('/users/:id', async (req, res) => {
try {
const user = await getUserFromDB(req.params.id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
const etag = generateETag(user);
// Check If-None-Match header
if (req.get('If-None-Match') === etag) {
return res.status(304).send(); // Not Modified
}
res.set('ETag', etag)
.set('Cache-Control', 'max-age=300')
.json(user);
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});Rate Limiting
Express.js Implementation:
const rateLimit = require('express-rate-limit');
// Create rate limiter
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
message: {
error: {
code: 'RATE_LIMIT_EXCEEDED',
message: 'Too many requests, please try again later'
}
},
standardHeaders: true, // Return rate limit info in headers
legacyHeaders: false
});
// Apply to all routes
app.use('/api/', apiLimiter);
// Or create specific limiters
const createAccountLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 5, // 5 requests per hour
message: 'Too many accounts created, please try again later'
});
app.post('/api/users', createAccountLimiter, async (req, res) => {
// Create user
});Compression
FastAPI Implementation:
from fastapi import FastAPI
from fastapi.middleware.gzip import GZipMiddleware
app = FastAPI()
# Add compression middleware
app.add_middleware(GZipMiddleware, minimum_size=1000)
@app.get("/large-data")
async def get_large_data():
"""This response will be compressed if > 1000 bytes"""
return {"data": [{"id": i, "value": f"item_{i}"} for i in range(1000)]}Express.js Implementation:
const compression = require('compression');
// Add compression middleware
app.use(compression({
threshold: 1024, // Only compress responses > 1KB
level: 6 // Compression level (0-9)
}));
app.get('/large-data', (req, res) => {
const data = Array.from({ length: 1000 }, (_, i) => ({
id: i,
value: `item_${i}`
}));
res.json({ data });
});Security Best Practices
Authentication Patterns
JWT Authentication in FastAPI:
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import jwt
from datetime import datetime, timedelta
app = FastAPI()
security = HTTPBearer()
SECRET_KEY = "your-secret-key"
ALGORITHM = "HS256"
def create_access_token(data: dict, expires_delta: timedelta = None):
"""Create JWT token"""
to_encode = data.copy()
expire = datetime.utcnow() + (expires_delta or timedelta(minutes=15))
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
"""Verify JWT token"""
token = credentials.credentials
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
return payload
except jwt.ExpiredSignatureError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token has expired"
)
except jwt.JWTError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials"
)
@app.post("/login")
async def login(email: str, password: str):
"""Login and get access token"""
user = authenticate_user(email, password)
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect email or password"
)
access_token = create_access_token(
data={"sub": user.email, "user_id": user.id}
)
return {"access_token": access_token, "token_type": "bearer"}
@app.get("/users/me")
async def get_current_user(payload: dict = Depends(verify_token)):
"""Protected endpoint - requires authentication"""
user_id = payload.get("user_id")
user = get_user_from_db(user_id)
return userJWT Authentication in Express.js:
const jwt = require('jsonwebtoken');
const express = require('express');
const app = express();
const SECRET_KEY = 'your-secret-key';
function createAccessToken(data, expiresIn = '15m') {
return jwt.sign(data, SECRET_KEY, { expiresIn });
}
function verifyToken(req, res, next) {
const authHeader = req.get('Authorization');
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing or invalid authorization header' });
}
const token = authHeader.substring(7);
try {
const payload = jwt.verify(token, SECRET_KEY);
req.user = payload;
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token has expired' });
}
return res.status(401).json({ error: 'Invalid token' });
}
}
app.post('/login', async (req, res) => {
const { email, password } = req.body;
const user = await authenticateUser(email, password);
if (!user) {
return res.status(401).json({ error: 'Incorrect email or password' });
}
const accessToken = createAccessToken({
sub: user.email,
user_id: user.id
});
res.json({ access_token: accessToken, token_type: 'bearer' });
});
app.get('/users/me', verifyToken, async (req, res) => {
const user = await getUserFromDB(req.user.user_id);
res.json(user);
});Input Validation and Sanitization
FastAPI Validation:
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr, Field, validator
from typing import Optional
class UserCreate(BaseModel):
email: EmailStr
username: str = Field(..., min_length=3, max_length=50, pattern="^[a-zA-Z0-9_-]+$")
password: str = Field(..., min_length=8)
age: Optional[int] = Field(None, ge=0, le=150)
@validator('password')
def password_strength(cls, v):
"""Validate password strength"""
if not any(char.isdigit() for char in v):
raise ValueError('Password must contain at least one digit')
if not any(char.isupper() for char in v):
raise ValueError('Password must contain at least one uppercase letter')
return v
@app.post("/users")
async def create_user(user: UserCreate):
"""Automatically validates input"""
# Input is already validated by Pydantic
hashed_password = hash_password(user.password)
new_user = create_user_in_db(user.email, user.username, hashed_password)
return new_userAPI Documentation
OpenAPI/Swagger with FastAPI
FastAPI automatically generates OpenAPI documentation:
from fastapi import FastAPI
from pydantic import BaseModel, Field
app = FastAPI(
title="My API",
description="Comprehensive API for managing resources",
version="1.0.0",
docs_url="/api/docs",
redoc_url="/api/redoc"
)
class Item(BaseModel):
"""Item model with rich documentation"""
name: str = Field(..., description="The name of the item", example="Widget")
price: float = Field(..., description="Price in USD", example=19.99, gt=0)
description: str = Field(None, description="Optional item description")
model_config = {
"json_schema_extra": {
"examples": [
{
"name": "Super Widget",
"price": 29.99,
"description": "An amazing widget"
}
]
}
}
@app.post(
"/items",
response_model=Item,
status_code=201,
summary="Create a new item",
description="Create a new item with name, price, and optional description",
response_description="The created item",
tags=["items"]
)
async def create_item(item: Item):
"""
Create a new item with all the information:
- **name**: The item name (required)
- **price**: The item price in USD (required, must be positive)
- **description**: Optional description of the item
"""
return itemBest Practices Summary
API Design Principles
1. Use nouns for resources, not verbs 2. Use plural nouns for collections 3. Use HTTP methods correctly (GET, POST, PUT, PATCH, DELETE) 4. Use proper HTTP status codes 5. Version your API (URI versioning recommended) 6. Support pagination for collections 7. Allow filtering and sorting with query parameters 8. Return consistent error responses 9. Use HATEOAS for better discoverability 10. Document your API with OpenAPI/Swagger
Security Principles
1. Always use HTTPS in production 2. Implement authentication (JWT, OAuth, API keys) 3. Validate all inputs thoroughly 4. Use rate limiting to prevent abuse 5. Sanitize outputs to prevent XSS 6. Implement CORS correctly 7. Use security headers (CSP, X-Frame-Options, etc.) 8. Log security events for monitoring 9. Keep dependencies updated 10. Never expose sensitive data in responses
Performance Principles
1. Use caching (ETags, Cache-Control headers) 2. Implement compression for large responses 3. Use pagination for large datasets 4. Optimize database queries (indexes, N+1 prevention) 5. Use async/await for I/O operations 6. Implement connection pooling 7. Monitor API performance (response times, error rates) 8. Use CDNs for static content 9. Implement proper logging without blocking 10. Load test your API regularly
---
Skill Version: 1.0.0 Last Updated: October 2025 Skill Category: API Design, Backend Development, REST Architecture Compatible With: FastAPI, Express.js, Node.js, Python, HTTP Frameworks
REST API Design Patterns - Comprehensive Examples
This document provides 25+ practical, real-world examples demonstrating REST API design patterns using FastAPI and Express.js with Context7 integration.
Table of Contents
1. Basic CRUD Operations 2. Advanced Resource Modeling 3. Nested Resources and Relationships 4. Pagination Patterns 5. Filtering and Sorting 6. Versioning Implementations 7. Error Handling Patterns 8. Authentication and Authorization 9. HATEOAS Implementation 10. Performance Optimization 11. Bulk Operations 12. File Upload Patterns 13. Search and Full-Text Queries 14. Real-Time Updates 15. API Documentation
---
1. Basic CRUD Operations
Example 1.1: Complete User CRUD API (FastAPI)
Context7 Integration: Based on FastAPI resource modeling patterns
from fastapi import FastAPI, HTTPException, status, Response
from pydantic import BaseModel, EmailStr, Field
from typing import List, Optional
from datetime import datetime
app = FastAPI(title="User Management API", version="1.0.0")
# Data models from Context7 patterns
class UserBase(BaseModel):
"""Base user model with common fields"""
email: EmailStr
username: str = Field(..., min_length=3, max_length=50)
full_name: Optional[str] = None
class UserCreate(UserBase):
"""Model for creating users - includes password"""
password: str = Field(..., min_length=8)
class UserUpdate(UserBase):
"""Model for partial updates - all fields optional"""
email: Optional[EmailStr] = None
username: Optional[str] = None
full_name: Optional[str] = None
password: Optional[str] = None
class UserPublic(UserBase):
"""Public user model - excludes sensitive data"""
id: int
created_at: datetime
is_active: bool
class Config:
from_attributes = True
# In-memory database (use real DB in production)
users_db = {}
user_id_counter = 1
@app.post("/users", response_model=UserPublic, status_code=status.HTTP_201_CREATED)
async def create_user(user: UserCreate, response: Response):
"""
Create a new user account.
- **email**: Valid email address (required)
- **username**: Unique username, 3-50 characters (required)
- **password**: Minimum 8 characters (required)
- **full_name**: User's full name (optional)
"""
global user_id_counter
# Check if user exists
if any(u["email"] == user.email for u in users_db.values()):
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="User with this email already exists"
)
# Create user
user_data = {
"id": user_id_counter,
"email": user.email,
"username": user.username,
"full_name": user.full_name,
"password": hash_password(user.password), # Hash in real app
"created_at": datetime.utcnow(),
"is_active": True
}
users_db[user_id_counter] = user_data
response.headers["Location"] = f"/users/{user_id_counter}"
user_id_counter += 1
return user_data
@app.get("/users", response_model=List[UserPublic])
async def list_users(
skip: int = 0,
limit: int = 10,
is_active: Optional[bool] = None
):
"""
List all users with optional filtering.
- **skip**: Number of records to skip (pagination)
- **limit**: Maximum records to return
- **is_active**: Filter by active status
"""
users = list(users_db.values())
# Filter by active status
if is_active is not None:
users = [u for u in users if u["is_active"] == is_active]
# Apply pagination
return users[skip:skip + limit]
@app.get("/users/{user_id}", response_model=UserPublic)
async def get_user(user_id: int):
"""Get a specific user by ID."""
user = users_db.get(user_id)
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"User with ID {user_id} not found"
)
return user
@app.put("/users/{user_id}", response_model=UserPublic)
async def replace_user(user_id: int, user: UserCreate):
"""
Replace a user entirely (all fields required).
This is a full replacement - use PATCH for partial updates.
"""
if user_id not in users_db:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"User with ID {user_id} not found"
)
# Preserve system fields
user_data = users_db[user_id]
user_data.update({
"email": user.email,
"username": user.username,
"full_name": user.full_name,
"password": hash_password(user.password)
})
return user_data
@app.patch("/users/{user_id}", response_model=UserPublic)
async def update_user(user_id: int, user: UserUpdate):
"""
Partially update a user (only provided fields).
Context7 pattern: use exclude_unset for partial updates.
"""
if user_id not in users_db:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"User with ID {user_id} not found"
)
user_data = users_db[user_id]
# Update only provided fields (Context7 pattern)
update_data = user.model_dump(exclude_unset=True)
if "password" in update_data:
update_data["password"] = hash_password(update_data["password"])
user_data.update(update_data)
return user_data
@app.delete("/users/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_user(user_id: int):
"""Delete a user permanently."""
if user_id not in users_db:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"User with ID {user_id} not found"
)
del users_db[user_id]
return None
def hash_password(password: str) -> str:
"""Hash password (use bcrypt in production)"""
return f"hashed_{password}"Example 1.2: Complete Product CRUD API (Express.js)
Context7 Integration: Based on Express.js HTTP methods patterns
const express = require('express');
const app = express();
app.use(express.json());
// In-memory database
const productsDB = {};
let productIdCounter = 1;
// Validation middleware
function validateProduct(req, res, next) {
const { name, price, category } = req.body;
if (!name || typeof name !== 'string' || name.length < 3) {
return res.status(400).json({
error: 'Invalid name: must be string with minimum 3 characters'
});
}
if (!price || typeof price !== 'number' || price <= 0) {
return res.status(400).json({
error: 'Invalid price: must be positive number'
});
}
if (!category || typeof category !== 'string') {
return res.status(400).json({
error: 'Invalid category: must be non-empty string'
});
}
next();
}
// CREATE - POST /products
app.post('/products', validateProduct, (req, res) => {
const { name, price, category, description, in_stock = true } = req.body;
// Check for duplicate
const duplicate = Object.values(productsDB).find(p => p.name === name);
if (duplicate) {
return res.status(409).json({
error: 'Product with this name already exists'
});
}
// Create product (Context7 pattern)
const product = {
id: productIdCounter,
name,
price,
category,
description: description || null,
in_stock,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
};
productsDB[productIdCounter] = product;
res.location(`/products/${productIdCounter}`)
.status(201)
.json(product);
productIdCounter++;
});
// READ - GET /products (list with filtering)
app.get('/products', (req, res) => {
const { category, min_price, max_price, in_stock, limit = 10, offset = 0 } = req.query;
let products = Object.values(productsDB);
// Apply filters (Context7 filtering pattern)
if (category) {
products = products.filter(p => p.category === category);
}
if (min_price) {
products = products.filter(p => p.price >= parseFloat(min_price));
}
if (max_price) {
products = products.filter(p => p.price <= parseFloat(max_price));
}
if (in_stock !== undefined) {
products = products.filter(p => p.in_stock === (in_stock === 'true'));
}
// Apply pagination
const total = products.length;
const paginatedProducts = products.slice(
parseInt(offset),
parseInt(offset) + parseInt(limit)
);
res.json({
products: paginatedProducts,
total,
limit: parseInt(limit),
offset: parseInt(offset)
});
});
// READ - GET /products/:id (single item)
app.get('/products/:id', (req, res) => {
const product = productsDB[req.params.id];
if (!product) {
return res.status(404).json({
error: `Product with ID ${req.params.id} not found`
});
}
res.json(product);
});
// UPDATE - PUT /products/:id (full replacement)
app.put('/products/:id', validateProduct, (req, res) => {
const product = productsDB[req.params.id];
if (!product) {
return res.status(404).json({
error: `Product with ID ${req.params.id} not found`
});
}
// Full replacement (Context7 PUT pattern)
const { name, price, category, description, in_stock } = req.body;
productsDB[req.params.id] = {
...product,
name,
price,
category,
description: description || null,
in_stock: in_stock !== undefined ? in_stock : true,
updated_at: new Date().toISOString()
};
res.json(productsDB[req.params.id]);
});
// UPDATE - PATCH /products/:id (partial update)
app.patch('/products/:id', (req, res) => {
const product = productsDB[req.params.id];
if (!product) {
return res.status(404).json({
error: `Product with ID ${req.params.id} not found`
});
}
// Partial update (Context7 PATCH pattern)
const updates = {};
if (req.body.name !== undefined) {
if (typeof req.body.name !== 'string' || req.body.name.length < 3) {
return res.status(400).json({
error: 'Invalid name: must be string with minimum 3 characters'
});
}
updates.name = req.body.name;
}
if (req.body.price !== undefined) {
if (typeof req.body.price !== 'number' || req.body.price <= 0) {
return res.status(400).json({
error: 'Invalid price: must be positive number'
});
}
updates.price = req.body.price;
}
if (req.body.category !== undefined) updates.category = req.body.category;
if (req.body.description !== undefined) updates.description = req.body.description;
if (req.body.in_stock !== undefined) updates.in_stock = req.body.in_stock;
updates.updated_at = new Date().toISOString();
productsDB[req.params.id] = {
...product,
...updates
};
res.json(productsDB[req.params.id]);
});
// DELETE - DELETE /products/:id
app.delete('/products/:id', (req, res) => {
const product = productsDB[req.params.id];
if (!product) {
return res.status(404).json({
error: `Product with ID ${req.params.id} not found`
});
}
delete productsDB[req.params.id];
res.status(204).send();
});
const PORT = 3000;
app.listen(PORT, () => {
console.log(`Product API listening on port ${PORT}`);
});---
2. Advanced Resource Modeling
Example 2.1: Blog Post with Tags and Categories (FastAPI)
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
from typing import List, Optional, Set
from datetime import datetime
from enum import Enum
app = FastAPI()
class PostStatus(str, Enum):
DRAFT = "draft"
PUBLISHED = "published"
ARCHIVED = "archived"
class PostBase(BaseModel):
title: str = Field(..., min_length=1, max_length=200)
content: str = Field(..., min_length=1)
excerpt: Optional[str] = Field(None, max_length=500)
tags: Set[str] = Field(default_factory=set)
category: str
class PostCreate(PostBase):
status: PostStatus = PostStatus.DRAFT
class PostUpdate(BaseModel):
title: Optional[str] = Field(None, min_length=1, max_length=200)
content: Optional[str] = None
excerpt: Optional[str] = None
tags: Optional[Set[str]] = None
category: Optional[str] = None
status: Optional[PostStatus] = None
class PostPublic(PostBase):
id: int
slug: str
status: PostStatus
author_id: int
view_count: int
created_at: datetime
updated_at: datetime
published_at: Optional[datetime] = None
posts_db = {}
post_id_counter = 1
def generate_slug(title: str) -> str:
"""Generate URL-friendly slug from title"""
return title.lower().replace(" ", "-").replace("_", "-")
@app.post("/posts", response_model=PostPublic, status_code=201)
async def create_post(post: PostCreate, author_id: int = 1):
"""Create a new blog post with tags and category"""
global post_id_counter
slug = generate_slug(post.title)
# Check for duplicate slug
if any(p["slug"] == slug for p in posts_db.values()):
raise HTTPException(
status_code=409,
detail=f"Post with slug '{slug}' already exists"
)
post_data = {
"id": post_id_counter,
"slug": slug,
"title": post.title,
"content": post.content,
"excerpt": post.excerpt,
"tags": list(post.tags),
"category": post.category,
"status": post.status,
"author_id": author_id,
"view_count": 0,
"created_at": datetime.utcnow(),
"updated_at": datetime.utcnow(),
"published_at": datetime.utcnow() if post.status == PostStatus.PUBLISHED else None
}
posts_db[post_id_counter] = post_data
post_id_counter += 1
return post_data
@app.get("/posts", response_model=List[PostPublic])
async def list_posts(
status: Optional[PostStatus] = None,
category: Optional[str] = None,
tag: Optional[str] = None,
author_id: Optional[int] = None,
limit: int = 10,
offset: int = 0
):
"""
List posts with advanced filtering.
Filter by status, category, tags, or author.
"""
posts = list(posts_db.values())
# Apply filters
if status:
posts = [p for p in posts if p["status"] == status]
if category:
posts = [p for p in posts if p["category"] == category]
if tag:
posts = [p for p in posts if tag in p["tags"]]
if author_id:
posts = [p for p in posts if p["author_id"] == author_id]
# Sort by created_at descending
posts.sort(key=lambda p: p["created_at"], reverse=True)
# Apply pagination
return posts[offset:offset + limit]
@app.get("/posts/{post_id}", response_model=PostPublic)
async def get_post(post_id: int):
"""Get a specific post and increment view count"""
post = posts_db.get(post_id)
if not post:
raise HTTPException(status_code=404, detail="Post not found")
# Increment view count
post["view_count"] += 1
return post
@app.get("/posts/slug/{slug}", response_model=PostPublic)
async def get_post_by_slug(slug: str):
"""Get a post by its URL slug"""
post = next((p for p in posts_db.values() if p["slug"] == slug), None)
if not post:
raise HTTPException(status_code=404, detail=f"Post with slug '{slug}' not found")
post["view_count"] += 1
return post
@app.patch("/posts/{post_id}", response_model=PostPublic)
async def update_post(post_id: int, post_update: PostUpdate):
"""Partially update a post"""
post = posts_db.get(post_id)
if not post:
raise HTTPException(status_code=404, detail="Post not found")
update_data = post_update.model_dump(exclude_unset=True)
# Update slug if title changed
if "title" in update_data:
update_data["slug"] = generate_slug(update_data["title"])
# Set published_at if status changed to published
if "status" in update_data and update_data["status"] == PostStatus.PUBLISHED:
if not post["published_at"]:
update_data["published_at"] = datetime.utcnow()
update_data["updated_at"] = datetime.utcnow()
# Convert tags set to list if present
if "tags" in update_data:
update_data["tags"] = list(update_data["tags"])
post.update(update_data)
return post
@app.post("/posts/{post_id}/publish", response_model=PostPublic)
async def publish_post(post_id: int):
"""Action endpoint: Publish a draft post"""
post = posts_db.get(post_id)
if not post:
raise HTTPException(status_code=404, detail="Post not found")
if post["status"] == PostStatus.PUBLISHED:
raise HTTPException(status_code=400, detail="Post is already published")
post["status"] = PostStatus.PUBLISHED
post["published_at"] = datetime.utcnow()
post["updated_at"] = datetime.utcnow()
return post
@app.post("/posts/{post_id}/archive", response_model=PostPublic)
async def archive_post(post_id: int):
"""Action endpoint: Archive a post"""
post = posts_db.get(post_id)
if not post:
raise HTTPException(status_code=404, detail="Post not found")
post["status"] = PostStatus.ARCHIVED
post["updated_at"] = datetime.utcnow()
return post---
3. Nested Resources and Relationships
Example 3.1: Posts with Comments (Express.js)
Context7 Integration: Based on Express.js nested routes pattern
const express = require('express');
const app = express();
app.use(express.json());
// Databases
const postsDB = {};
const commentsDB = {};
let postIdCounter = 1;
let commentIdCounter = 1;
// ===== POSTS ENDPOINTS =====
app.post('/posts', (req, res) => {
const { title, content } = req.body;
if (!title || !content) {
return res.status(400).json({ error: 'Title and content required' });
}
const post = {
id: postIdCounter,
title,
content,
created_at: new Date().toISOString()
};
postsDB[postIdCounter] = post;
postIdCounter++;
res.status(201).json(post);
});
app.get('/posts/:postId', (req, res) => {
const post = postsDB[req.params.postId];
if (!post) {
return res.status(404).json({ error: 'Post not found' });
}
res.json(post);
});
// ===== NESTED COMMENTS ENDPOINTS =====
// List comments for a post (nested route)
app.get('/posts/:postId/comments', (req, res) => {
const post = postsDB[req.params.postId];
if (!post) {
return res.status(404).json({ error: 'Post not found' });
}
// Filter comments by post_id
const postComments = Object.values(commentsDB)
.filter(c => c.post_id === parseInt(req.params.postId))
.sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
res.json({
post_id: parseInt(req.params.postId),
comments: postComments,
count: postComments.length
});
});
// Create comment on a post (nested route)
app.post('/posts/:postId/comments', (req, res) => {
const post = postsDB[req.params.postId];
if (!post) {
return res.status(404).json({ error: 'Post not found' });
}
const { author, content, parent_comment_id = null } = req.body;
if (!author || !content) {
return res.status(400).json({ error: 'Author and content required' });
}
// Validate parent comment if provided
if (parent_comment_id) {
const parentComment = commentsDB[parent_comment_id];
if (!parentComment || parentComment.post_id !== parseInt(req.params.postId)) {
return res.status(400).json({ error: 'Invalid parent comment' });
}
}
const comment = {
id: commentIdCounter,
post_id: parseInt(req.params.postId),
parent_comment_id,
author,
content,
created_at: new Date().toISOString()
};
commentsDB[commentIdCounter] = comment;
commentIdCounter++;
res.status(201)
.location(`/posts/${req.params.postId}/comments/${comment.id}`)
.json(comment);
});
// Get specific comment (nested route)
app.get('/posts/:postId/comments/:commentId', (req, res) => {
const post = postsDB[req.params.postId];
if (!post) {
return res.status(404).json({ error: 'Post not found' });
}
const comment = commentsDB[req.params.commentId];
if (!comment || comment.post_id !== parseInt(req.params.postId)) {
return res.status(404).json({ error: 'Comment not found' });
}
res.json(comment);
});
// Update comment (nested route)
app.patch('/posts/:postId/comments/:commentId', (req, res) => {
const post = postsDB[req.params.postId];
if (!post) {
return res.status(404).json({ error: 'Post not found' });
}
const comment = commentsDB[req.params.commentId];
if (!comment || comment.post_id !== parseInt(req.params.postId)) {
return res.status(404).json({ error: 'Comment not found' });
}
// Update only content (comments are immutable otherwise)
if (req.body.content) {
comment.content = req.body.content;
comment.updated_at = new Date().toISOString();
}
res.json(comment);
});
// Delete comment (nested route)
app.delete('/posts/:postId/comments/:commentId', (req, res) => {
const post = postsDB[req.params.postId];
if (!post) {
return res.status(404).json({ error: 'Post not found' });
}
const comment = commentsDB[req.params.commentId];
if (!comment || comment.post_id !== parseInt(req.params.postId)) {
return res.status(404).json({ error: 'Comment not found' });
}
// Delete child comments first
const childComments = Object.values(commentsDB)
.filter(c => c.parent_comment_id === parseInt(req.params.commentId));
childComments.forEach(c => delete commentsDB[c.id]);
delete commentsDB[req.params.commentId];
res.status(204).send();
});
// ===== FLAT COMMENTS ENDPOINTS (Alternative Access) =====
// Get comment by ID directly (flat route)
app.get('/comments/:commentId', (req, res) => {
const comment = commentsDB[req.params.commentId];
if (!comment) {
return res.status(404).json({ error: 'Comment not found' });
}
// Include post information
const post = postsDB[comment.post_id];
res.json({
...comment,
post: {
id: post.id,
title: post.title
}
});
});
// Query comments across all posts (flat route)
app.get('/comments', (req, res) => {
const { author, post_id } = req.query;
let comments = Object.values(commentsDB);
if (author) {
comments = comments.filter(c => c.author === author);
}
if (post_id) {
comments = comments.filter(c => c.post_id === parseInt(post_id));
}
res.json({
comments,
count: comments.length
});
});
app.listen(3000, () => console.log('Server running on port 3000'));---
4. Pagination Patterns
Example 4.1: Offset-Based Pagination (FastAPI)
from fastapi import FastAPI, Query
from pydantic import BaseModel
from typing import List
from math import ceil
app = FastAPI()
class PaginatedResponse(BaseModel):
items: List[dict]
total: int
page: int
page_size: int
total_pages: int
has_previous: bool
has_next: bool
# Sample data
items_db = [{"id": i, "name": f"Item {i}"} for i in range(1, 101)]
@app.get("/items/offset", response_model=PaginatedResponse)
async def list_items_offset(
page: int = Query(1, ge=1, description="Page number (1-indexed)"),
page_size: int = Query(10, ge=1, le=100, description="Items per page")
):
"""
Offset-based pagination (traditional page/limit).
Pros: Simple, supports jumping to any page
Cons: Performance issues with large offsets
"""
total = len(items_db)
total_pages = ceil(total / page_size)
# Calculate offset
offset = (page - 1) * page_size
# Get paginated items
items = items_db[offset:offset + page_size]
return {
"items": items,
"total": total,
"page": page,
"page_size": page_size,
"total_pages": total_pages,
"has_previous": page > 1,
"has_next": page < total_pages
}Example 4.2: Cursor-Based Pagination (FastAPI)
from fastapi import FastAPI, Query
from pydantic import BaseModel
from typing import List, Optional
import base64
import json
app = FastAPI()
class CursorPaginatedResponse(BaseModel):
items: List[dict]
next_cursor: Optional[str] = None
previous_cursor: Optional[str] = None
has_more: bool
def encode_cursor(item_id: int) -> str:
"""Encode cursor to base64"""
cursor_data = {"id": item_id}
cursor_json = json.dumps(cursor_data)
return base64.b64encode(cursor_json.encode()).decode()
def decode_cursor(cursor: str) -> int:
"""Decode cursor from base64"""
cursor_json = base64.b64decode(cursor.encode()).decode()
cursor_data = json.loads(cursor_json)
return cursor_data["id"]
# Sample data (sorted by ID)
items_db = [{"id": i, "name": f"Item {i}", "created_at": f"2024-01-{i:02d}"}
for i in range(1, 101)]
@app.get("/items/cursor", response_model=CursorPaginatedResponse)
async def list_items_cursor(
cursor: Optional[str] = Query(None, description="Cursor for pagination"),
limit: int = Query(10, ge=1, le=100, description="Items per page"),
direction: str = Query("forward", regex="^(forward|backward)$")
):
"""
Cursor-based pagination (recommended for large datasets).
Pros: Efficient, consistent results, handles data changes
Cons: Can't jump to arbitrary page
Usage:
1. First request: GET /items/cursor?limit=10
2. Next page: GET /items/cursor?cursor={next_cursor}&limit=10
3. Previous page: GET /items/cursor?cursor={previous_cursor}&limit=10&direction=backward
"""
if cursor:
cursor_id = decode_cursor(cursor)
if direction == "forward":
# Get items after cursor
items = [item for item in items_db if item["id"] > cursor_id][:limit + 1]
else:
# Get items before cursor (reverse order)
items = [item for item in reversed(items_db) if item["id"] < cursor_id][:limit + 1]
items = list(reversed(items))
else:
# First page
items = items_db[:limit + 1]
# Check if there are more items
has_more = len(items) > limit
# Remove extra item
if has_more:
items = items[:limit]
# Generate cursors
next_cursor = None
previous_cursor = None
if items:
if has_more:
next_cursor = encode_cursor(items[-1]["id"])
if cursor:
previous_cursor = encode_cursor(items[0]["id"])
return {
"items": items,
"next_cursor": next_cursor,
"previous_cursor": previous_cursor,
"has_more": has_more
}Example 4.3: Keyset Pagination (Express.js)
const express = require('express');
const app = express();
// Sample data (must be sorted by keyset field)
const itemsDB = Array.from({ length: 100 }, (_, i) => ({
id: i + 1,
created_at: new Date(2024, 0, i + 1).toISOString(),
name: `Item ${i + 1}`
}));
app.get('/items/keyset', (req, res) => {
const { since_id, before_id, limit = 10 } = req.query;
const pageLimit = Math.min(parseInt(limit), 100);
let items;
if (since_id) {
// Get items after since_id (forward pagination)
const sinceIndex = itemsDB.findIndex(item => item.id === parseInt(since_id));
if (sinceIndex === -1) {
return res.status(400).json({ error: 'Invalid since_id' });
}
items = itemsDB.slice(sinceIndex + 1, sinceIndex + 1 + pageLimit + 1);
} else if (before_id) {
// Get items before before_id (backward pagination)
const beforeIndex = itemsDB.findIndex(item => item.id === parseInt(before_id));
if (beforeIndex === -1) {
return res.status(400).json({ error: 'Invalid before_id' });
}
const startIndex = Math.max(0, beforeIndex - pageLimit);
items = itemsDB.slice(startIndex, beforeIndex);
} else {
// First page
items = itemsDB.slice(0, pageLimit + 1);
}
// Check for more items
const hasMore = items.length > pageLimit;
if (hasMore) {
items = items.slice(0, pageLimit);
}
// Generate navigation links
const links = {
self: `/items/keyset?limit=${pageLimit}`
};
if (items.length > 0) {
if (hasMore) {
links.next = `/items/keyset?since_id=${items[items.length - 1].id}&limit=${pageLimit}`;
}
if (since_id || before_id) {
links.prev = `/items/keyset?before_id=${items[0].id}&limit=${pageLimit}`;
}
}
res.json({
items,
has_more: hasMore,
_links: links
});
});
app.listen(3000);---
5. Filtering and Sorting
Example 5.1: Advanced Filtering (FastAPI)
from fastapi import FastAPI, Query
from pydantic import BaseModel
from typing import List, Optional
from datetime import date
from enum import Enum
app = FastAPI()
class SortOrder(str, Enum):
ASC = "asc"
DESC = "desc"
class Product(BaseModel):
id: int
name: str
price: float
category: str
brand: str
in_stock: bool
rating: float
created_at: date
# Sample database
products_db = [
Product(id=1, name="Laptop", price=999.99, category="Electronics",
brand="TechCo", in_stock=True, rating=4.5, created_at=date(2024, 1, 1)),
Product(id=2, name="Mouse", price=29.99, category="Electronics",
brand="TechCo", in_stock=True, rating=4.2, created_at=date(2024, 1, 5)),
Product(id=3, name="Desk", price=299.99, category="Furniture",
brand="HomeStyle", in_stock=False, rating=4.0, created_at=date(2024, 1, 10)),
# ... more products
]
@app.get("/products/advanced", response_model=List[Product])
async def search_products(
# Text search
q: Optional[str] = Query(None, description="Search in name and category"),
# Exact match filters
category: Optional[str] = Query(None, description="Filter by exact category"),
brand: Optional[str] = Query(None, description="Filter by exact brand"),
in_stock: Optional[bool] = Query(None, description="Filter by stock status"),
# Range filters
min_price: Optional[float] = Query(None, ge=0, description="Minimum price"),
max_price: Optional[float] = Query(None, ge=0, description="Maximum price"),
min_rating: Optional[float] = Query(None, ge=0, le=5, description="Minimum rating"),
# Date filters
created_after: Optional[date] = Query(None, description="Created after date"),
created_before: Optional[date] = Query(None, description="Created before date"),
# Array filters
categories: Optional[List[str]] = Query(None, description="Multiple categories (OR)"),
brands: Optional[List[str]] = Query(None, description="Multiple brands (OR)"),
# Sorting
sort_by: Optional[str] = Query("created_at", regex="^(name|price|rating|created_at)$"),
sort_order: SortOrder = SortOrder.DESC,
# Pagination
limit: int = Query(10, ge=1, le=100),
offset: int = Query(0, ge=0)
):
"""
Advanced product filtering and sorting.
Examples:
- Search: ?q=laptop
- Filter by category: ?category=Electronics
- Price range: ?min_price=100&max_price=500
- Multiple categories: ?categories=Electronics&categories=Furniture
- Sort by price: ?sort_by=price&sort_order=asc
- Combine: ?category=Electronics&min_price=100&sort_by=price&sort_order=asc
"""
products = list(products_db)
# Text search
if q:
q_lower = q.lower()
products = [p for p in products if q_lower in p.name.lower() or q_lower in p.category.lower()]
# Exact match filters
if category:
products = [p for p in products if p.category == category]
if brand:
products = [p for p in products if p.brand == brand]
if in_stock is not None:
products = [p for p in products if p.in_stock == in_stock]
# Range filters
if min_price is not None:
products = [p for p in products if p.price >= min_price]
if max_price is not None:
products = [p for p in products if p.price <= max_price]
if min_rating is not None:
products = [p for p in products if p.rating >= min_rating]
# Date filters
if created_after:
products = [p for p in products if p.created_at >= created_after]
if created_before:
products = [p for p in products if p.created_at <= created_before]
# Array filters (OR logic)
if categories:
products = [p for p in products if p.category in categories]
if brands:
products = [p for p in products if p.brand in brands]
# Sorting
reverse = (sort_order == SortOrder.DESC)
products.sort(key=lambda p: getattr(p, sort_by), reverse=reverse)
# Pagination
total = len(products)
products = products[offset:offset + limit]
return products---
6. Versioning Implementations
Example 6.1: Complete URI Versioning (FastAPI)
from fastapi import FastAPI, APIRouter
from pydantic import BaseModel, EmailStr
from typing import Optional, List
app = FastAPI(title="Versioned API Example")
# ===== VERSION 1 MODELS =====
class UserV1(BaseModel):
id: int
name: str
email: str
# ===== VERSION 2 MODELS =====
class UserV2(BaseModel):
id: int
first_name: str
last_name: str
email: EmailStr
phone: Optional[str] = None
created_at: str
# ===== VERSION 1 ROUTER =====
v1_router = APIRouter(prefix="/api/v1", tags=["v1"])
@v1_router.get("/users", response_model=List[UserV1])
async def get_users_v1():
"""V1: Returns users with single 'name' field"""
return [
{"id": 1, "name": "John Doe", "email": "john@example.com"},
{"id": 2, "name": "Jane Smith", "email": "jane@example.com"}
]
@v1_router.get("/users/{user_id}", response_model=UserV1)
async def get_user_v1(user_id: int):
"""V1: Get user by ID"""
return {"id": user_id, "name": "John Doe", "email": "john@example.com"}
@v1_router.post("/users", response_model=UserV1, status_code=201)
async def create_user_v1(user: UserV1):
"""V1: Create user with simple fields"""
return user
# ===== VERSION 2 ROUTER =====
v2_router = APIRouter(prefix="/api/v2", tags=["v2"])
@v2_router.get("/users", response_model=List[UserV2])
async def get_users_v2(
limit: int = 10,
offset: int = 0
):
"""
V2: Returns users with separated first_name/last_name
Adds pagination support and phone field
"""
return [
{
"id": 1,
"first_name": "John",
"last_name": "Doe",
"email": "john@example.com",
"phone": "+1234567890",
"created_at": "2024-01-01T00:00:00Z"
},
{
"id": 2,
"first_name": "Jane",
"last_name": "Smith",
"email": "jane@example.com",
"phone": None,
"created_at": "2024-01-02T00:00:00Z"
}
][offset:offset + limit]
@v2_router.get("/users/{user_id}", response_model=UserV2)
async def get_user_v2(user_id: int):
"""V2: Get user with enhanced fields"""
return {
"id": user_id,
"first_name": "John",
"last_name": "Doe",
"email": "john@example.com",
"phone": "+1234567890",
"created_at": "2024-01-01T00:00:00Z"
}
@v2_router.post("/users", response_model=UserV2, status_code=201)
async def create_user_v2(user: UserV2):
"""V2: Create user with enhanced validation"""
return user
# Register routers
app.include_router(v1_router)
app.include_router(v2_router)
@app.get("/")
async def root():
"""API version information"""
return {
"versions": {
"v1": {
"status": "deprecated",
"sunset_date": "2025-06-30",
"docs": "/api/v1/docs"
},
"v2": {
"status": "current",
"docs": "/api/v2/docs"
}
},
"latest": "v2"
}---
7. Error Handling Patterns
Example 7.1: Comprehensive Error Handling (Express.js)
const express = require('express');
const app = express();
app.use(express.json());
// ===== CUSTOM ERROR CLASSES =====
class APIError extends Error {
constructor(statusCode, code, message, details = null) {
super(message);
this.name = this.constructor.name;
this.statusCode = statusCode;
this.code = code;
this.details = details;
this.timestamp = new Date().toISOString();
}
toJSON() {
const error = {
code: this.code,
message: this.message,
timestamp: this.timestamp
};
if (this.details) {
error.details = this.details;
}
if (process.env.NODE_ENV !== 'production') {
error.stack = this.stack;
}
return { error };
}
}
class ValidationError extends APIError {
constructor(message, details) {
super(422, 'VALIDATION_ERROR', message, details);
}
}
class NotFoundError extends APIError {
constructor(resource, resourceId) {
super(
404,
'RESOURCE_NOT_FOUND',
`${resource} not found`,
{ resource, resource_id: resourceId }
);
}
}
class ConflictError extends APIError {
constructor(message, details) {
super(409, 'CONFLICT', message, details);
}
}
class UnauthorizedError extends APIError {
constructor(message = 'Authentication required') {
super(401, 'UNAUTHORIZED', message);
}
}
class ForbiddenError extends APIError {
constructor(message = 'Insufficient permissions') {
super(403, 'FORBIDDEN', message);
}
}
class RateLimitError extends APIError {
constructor(retryAfter) {
super(
429,
'RATE_LIMIT_EXCEEDED',
'Too many requests, please try again later',
{ retry_after: retryAfter }
);
}
}
// ===== SAMPLE ROUTES WITH ERROR HANDLING =====
const usersDB = {};
let userIdCounter = 1;
app.post('/users', (req, res, next) => {
try {
const { email, username, password } = req.body;
// Validation errors
const errors = [];
if (!email || !email.includes('@')) {
errors.push({ field: 'email', message: 'Valid email required' });
}
if (!username || username.length < 3) {
errors.push({ field: 'username', message: 'Username must be at least 3 characters' });
}
if (!password || password.length < 8) {
errors.push({ field: 'password', message: 'Password must be at least 8 characters' });
}
if (errors.length > 0) {
throw new ValidationError('Invalid user data', errors);
}
// Conflict check
const existingUser = Object.values(usersDB).find(u => u.email === email);
if (existingUser) {
throw new ConflictError(
'User already exists',
{ field: 'email', value: email }
);
}
// Create user
const user = {
id: userIdCounter++,
email,
username,
password: `hashed_${password}`,
created_at: new Date().toISOString()
};
usersDB[user.id] = user;
const { password: _, ...userResponse } = user;
res.status(201).json(userResponse);
} catch (error) {
next(error);
}
});
app.get('/users/:id', (req, res, next) => {
try {
const user = usersDB[req.params.id];
if (!user) {
throw new NotFoundError('user', req.params.id);
}
const { password: _, ...userResponse } = user;
res.json(userResponse);
} catch (error) {
next(error);
}
});
app.delete('/users/:id', (req, res, next) => {
try {
const user = usersDB[req.params.id];
if (!user) {
throw new NotFoundError('user', req.params.id);
}
// Authorization check (simulated)
const currentUserId = req.headers['x-user-id'];
if (parseInt(currentUserId) !== user.id) {
throw new ForbiddenError('You can only delete your own account');
}
delete usersDB[req.params.id];
res.status(204).send();
} catch (error) {
next(error);
}
});
// ===== JSON PARSING ERROR HANDLER =====
app.use((err, req, res, next) => {
if (err instanceof SyntaxError && err.status === 400 && 'body' in err) {
return res.status(400).json({
error: {
code: 'INVALID_JSON',
message: 'Request body contains invalid JSON',
timestamp: new Date().toISOString()
}
});
}
next(err);
});
// ===== GLOBAL ERROR HANDLER (MUST BE LAST) =====
app.use((err, req, res, next) => {
// Log error
console.error('Error:', err);
// Handle known API errors
if (err instanceof APIError) {
return res.status(err.statusCode).json(err.toJSON());
}
// Handle unknown errors
const statusCode = err.statusCode || 500;
const errorResponse = {
error: {
code: 'INTERNAL_SERVER_ERROR',
message: process.env.NODE_ENV === 'production'
? 'An unexpected error occurred'
: err.message,
timestamp: new Date().toISOString()
}
};
if (process.env.NODE_ENV !== 'production') {
errorResponse.error.stack = err.stack;
}
res.status(statusCode).json(errorResponse);
});
app.listen(3000);---
8. Authentication and Authorization
Example 8.1: JWT Authentication (FastAPI)
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from pydantic import BaseModel, EmailStr
import jwt
from datetime import datetime, timedelta
from passlib.context import CryptContext
app = FastAPI()
security = HTTPBearer()
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
SECRET_KEY = "your-secret-key-change-in-production"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
# Models
class UserLogin(BaseModel):
email: EmailStr
password: str
class Token(BaseModel):
access_token: str
token_type: str
expires_in: int
class User(BaseModel):
id: int
email: str
is_active: bool
is_admin: bool
# Mock database
users_db = {
1: {
"email": "user@example.com",
"hashed_password": pwd_context.hash("password123"),
"is_active": True,
"is_admin": False
},
2: {
"email": "admin@example.com",
"hashed_password": pwd_context.hash("admin123"),
"is_active": True,
"is_admin": True
}
}
def create_access_token(data: dict, expires_delta: timedelta = None):
"""Create JWT access token"""
to_encode = data.copy()
expire = datetime.utcnow() + (expires_delta or timedelta(minutes=15))
to_encode.update({"exp": expire, "iat": datetime.utcnow()})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""Verify password against hash"""
return pwd_context.verify(plain_password, hashed_password)
def get_user_by_email(email: str):
"""Get user from database by email"""
for user_id, user_data in users_db.items():
if user_data["email"] == email:
return {"id": user_id, **user_data}
return None
async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)) -> User:
"""Dependency: Get current authenticated user"""
token = credentials.credentials
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
user_id = payload.get("sub")
if user_id is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
headers={"WWW-Authenticate": "Bearer"}
)
user_data = users_db.get(int(user_id))
if user_data is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="User not found"
)
return User(
id=int(user_id),
email=user_data["email"],
is_active=user_data["is_active"],
is_admin=user_data["is_admin"]
)
except jwt.ExpiredSignatureError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token has expired",
headers={"WWW-Authenticate": "Bearer"}
)
except jwt.JWTError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"}
)
async def require_admin(current_user: User = Depends(get_current_user)) -> User:
"""Dependency: Require admin privileges"""
if not current_user.is_admin:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Admin privileges required"
)
return current_user
# Authentication endpoints
@app.post("/auth/login", response_model=Token)
async def login(user_login: UserLogin):
"""
Login endpoint - returns JWT access token
Example:POST /auth/login { "email": "user@example.com", "password": "password123" }
"""
user = get_user_by_email(user_login.email)
if not user or not verify_password(user_login.password, user["hashed_password"]):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect email or password",
headers={"WWW-Authenticate": "Bearer"}
)
if not user["is_active"]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="User account is inactive"
)
# Create access token
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": str(user["id"])},
expires_delta=access_token_expires
)
return {
"access_token": access_token,
"token_type": "bearer",
"expires_in": ACCESS_TOKEN_EXPIRE_MINUTES * 60
}
# Protected endpoints
@app.get("/users/me", response_model=User)
async def get_current_user_profile(current_user: User = Depends(get_current_user)):
"""
Get current authenticated user's profile
Requires: Bearer token in Authorization header
"""
return current_user
@app.get("/admin/users")
async def list_all_users(admin_user: User = Depends(require_admin)):
"""
Admin-only endpoint: List all users
Requires: Admin privileges
"""
users = [
{
"id": user_id,
"email": user_data["email"],
"is_active": user_data["is_active"],
"is_admin": user_data["is_admin"]
}
for user_id, user_data in users_db.items()
]
return {"users": users}
@app.post("/admin/users/{user_id}/deactivate")
async def deactivate_user(user_id: int, admin_user: User = Depends(require_admin)):
"""Admin-only: Deactivate a user account"""
if user_id not in users_db:
raise HTTPException(status_code=404, detail="User not found")
users_db[user_id]["is_active"] = False
return {"message": f"User {user_id} deactivated"}This is the first half of the EXAMPLES.md file. The file is getting quite long, so I'll continue with more examples...
---
9. HATEOAS Implementation
Example 9.1: Hypermedia API (Express.js)
const express = require('express');
const app = express();
app.use(express.json());
const ordersDB = {
1: { id: 1, customer: 'John Doe', total: 150.00, status: 'pending', items: [1, 2] },
2: { id: 2, customer: 'Jane Smith', total: 85.50, status: 'shipped', items: [3] }
};
function buildOrderLinks(order, baseUrl) {
const links = {
self: { href: `${baseUrl}/orders/${order.id}`, method: 'GET' }
};
// State-based links
if (order.status === 'pending') {
links.cancel = { href: `${baseUrl}/orders/${order.id}/cancel`, method: 'POST' };
links.pay = { href: `${baseUrl}/orders/${order.id}/pay`, method: 'POST' };
}
if (order.status === 'paid') {
links.ship = { href: `${baseUrl}/orders/${order.id}/ship`, method: 'POST' };
}
if (order.status === 'shipped') {
links.track = { href: `${baseUrl}/orders/${order.id}/tracking`, method: 'GET' };
}
// Always available
links.items = { href: `${baseUrl}/orders/${order.id}/items`, method: 'GET' };
links.customer = { href: `${baseUrl}/customers/${order.customer}`, method: 'GET' };
return links;
}
app.get('/orders/:id', (req, res) => {
const order = ordersDB[req.params.id];
if (!order) {
return res.status(404).json({ error: 'Order not found' });
}
const baseUrl = `${req.protocol}://${req.get('host')}`;
res.json({
...order,
_links: buildOrderLinks(order, baseUrl)
});
});
app.post('/orders/:id/pay', (req, res) => {
const order = ordersDB[req.params.id];
if (!order) {
return res.status(404).json({ error: 'Order not found' });
}
if (order.status !== 'pending') {
return res.status(400).json({
error: `Cannot pay for order with status: ${order.status}`
});
}
order.status = 'paid';
const baseUrl = `${req.protocol}://${req.get('host')}`;
res.json({
...order,
_links: buildOrderLinks(order, baseUrl)
});
});
app.listen(3000);---
10. Performance Optimization
Example 10.1: Caching with ETags (FastAPI)
from fastapi import FastAPI, Request, Response
from fastapi.responses import JSONResponse
import hashlib
import json
app = FastAPI()
# Simulated database
products_db = {
1: {"id": 1, "name": "Product 1", "price": 99.99, "updated_at": "2024-01-01"}
}
def generate_etag(data: dict) -> str:
"""Generate ETag from response data"""
content = json.dumps(data, sort_keys=True).encode('utf-8')
return f'"{hashlib.md5(content).hexdigest()}"'
@app.get("/products/{product_id}")
async def get_product_cached(product_id: int, request: Request):
"""
Get product with ETag caching support
Client workflow:
1. First request: GET /products/1
Response: Product data with ETag header
2. Subsequent requests: GET /products/1 with If-None-Match: "{etag}"
Response: 304 Not Modified (if unchanged) or 200 with new data
"""
product = products_db.get(product_id)
if not product:
raise HTTPException(status_code=404, detail="Product not found")
# Generate ETag
etag = generate_etag(product)
# Check If-None-Match header
if_none_match = request.headers.get("if-none-match")
if if_none_match == etag:
return Response(status_code=304) # Not Modified
# Return with ETag and Cache-Control headers
return JSONResponse(
content=product,
headers={
"ETag": etag,
"Cache-Control": "max-age=300, must-revalidate" # Cache for 5 minutes
}
)
@app.get("/products")
async def list_products_cached(request: Request):
"""List products with aggressive caching"""
products = list(products_db.values())
etag = generate_etag(products)
if_none_match = request.headers.get("if-none-match")
if if_none_match == etag:
return Response(status_code=304)
return JSONResponse(
content={"products": products},
headers={
"ETag": etag,
"Cache-Control": "public, max-age=600" # Public cache for 10 minutes
}
)---
This concludes the first 10 sections with 15+ examples. The file continues below with additional advanced patterns...
11. Bulk Operations
Example 11.1: Bulk Create with Transaction Support (Express.js)
app.post('/users/bulk', async (req, res) => {
const { users } = req.body;
if (!Array.isArray(users) || users.length === 0) {
return res.status(400).json({
error: 'Request body must contain a non-empty array of users'
});
}
const results = {
created: [],
errors: []
};
for (let i = 0; i < users.length; i++) {
const user = users[i];
try {
// Validate
if (!user.email || !user.username) {
throw new Error('Missing required fields');
}
// Create user
const newUser = await createUserInDB(user);
results.created.push({ index: i, user: newUser });
} catch (error) {
results.errors.push({
index: i,
user: user,
error: error.message
});
}
}
const statusCode = results.errors.length === 0 ? 201 : 207; // 207 Multi-Status
res.status(statusCode).json(results);
});---
12. File Upload Patterns
Example 12.1: File Upload with Validation (FastAPI)
from fastapi import FastAPI, File, UploadFile, HTTPException
from typing import List
import shutil
from pathlib import Path
app = FastAPI()
UPLOAD_DIR = Path("uploads")
UPLOAD_DIR.mkdir(exist_ok=True)
MAX_FILE_SIZE = 10 * 1024 * 1024 # 10MB
ALLOWED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".pdf"}
@app.post("/upload")
async def upload_file(file: UploadFile = File(...)):
"""
Upload a single file with validation
- Max size: 10MB
- Allowed types: JPG, JPEG, PNG, PDF
"""
# Check file extension
file_ext = Path(file.filename).suffix.lower()
if file_ext not in ALLOWED_EXTENSIONS:
raise HTTPException(
status_code=400,
detail=f"File type {file_ext} not allowed. Allowed types: {ALLOWED_EXTENSIONS}"
)
# Check file size
file.file.seek(0, 2) # Seek to end
file_size = file.file.tell()
file.file.seek(0) # Reset to beginning
if file_size > MAX_FILE_SIZE:
raise HTTPException(
status_code=400,
detail=f"File size {file_size} exceeds maximum {MAX_FILE_SIZE} bytes"
)
# Save file
file_path = UPLOAD_DIR / file.filename
with file_path.open("wb") as buffer:
shutil.copyfileobj(file.file, buffer)
return {
"filename": file.filename,
"size": file_size,
"path": str(file_path)
}
@app.post("/upload/multiple")
async def upload_multiple_files(files: List[UploadFile] = File(...)):
"""Upload multiple files"""
results = []
for file in files:
file_path = UPLOAD_DIR / file.filename
with file_path.open("wb") as buffer:
shutil.copyfileobj(file.file, buffer)
results.append({
"filename": file.filename,
"path": str(file_path)
})
return {"files": results}---
13. Search and Full-Text Queries
Example 13.1: Advanced Search API (FastAPI)
from fastapi import FastAPI, Query
from typing import List, Optional
from enum import Enum
app = FastAPI()
class SearchField(str, Enum):
TITLE = "title"
CONTENT = "content"
TAGS = "tags"
ALL = "all"
@app.get("/search")
async def search(
q: str = Query(..., min_length=1, description="Search query"),
field: SearchField = SearchField.ALL,
limit: int = Query(10, ge=1, le=100),
offset: int = 0
):
"""
Full-text search across resources
Examples:
- Search all fields: GET /search?q=fastapi
- Search title only: GET /search?q=rest&field=title
- Search with quotes: GET /search?q="api design"
"""
# Implement full-text search logic here
results = perform_search(q, field, limit, offset)
return {
"query": q,
"field": field,
"results": results,
"total": len(results)
}---
14. Real-Time Updates
Example 14.1: Server-Sent Events (FastAPI)
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import asyncio
import json
app = FastAPI()
@app.get("/events")
async def stream_events():
"""
Server-Sent Events endpoint for real-time updates
Client usage:const eventSource = new EventSource('/events'); eventSource.onmessage = (event) => { console.log('New data:', JSON.parse(event.data)); };
"""
async def event_generator():
while True:
# Simulate getting new data
data = {"timestamp": datetime.now().isoformat(), "value": random.randint(1, 100)}
yield f"data: {json.dumps(data)}\n\n"
await asyncio.sleep(1)
return StreamingResponse(
event_generator(),
media_type="text/event-stream"
)---
15. API Documentation
Example 15.1: Rich OpenAPI Documentation (FastAPI)
from fastapi import FastAPI, Path, Query
from pydantic import BaseModel, Field
app = FastAPI(
title="My API",
description="""
## Features
- User management
- Product catalog
- Order processing
## Authentication
Use Bearer token in Authorization header
""",
version="1.0.0",
terms_of_service="http://example.com/terms/",
contact={
"name": "API Support",
"email": "support@example.com"
},
license_info={
"name": "MIT",
"url": "https://opensource.org/licenses/MIT"
}
)
class Product(BaseModel):
name: str = Field(..., example="Laptop", description="Product name")
price: float = Field(..., example=999.99, gt=0, description="Price in USD")
description: str = Field(None, example="High-performance laptop")
@app.post(
"/products",
response_model=Product,
status_code=201,
summary="Create a new product",
description="Create a new product in the catalog with name and price",
response_description="The created product with generated ID",
tags=["products"]
)
async def create_product(product: Product):
"""
Create a product with detailed information:
- **name**: Product name (required)
- **price**: Price in USD, must be positive (required)
- **description**: Optional product description
"""
return product---
End of Examples Document
This comprehensive examples file includes 25+ real-world patterns covering all major aspects of REST API design with both FastAPI and Express.js implementations.
REST API Design Patterns
Comprehensive guide for designing, implementing, and maintaining world-class RESTful APIs
Overview
This skill provides a complete framework for building RESTful APIs following industry best practices. Whether you're designing a new API from scratch or refactoring an existing one, this guide covers everything from resource modeling to performance optimization.
What You'll Learn
Core REST Concepts
- REST architectural principles and maturity levels
- Resource-based design thinking
- Stateless communication patterns
- Uniform interface constraints
- Client-server separation
Resource Modeling
- Naming conventions and URL structure
- Collection vs. individual resource patterns
- Nested resources and relationships
- Resource hierarchies and depth limits
- Action-based endpoints for non-CRUD operations
HTTP Methods Mastery
- GET: Safe, idempotent retrieval operations
- POST: Resource creation and non-idempotent actions
- PUT: Full resource replacement (idempotent)
- PATCH: Partial updates with granular control
- DELETE: Resource removal patterns
- OPTIONS: CORS and capability discovery
- HEAD: Metadata retrieval without body
API Versioning Strategies
1. URI Versioning (e.g., /api/v1/users) - Most common 2. Header Versioning (e.g., X-API-Version: 2.0) - Clean URIs 3. Content Negotiation (e.g., Accept: application/vnd.api.v2+json) - RESTful 4. Query Parameter Versioning (e.g., /users?version=2) - Simple but limited
Pagination Patterns
- Offset-based: Traditional page/limit approach
- Cursor-based: Efficient for large datasets, prevents missed records
- Page-based: User-friendly numbered pages
- Keyset: Performance-optimized for ordered data
Filtering and Sorting
- Query parameter conventions
- Complex filtering patterns
- Multi-field sorting
- Full-text search integration
- Faceted filtering for rich UIs
Error Handling
- Consistent error response formats
- HTTP status code selection guide
- Validation error structures
- Error codes and debugging information
- User-friendly vs. developer-friendly messages
HATEOAS and Hypermedia
- Link relations and affordances
- Self-documenting APIs
- Dynamic navigation
- Discoverability patterns
- HAL, JSON:API, and other formats
Performance Optimization
- Caching strategies (ETags, Cache-Control)
- Response compression (gzip, brotli)
- Database query optimization
- Connection pooling
- Rate limiting patterns
- CDN integration
Security Best Practices
- Authentication patterns (JWT, OAuth 2.0, API keys)
- Authorization and RBAC
- Input validation and sanitization
- CORS configuration
- Security headers
- Rate limiting and DDoS prevention
- Secrets management
API Documentation
- OpenAPI/Swagger specifications
- Interactive documentation (Swagger UI, ReDoc)
- Code examples and SDKs
- Versioning documentation
- Changelog maintenance
Quick Reference
REST Design Checklist
Resource Design:
✓ Use plural nouns for collections (/users, /products)
✓ Use lowercase with hyphens (/user-profiles)
✓ Keep nesting to 2-3 levels maximum
✓ Use query parameters for filtering/sorting
✓ Implement proper pagination
HTTP Methods:
✓ GET for retrieval (safe, idempotent, cacheable)
✓ POST for creation (not idempotent)
✓ PUT for full replacement (idempotent)
✓ PATCH for partial updates (idempotent)
✓ DELETE for removal (idempotent)
Status Codes:
✓ 200 OK for successful GET/PUT/PATCH
✓ 201 Created for successful POST
✓ 204 No Content for successful DELETE
✓ 400 Bad Request for client errors
✓ 401 Unauthorized for authentication required
✓ 403 Forbidden for authorization failures
✓ 404 Not Found for missing resources
✓ 422 Unprocessable Entity for validation errors
✓ 429 Too Many Requests for rate limiting
✓ 500 Internal Server Error for server issues
Versioning:
✓ Choose one strategy and stick to it
✓ Document version lifecycle
✓ Support multiple versions temporarily
✓ Deprecate gracefully with warnings
✓ Sunset old versions with notice
Security:
✓ Always use HTTPS in production
✓ Implement authentication
✓ Validate all inputs
✓ Sanitize all outputs
✓ Use rate limiting
✓ Enable CORS carefully
✓ Log security events
Performance:
✓ Implement caching (ETags, Cache-Control)
✓ Use compression for large responses
✓ Paginate collections
✓ Optimize database queries
✓ Use async I/O
✓ Monitor performance metrics
Documentation:
✓ Generate OpenAPI specs
✓ Provide interactive docs
✓ Include code examples
✓ Document error responses
✓ Keep changelog updatedDesign Decision Framework
When to Use Each HTTP Method
GET - Retrieval
Use when: Fetching data without side effects
Examples:
- List all users: GET /users
- Get user by ID: GET /users/123
- Search products: GET /products?q=laptop
Safe: Yes | Idempotent: Yes | Cacheable: YesPOST - Creation or Actions
Use when: Creating new resources or triggering actions
Examples:
- Create user: POST /users
- Login: POST /auth/login
- Process payment: POST /payments/123/process
Safe: No | Idempotent: No | Cacheable: NoPUT - Full Replacement
Use when: Replacing entire resource
Examples:
- Update all user fields: PUT /users/123
- Replace configuration: PUT /settings
Requires: All fields in request body
Safe: No | Idempotent: Yes | Cacheable: NoPATCH - Partial Update
Use when: Updating specific fields
Examples:
- Update email only: PATCH /users/123 {"email": "new@example.com"}
- Change status: PATCH /orders/456 {"status": "shipped"}
Requires: Only fields to update
Safe: No | Idempotent: Yes | Cacheable: NoDELETE - Removal
Use when: Removing resources
Examples:
- Delete user: DELETE /users/123
- Cancel subscription: DELETE /subscriptions/789
Safe: No | Idempotent: Yes | Cacheable: NoWhen to Use Nested Resources
Use Nested Routes When:
- Strong ownership relationship exists (comments belong to posts)
- Parent context is always required
- Nesting is 2-3 levels maximum
- Example:
GET /posts/42/comments
Use Flat Routes When:
- Resources can exist independently
- You need to query across parents
- Resource has multiple parents
- Example:
GET /comments?post_id=42&user_id=5
Hybrid Approach:
# Create comment on post (nested)
POST /posts/42/comments
# Get comments across all posts (flat)
GET /comments?user_id=5
# Get specific comment (flat - if you have ID)
GET /comments/123Versioning Strategy Selection
Choose URI Versioning if:
- You want explicit, visible versions
- You need different routing logic
- You're building a public API
- You want browser-friendly testing
- Example:
/api/v1/users,/api/v2/users
Choose Header Versioning if:
- You want clean, unchanging URIs
- You're building an internal API
- You understand content negotiation
- Example:
X-API-Version: 2.0
Choose Content Negotiation if:
- You're a REST purist
- You understand Accept headers well
- You want maximum RESTfulness
- Example:
Accept: application/vnd.api.v2+json
Pagination Strategy Selection
Offset-Based (Traditional)
Best for: Small to medium datasets, user-facing pages
Pros: Simple, supports jumping to any page
Cons: Performance issues with large offsets, inconsistent with data changes
Example: GET /items?limit=10&offset=20Cursor-Based (Recommended)
Best for: Large datasets, real-time data, feeds
Pros: Consistent results, efficient, handles data changes
Cons: Can't jump to arbitrary page
Example: GET /items?limit=10&cursor=eyJpZCI6MTIzfQ==Page-Based
Best for: User interfaces with page numbers
Pros: User-friendly, intuitive
Cons: Same issues as offset-based
Example: GET /items?page=3&page_size=10Keyset Pagination
Best for: Performance-critical applications
Pros: Best performance, consistent
Cons: Requires indexed column, complex implementation
Example: GET /items?limit=10&since_id=123Common Patterns
Resource Filtering
Single field:
GET /products?category=electronics
Multiple fields:
GET /products?category=electronics&min_price=100&max_price=500
Multiple values (OR):
GET /products?tags=wireless,bluetooth
Range queries:
GET /events?start_date=2024-01-01&end_date=2024-12-31
Text search:
GET /articles?q=machine+learning
Negation:
GET /users?status!=inactive
Complex queries (JSON):
GET /products?filter={"category": "electronics", "price": {"$gte": 100}}Resource Sorting
Single field:
GET /products?sort=price
Descending:
GET /products?sort=-price
Multiple fields:
GET /products?sort=category,price
Mixed order:
GET /products?sort=category,-price
Query parameter style:
GET /products?sort_by=price&order=descBulk Operations
Bulk create:
POST /users/bulk
Body: [{"name": "User 1"}, {"name": "User 2"}]
Bulk update:
PATCH /users/bulk
Body: [{"id": 1, "status": "active"}, {"id": 2, "status": "inactive"}]
Bulk delete:
DELETE /users?ids=1,2,3,4
Batch processing:
POST /jobs/batch
Body: {"operations": [{"action": "create", "resource": "user", "data": {...}}]}Action Endpoints
For operations that don't fit CRUD:
User actions:
POST /users/123/activate
POST /users/123/deactivate
POST /users/123/reset-password
Order actions:
POST /orders/456/cancel
POST /orders/456/refund
POST /orders/456/ship
Document actions:
POST /documents/789/publish
POST /documents/789/archive
POST /documents/789/duplicateFramework-Specific Examples
FastAPI Advantages
- Automatic OpenAPI documentation
- Pydantic data validation
- Type hints for better IDE support
- Async/await support out of the box
- Dependency injection system
Express.js Advantages
- Mature ecosystem with extensive middleware
- Flexible and unopinionated
- Large community and resources
- Easy to integrate with existing Node.js apps
- Great for microservices
Integration Patterns
Database Integration
Connection Pooling:
# FastAPI with asyncpg
from databases import Database
database = Database("postgresql://user:pass@localhost/db")
@app.on_event("startup")
async def startup():
await database.connect()
@app.on_event("shutdown")
async def shutdown():
await database.disconnect()Authentication Integration
JWT with Refresh Tokens:
POST /auth/login
→ Returns: access_token (15 min), refresh_token (7 days)
GET /api/resource
→ Header: Authorization: Bearer {access_token}
POST /auth/refresh
→ Body: {refresh_token}
→ Returns: new access_tokenThird-Party API Integration
Webhook Patterns:
Register webhook:
POST /webhooks
Body: {"url": "https://example.com/webhook", "events": ["user.created"]}
Webhook delivery:
POST https://example.com/webhook
Headers: X-Webhook-Signature: {hmac_signature}
Body: {"event": "user.created", "data": {...}}
Verify webhooks:
GET /webhooks/{id}
DELETE /webhooks/{id}When to Use This Skill
Perfect For
- Building new RESTful APIs from scratch
- Refactoring legacy APIs for better design
- Standardizing API patterns across teams
- Onboarding new developers to REST principles
- API design reviews and audits
- Creating API style guides
- Microservices architecture
- Mobile and web backends
- Third-party integrations
Also Useful For
- Understanding API best practices
- Preparing for technical interviews
- Learning HTTP protocol deeply
- Designing consistent error handling
- Implementing security patterns
- Optimizing API performance
- Creating API documentation
- Building developer-friendly APIs
Resources and Further Reading
Official Specifications
- RFC 7231 (HTTP/1.1 Semantics)
- RFC 6749 (OAuth 2.0)
- OpenAPI Specification
- JSON Schema
Books
- "RESTful Web APIs" by Leonard Richardson & Mike Amundsen
- "REST API Design Rulebook" by Mark Masse
- "Building Microservices" by Sam Newman
Online Resources
- FastAPI Documentation: https://fastapi.tiangolo.com
- Express.js Guide: https://expressjs.com/en/guide/routing.html
- MDN HTTP Documentation: https://developer.mozilla.org/en-US/docs/Web/HTTP
- REST API Tutorial: https://restfulapi.net
Contributing
This skill is continuously updated with new patterns, examples, and best practices from real-world API development.
---
Version: 1.0.0 Last Updated: October 2025 Maintained By: Claude Code Skills Team
Related skills
How it compares
Use rest-api-design-patterns when defining HTTP resource contracts; use framework-specific deployment skills when the task is only cloud hosting rather than API shape and semantics.
FAQ
What REST maturity levels does rest-api-design-patterns explain?
The rest-api-design-patterns skill walks Richardson levels 0–3 from single-endpoint POST through resource URIs, proper HTTP verbs, to HATEOAS hypermedia controls with _links in responses.
Which frameworks does rest-api-design-patterns reference?
The rest-api-design-patterns skill includes implementation patterns for FastAPI and Express.js while focusing on framework-agnostic HTTP resource design, pagination, filtering, and OpenAPI documentation practices.
How deep should nested REST resources be?
The rest-api-design-patterns skill recommends hierarchy like /users/123/posts/456 but warns against chains deeper than 2–3 levels, suggesting flatter resources or query parameters for deeper relationships.