REST API Design Guide
Designing a REST API that developers actually enjoy using is harder than it looks. This guide gives you battle-tested patterns for resource naming, HTTP method semantics, versioning strategies, pagination, filtering, sorting, error responses, and security — backed by OpenAPI 3.1 specs you can copy directly into your project. No hand-wavy theory. Every recommendation comes with a concrete code example and the reasoning behind the decision.
Key Features
- Resource Naming Conventions — Rules and anti-patterns for URL structure with 30+ real examples covering nested resources, actions, and bulk operations
- HTTP Method Semantics — Precise guidance on when to use POST vs PUT vs PATCH, idempotency guarantees, and correct status codes for each method
-
Three Versioning Strategies — URL path (
/v2/users), header (Accept: application/vnd.acme.v2+json), and query param (?version=2) with trade-off analysis - Cursor & Offset Pagination — Complete implementations of both patterns with performance analysis (why cursors win at scale)
-
Filtering, Sorting & Field Selection — Query parameter conventions:
?status=active&sort=-created_at&fields=id,name,email - Error Response Standard — RFC 7807 Problem Details format with error codes, validation details, and machine-readable types
- OpenAPI 3.1 Spec Templates — Copy-paste-ready spec files demonstrating every pattern in the guide
Quick Start
- Start with the OpenAPI spec template:
# templates/openapi-template.yaml
openapi: 3.1.0
info:
title: Acme Corp API
version: 2.0.0
servers:
- url: https://api.example.com/v2
paths:
/users:
get:
summary: List users with pagination and filtering
operationId: listUsers
parameters:
- $ref: '#/components/parameters/PageCursor'
- $ref: '#/components/parameters/PageSize'
- name: status
in: query
schema:
type: string
enum: [active, inactive, suspended]
responses:
'200':
description: Paginated list of users
content:
application/json:
schema:
$ref: '#/components/schemas/UserListResponse'
'400':
$ref: '#/components/responses/BadRequest'
- Review the error response pattern:
# src/error_responses.py — FastAPI implementation
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse
app = FastAPI()
@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
"""RFC 7807 Problem Details error format."""
return JSONResponse(
status_code=exc.status_code,
content={
"type": f"https://api.example.com/errors/{exc.status_code}",
"title": exc.detail if isinstance(exc.detail, str) else "Error",
"status": exc.status_code,
"detail": "See 'errors' for specifics.",
"instance": str(request.url),
"errors": exc.detail if isinstance(exc.detail, list) else []
},
media_type="application/problem+json"
)
How It Works
This guide is structured around the API request lifecycle: URL Design -> Method Semantics -> Query Parameters -> Request Validation -> Processing -> Status Codes -> Error Bodies -> Pagination Metadata. Each section maps to one stage. The OpenAPI specs in templates/ codify every recommendation.
Usage Examples
Pagination: Cursor vs Offset
# Offset-based (simple but breaks at scale)
# GET /users?page=5&per_page=20
# Problem: INSERT/DELETE between page fetches causes skipped/duplicated rows
# Cursor-based (stable, performant)
# GET /users?after=eyJpZCI6MTAwfQ&limit=20
# Response format for cursor pagination:
{
"data": [
{"id": 101, "name": "Jane Doe", "email": "user@example.com"},
{"id": 102, "name": "John Smith", "email": "john@example.com"}
],
"pagination": {
"next_cursor": "eyJpZCI6MTIwfQ",
"has_more": true,
"limit": 20
},
"links": {
"next": "/v2/users?after=eyJpZCI6MTIwfQ&limit=20",
"self": "/v2/users?after=eyJpZCI6MTAwfQ&limit=20"
}
}
Filtering, Sorting & Versioning
# Filter + sort + field selection
GET /v2/users?status=active&role=admin&sort=-created_at&fields=id,name,email&after=abc123&limit=50
# Versioning: URL path (recommended) | Accept header | Query param
GET /v2/users/42
GET /users/42 # with Accept: application/vnd.acme.v2+json
GET /users/42?version=2
Configuration
The guide includes configuration templates for common frameworks:
| Framework | Config File | Key Settings |
|---|---|---|
| FastAPI | examples/fastapi_config.py |
Error handlers, CORS, pagination defaults |
| Flask | examples/flask_config.py |
Blueprints, error handlers, request parsing |
| Express | examples/express_config.js |
Middleware chain, error handler, rate limits |
| OpenAPI | templates/openapi-template.yaml |
Reusable components, security schemes |
| Setting | Recommendation | Reasoning |
|---|---|---|
| Default page size | 20 | Balances payload size and round trips |
| Max page size | 100 | Prevents fetching entire tables |
| URL nesting depth | Max 2 levels |
/users/42/posts OK; deeper is not |
| Date format | ISO 8601 |
2026-03-23T10:30:00Z — universally parseable |
| ID format | UUID v4 or ULID | Don't expose auto-increment integers |
Best Practices
-
Use nouns for resources, not verbs.
POST /usersnotPOST /createUser. The HTTP method IS the verb. -
Return the created/updated resource. After
POSTorPATCH, return the full resource — clients shouldn't need a follow-upGET. - Use 204 No Content for DELETE. Don't return the deleted resource — the 204 status is the confirmation.
- Never break backward compatibility. Adding fields is safe. Removing or renaming is breaking — use a new version and support the old one for 6+ months.
- Validate early, fail fast. Return 422 with field-level error details rather than letting bad data reach your database.
Troubleshooting
Clients receive 406 Not Acceptable
The Accept header doesn't match any content type your API produces. If using header-based versioning, ensure clients send the exact media type. Consider supporting application/json as a fallback.
Pagination returns duplicate items
You're likely using offset pagination with concurrent writes. Switch to cursor-based pagination using an indexed, unique, sortable column as the cursor.
PATCH requests overwriting fields with null
Use JSON Merge Patch (RFC 7386) where missing fields are unchanged and null means delete. Or use JSON Patch (RFC 6902) for precise operations.
Nested resource URLs becoming unwieldy
Beyond /parents/42/children/7, flatten the URL. Use /comments/3 with a post_id filter instead of deep nesting.
This is 1 of 7 resources in the API Developer Pro toolkit. Get the complete [REST API Design Guide] with all files, templates, and documentation for $29.
Or grab the entire API Developer Pro bundle (7 products) for $79 — save 30%.
Top comments (0)