API Design¶
Build APIs that are consistent, documented, and evolvable.
Design Principles¶
1. Consistency¶
Use the same patterns everywhere:
GET /resources → List resources
GET /resources/{id} → Get single resource
POST /resources → Create resource
PUT /resources/{id} → Replace resource
PATCH /resources/{id} → Partial update
DELETE /resources/{id} → Delete resource
2. Predictability¶
Users should guess correctly how your API works:
# Consistent naming
GET /users/{user_id}/posts # User's posts
GET /posts/{post_id}/comments # Post's comments
# Consistent filtering
GET /posts?status=published&author_id=123
GET /users?role=admin&active=true
# Consistent pagination
GET /posts?page=2&per_page=20
GET /users?page=2&per_page=20
3. Discoverability¶
Make the API self-documenting:
{
"data": {...},
"links": {
"self": "/posts/123",
"author": "/users/456",
"comments": "/posts/123/comments"
}
}
Section Contents¶
| Topic | Description |
|---|---|
| OpenAPI | FastAPI OpenAPI generation |
| Versioning | API versioning strategies |
| Backwards Compatibility | Non-breaking changes |
| Error Contracts | Consistent error responses |
| Documentation | API docs best practices |
Quick Reference¶
HTTP Methods¶
| Method | Idempotent | Safe | Use Case |
|---|---|---|---|
| GET | Yes | Yes | Read resources |
| POST | No | No | Create resources |
| PUT | Yes | No | Replace resources |
| PATCH | No | No | Partial update |
| DELETE | Yes | No | Delete resources |
Status Codes¶
| Code | Meaning | When to Use |
|---|---|---|
| 200 | OK | Successful GET, PUT, PATCH |
| 201 | Created | Successful POST |
| 204 | No Content | Successful DELETE |
| 400 | Bad Request | Invalid input |
| 401 | Unauthorized | No/invalid auth |
| 403 | Forbidden | Auth valid, not permitted |
| 404 | Not Found | Resource doesn't exist |
| 409 | Conflict | Duplicate, version conflict |
| 422 | Unprocessable | Validation failed |
| 429 | Too Many Requests | Rate limited |
| 500 | Server Error | Unexpected error |
Response Structure¶
from pydantic import BaseModel
from typing import Generic, TypeVar, Optional, List
T = TypeVar("T")
class PaginationMeta(BaseModel):
page: int
per_page: int
total: int
total_pages: int
class Links(BaseModel):
self: str
next: Optional[str] = None
prev: Optional[str] = None
class ApiResponse(BaseModel, Generic[T]):
data: T
meta: Optional[dict] = None
links: Optional[Links] = None
class PaginatedResponse(BaseModel, Generic[T]):
data: List[T]
meta: PaginationMeta
links: Links
Example Endpoints¶
from fastapi import FastAPI, Query, Path, HTTPException
from typing import List
app = FastAPI()
@app.get("/posts", response_model=PaginatedResponse[Post])
async def list_posts(
page: int = Query(1, ge=1),
per_page: int = Query(20, ge=1, le=100),
status: Optional[str] = Query(None, enum=["draft", "published"]),
author_id: Optional[int] = None,
):
"""List posts with filtering and pagination."""
pass
@app.get("/posts/{post_id}", response_model=ApiResponse[Post])
async def get_post(
post_id: int = Path(..., ge=1),
):
"""Get a single post by ID."""
pass
@app.post("/posts", response_model=ApiResponse[Post], status_code=201)
async def create_post(
post: PostCreate,
):
"""Create a new post."""
pass
@app.patch("/posts/{post_id}", response_model=ApiResponse[Post])
async def update_post(
post_id: int,
post: PostUpdate,
):
"""Partially update a post."""
pass
@app.delete("/posts/{post_id}", status_code=204)
async def delete_post(
post_id: int,
):
"""Delete a post."""
pass
URL Design¶
Good URLs¶
GET /users/123/posts # User's posts
GET /posts?author_id=123 # Filter by author
GET /posts/recent # Named query
POST /posts/123/publish # Action on resource
Bad URLs¶
GET /getUserPosts?userId=123 # Verb in URL
GET /posts/getRecent # Verb in URL
POST /posts/123?action=publish # Action in query
GET /Posts/123 # Inconsistent casing
Nesting Guidelines¶
# Good: One level of nesting
GET /users/123/posts
# Avoid: Deep nesting
GET /users/123/posts/456/comments/789/likes
# Better: Direct access with filtering
GET /comments/789/likes
GET /comments?post_id=456
Filtering & Sorting¶
Query Parameters¶
@app.get("/posts")
async def list_posts(
# Filtering
status: Optional[str] = None,
author_id: Optional[int] = None,
created_after: Optional[datetime] = None,
created_before: Optional[datetime] = None,
# Sorting
sort: str = Query("created_at", regex="^-?(created_at|title|views)$"),
# Pagination
page: int = Query(1, ge=1),
per_page: int = Query(20, ge=1, le=100),
):
# Parse sort direction
desc = sort.startswith("-")
sort_field = sort.lstrip("-")
query = select(Post)
if status:
query = query.where(Post.status == status)
if author_id:
query = query.where(Post.author_id == author_id)
if created_after:
query = query.where(Post.created_at >= created_after)
if desc:
query = query.order_by(getattr(Post, sort_field).desc())
else:
query = query.order_by(getattr(Post, sort_field))
# Paginate
total = await count_query(query)
query = query.offset((page - 1) * per_page).limit(per_page)
posts = await session.execute(query)
return paginated_response(posts, page, per_page, total)
Request/Response Examples¶
Create Resource¶
POST /api/v1/posts HTTP/1.1
Content-Type: application/json
{
"title": "My Post",
"content": "Post content here",
"tags": ["python", "api"]
}
HTTP/1.1 201 Created
Content-Type: application/json
Location: /api/v1/posts/123
{
"data": {
"id": 123,
"title": "My Post",
"content": "Post content here",
"tags": ["python", "api"],
"status": "draft",
"created_at": "2024-01-15T10:30:00Z"
},
"links": {
"self": "/api/v1/posts/123"
}
}
List with Pagination¶
HTTP/1.1 200 OK
Content-Type: application/json
{
"data": [
{"id": 11, "title": "Post 11", ...},
{"id": 12, "title": "Post 12", ...}
],
"meta": {
"page": 2,
"per_page": 10,
"total": 45,
"total_pages": 5
},
"links": {
"self": "/api/v1/posts?page=2&per_page=10&status=published",
"next": "/api/v1/posts?page=3&per_page=10&status=published",
"prev": "/api/v1/posts?page=1&per_page=10&status=published"
}
}
Related Documentation¶
- Backend API Design — FastAPI-specific patterns
- Error Handling — Exception patterns
- Authentication & Auth Patterns — API authentication dependency patterns