rest-api-design
REST API Design
Master REST API design principles to build intuitive, scalable, and developer-friendly APIs that stand the test of time.
When to Use This Skill
- Designing new REST APIs or endpoints
- Refactoring existing APIs for better usability
- Establishing REST design standards for your team
- Reviewing REST API specifications before implementation
- Implementing pagination, filtering, and searching
- Designing error handling and status code strategies
- Planning API versioning and deprecation strategies
Core REST Principles
Resource-Oriented Architecture
REST APIs should be resource-oriented, not action-oriented:
- Resources are nouns (users, orders, products), not verbs
- Use HTTP methods for actions (GET, POST, PUT, PATCH, DELETE)
- URLs represent resource hierarchies
- Consistent naming conventions across endpoints
Good patterns:
GET /api/users # List users
POST /api/users # Create user
GET /api/users/{id} # Get specific user
PUT /api/users/{id} # Replace user
PATCH /api/users/{id} # Update user fields
DELETE /api/users/{id} # Delete user
# Nested resources (shallow)
GET /api/users/{id}/orders # Get user's orders
POST /api/users/{id}/orders # Create order for user
Bad patterns (avoid):
POST /api/createUser
POST /api/getUserById
POST /api/deleteUser
GET /api/user (inconsistent singular)
HTTP Methods Semantics
Each HTTP method has specific semantics that must be respected:
- GET: Retrieve resources (idempotent, safe, cacheable)
- POST: Create new resources or trigger actions (not idempotent)
- PUT: Replace entire resource (idempotent)
- PATCH: Partial resource updates (not always idempotent)
- DELETE: Remove resources (idempotent)
Idempotency: GET, PUT, DELETE must be idempotent (same result when called multiple times)
URL Structure Best Practices
Resource Naming:
- Use plural nouns:
/api/usersnot/api/user - Use lowercase:
/api/usersnot/api/Users - Use hyphens for multi-word names:
/api/user-profilesnot/api/userProfiles
Nested Resources (Shallow Preferred):
# Shallow nesting (preferred) - easy to understand
GET /api/users/{id}/orders
# Deep nesting (avoid) - hard to route and query
GET /api/users/{id}/orders/{orderId}/items/{itemId}/reviews
# Better approach for deep hierarchies:
GET /api/order-items/{id}/reviews
HTTP Status Codes
Use status codes correctly to communicate request outcomes:
2xx Success
200 OK- Successful GET, PATCH, PUT201 Created- Successful POST (include Location header)204 No Content- Successful DELETE or empty response
4xx Client Errors
400 Bad Request- Malformed request syntax401 Unauthorized- Authentication required403 Forbidden- Authenticated but not authorized404 Not Found- Resource doesn't exist409 Conflict- State conflict (duplicate email, etc.)422 Unprocessable Entity- Validation errors
5xx Server Errors
500 Internal Server Error- Server error503 Service Unavailable- Temporary downtime
Rate Limiting
429 Too Many Requests- Rate limit exceeded
Resource Collection Patterns
Standard CRUD Operations
# List collections
GET /api/users?page=1&limit=20
→ 200 OK
→ Returns: [user1, user2, ...]
# Get specific resource
GET /api/users/{id}
→ 200 OK or 404 Not Found
→ Returns: {id, name, email, ...}
# Create new resource
POST /api/users
Body: {"name": "John", "email": "john@example.com"}
→ 201 Created
→ Location: /api/users/123
→ Returns: {id: "123", name: "John", ...}
# Update entire resource
PUT /api/users/{id}
Body: {complete user object with ALL fields}
→ 200 OK or 404 Not Found
# Partial update
PATCH /api/users/{id}
Body: {"name": "Jane"} (only changed fields)
→ 200 OK or 404 Not Found
# Delete resource
DELETE /api/users/{id}
→ 204 No Content or 404 Not Found
Pagination Strategies
Always paginate large collections. Three main approaches:
1. Offset-Based Pagination
Best for: Small datasets, traditional pagination UI
GET /api/users?page=2&page_size=20
Response:
{
"items": [...],
"page": 2,
"page_size": 20,
"total": 150,
"pages": 8,
"has_next": true,
"has_prev": true
}
2. Cursor-Based Pagination
Best for: Large datasets, real-time data, consistent results
GET /api/users?limit=20&cursor=eyJpZCI6MTIzfQ
Response:
{
"items": [...],
"next_cursor": "eyJpZCI6MTQzfQ",
"prev_cursor": "eyJpZCI6MTA3fQ",
"has_more": true
}
3. Link Header Pagination
Most RESTful approach using HTTP Link header:
GET /api/users?page=2
Response Headers:
Link: <https://api.example.com/users?page=3>; rel="next",
<https://api.example.com/users?page=1>; rel="prev",
<https://api.example.com/users?page=1>; rel="first",
<https://api.example.com/users?page=8>; rel="last"
Implementation pattern (FastAPI):
from fastapi import FastAPI, Query
from typing import Optional
@app.get("/api/users")
async def list_users(
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100)
):
offset = (page - 1) * page_size
total = await count_users()
users = await fetch_users(limit=page_size, offset=offset)
return {
"items": users,
"total": total,
"page": page,
"page_size": page_size,
"pages": (total + page_size - 1) // page_size
}
Filtering, Sorting, and Searching
Query Parameters
Filtering:
GET /api/users?status=active
GET /api/users?role=admin&status=active
Sorting:
GET /api/users?sort=created_at
GET /api/users?sort=-created_at # descending
GET /api/users?sort=name,created_at # multiple fields
Searching:
GET /api/users?search=john
GET /api/users?q=john
Field Selection (Sparse Fieldsets):
GET /api/users?fields=id,name,email
Error Response Format
Standardize error responses for consistency:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": [
{
"field": "email",
"message": "Invalid email format",
"value": "not-an-email"
}
],
"timestamp": "2025-10-16T12:00:00Z",
"path": "/api/users"
}
}
API Versioning
Plan for breaking changes from day one:
URL Versioning (Recommended)
Clear and easy to route:
/api/v1/users
/api/v2/users
Header Versioning
Clean URLs but less visible:
GET /api/users
Accept: application/vnd.api+json; version=2
Query Parameter Versioning
Easy to test but easy to forget:
GET /api/users?version=2
Security Patterns
Authentication & Authorization
Bearer Token (JWT):
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
401 Unauthorized - Missing/invalid token
403 Forbidden - Valid token, insufficient permissions
API Keys:
X-API-Key: your-api-key-here
Rate Limiting
Protect APIs from abuse:
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 742
X-RateLimit-Reset: 1640000000
Response when limited:
429 Too Many Requests
Retry-After: 3600
CORS Configuration
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["https://example.com"],
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
allow_headers=["*"],
)
Advanced Patterns
Idempotency
For non-idempotent operations (POST), use idempotency keys:
POST /api/orders
Idempotency-Key: unique-key-123
If duplicate request: → 200 OK (return cached response)
Bulk Operations
POST /api/users/batch
{
"items": [
{"name": "User1", "email": "user1@example.com"},
{"name": "User2", "email": "user2@example.com"}
]
}
Response:
{
"results": [
{"id": "1", "status": "created"},
{"id": null, "status": "failed", "error": "Email already exists"}
]
}
HATEOAS (Hypermedia Links)
Include links for related resources:
{
"id": "123",
"name": "John",
"email": "john@example.com",
"_links": {
"self": {"href": "/api/users/123"},
"orders": {"href": "/api/users/123/orders"},
"update": {"href": "/api/users/123", "method": "PATCH"},
"delete": {"href": "/api/users/123", "method": "DELETE"}
}
}
Caching
Cache Headers:
# Client caching
Cache-Control: public, max-age=3600
# No caching
Cache-Control: no-cache, no-store, must-revalidate
# Conditional requests
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
→ 304 Not Modified
Documentation
OpenAPI/Swagger
Generate interactive API documentation:
from fastapi import FastAPI, Path
app = FastAPI(
title="My API",
description="API for managing users",
version="1.0.0",
docs_url="/docs",
redoc_url="/redoc"
)
@app.get(
"/api/users/{user_id}",
summary="Get user by ID",
response_description="User details",
tags=["Users"]
)
async def get_user(
user_id: str = Path(..., description="The user ID")
):
"""
Retrieve user by ID.
Returns full user profile including:
- Basic information
- Contact details
- Account status
"""
pass
Best Practices Summary
- Resource-Oriented: Use nouns for endpoints, verbs for HTTP methods
- Stateless: Each request contains all necessary information
- HTTP Semantics: Respect GET/POST/PUT/PATCH/DELETE meanings
- Status Codes: Use correct codes (2xx, 4xx, 5xx appropriately)
- Pagination: Always paginate large collections
- Versioning: Plan for breaking changes from day one
- Documentation: Use OpenAPI for interactive docs
- Error Handling: Standardize error response format
- Security: Implement authentication, authorization, rate limiting
- Consistency: Maintain consistent naming and structure
Common Pitfalls to Avoid
- Using verbs in endpoints:
/api/createUser(wrong) - Inconsistent naming: Mixing
/usersand/user - Ignoring HTTP semantics: POST for idempotent operations
- Missing status codes: Not using 201 for creation
- Poor pagination: Returning all results
- No rate limiting: APIs vulnerable to abuse
- Tight coupling: API structure mirrors database schema
- Undocumented APIs: No OpenAPI/documentation
Cross-Skill References
- graphql-api-design skill - For comparison and GraphQL alternatives
- api-architecture skill - For versioning strategies, security, and monitoring
- api-testing skill - For testing REST endpoints and validation
Additional Resources
For detailed guidance, see:
references/http-methods-guide.md- Comprehensive HTTP method semanticsreferences/pagination-patterns.md- Comparison of pagination approachesreferences/rest-error-handling.md- Error handling standards