509 lines
13 KiB
Markdown
509 lines
13 KiB
Markdown
---
|
|
name: fastapi-patterns
|
|
description: Automatically applies when creating FastAPI endpoints, routers, and API structures. Enforces best practices for endpoint definitions, dependency injection, error handling, and documentation.
|
|
---
|
|
|
|
# FastAPI Endpoint Pattern Enforcer
|
|
|
|
When building APIs with FastAPI, follow these patterns for consistent, well-documented, and maintainable endpoints.
|
|
|
|
## ✅ Correct Pattern
|
|
|
|
```python
|
|
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
|
from pydantic import BaseModel
|
|
from typing import Optional
|
|
|
|
router = APIRouter(prefix="/api/v1/users", tags=["users"])
|
|
|
|
|
|
class UserCreate(BaseModel):
|
|
"""Request model for user creation."""
|
|
email: str
|
|
name: str
|
|
age: Optional[int] = None
|
|
|
|
|
|
class UserResponse(BaseModel):
|
|
"""Response model for user endpoints."""
|
|
id: str
|
|
email: str
|
|
name: str
|
|
created_at: str
|
|
|
|
|
|
@router.post(
|
|
"/",
|
|
response_model=UserResponse,
|
|
status_code=status.HTTP_201_CREATED,
|
|
summary="Create a new user",
|
|
responses={
|
|
201: {"description": "User created successfully"},
|
|
409: {"description": "Email already registered"},
|
|
422: {"description": "Validation error"}
|
|
}
|
|
)
|
|
async def create_user(
|
|
user: UserCreate,
|
|
current_user: User = Depends(get_current_user),
|
|
user_service: UserService = Depends()
|
|
) -> UserResponse:
|
|
"""
|
|
Create a new user account.
|
|
|
|
- **email**: Valid email address
|
|
- **name**: User's full name
|
|
- **age**: Optional user age
|
|
"""
|
|
try:
|
|
return await user_service.create(user)
|
|
except DuplicateEmailError:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_409_CONFLICT,
|
|
detail="Email already registered"
|
|
)
|
|
```
|
|
|
|
## Router Organization
|
|
|
|
```python
|
|
from fastapi import APIRouter
|
|
|
|
# Organize endpoints by resource
|
|
users_router = APIRouter(prefix="/api/v1/users", tags=["users"])
|
|
products_router = APIRouter(prefix="/api/v1/products", tags=["products"])
|
|
orders_router = APIRouter(prefix="/api/v1/orders", tags=["orders"])
|
|
|
|
# Register routers in main app
|
|
app = FastAPI()
|
|
app.include_router(users_router)
|
|
app.include_router(products_router)
|
|
app.include_router(orders_router)
|
|
```
|
|
|
|
## Dependency Injection
|
|
|
|
```python
|
|
from fastapi import Depends, HTTPException, status
|
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
|
from typing import Annotated
|
|
|
|
security = HTTPBearer()
|
|
|
|
|
|
async def get_current_user(
|
|
credentials: HTTPAuthorizationCredentials = Depends(security)
|
|
) -> User:
|
|
"""Extract and validate current user from token."""
|
|
token = credentials.credentials
|
|
user = await verify_token(token)
|
|
if not user:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
detail="Invalid authentication credentials"
|
|
)
|
|
return user
|
|
|
|
|
|
async def get_admin_user(
|
|
current_user: User = Depends(get_current_user)
|
|
) -> User:
|
|
"""Verify user has admin privileges."""
|
|
if not current_user.is_admin:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Admin privileges required"
|
|
)
|
|
return current_user
|
|
|
|
|
|
# Use in endpoints
|
|
@router.get("/admin-only")
|
|
async def admin_endpoint(
|
|
admin: User = Depends(get_admin_user)
|
|
) -> dict:
|
|
"""Admin-only endpoint."""
|
|
return {"message": "Admin access granted"}
|
|
```
|
|
|
|
## Request Validation
|
|
|
|
```python
|
|
from fastapi import Query, Path, Body
|
|
from pydantic import Field
|
|
|
|
@router.get("/users")
|
|
async def list_users(
|
|
page: int = Query(1, ge=1, description="Page number"),
|
|
page_size: int = Query(10, ge=1, le=100, description="Items per page"),
|
|
search: Optional[str] = Query(None, min_length=1, max_length=100),
|
|
sort_by: str = Query("created_at", regex="^(name|email|created_at)$")
|
|
) -> list[UserResponse]:
|
|
"""List users with pagination and filtering."""
|
|
return await user_service.list(
|
|
page=page,
|
|
page_size=page_size,
|
|
search=search,
|
|
sort_by=sort_by
|
|
)
|
|
|
|
|
|
@router.get("/users/{user_id}")
|
|
async def get_user(
|
|
user_id: str = Path(..., min_length=1, description="User ID")
|
|
) -> UserResponse:
|
|
"""Get user by ID."""
|
|
user = await user_service.get(user_id)
|
|
if not user:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=f"User {user_id} not found"
|
|
)
|
|
return user
|
|
|
|
|
|
@router.patch("/users/{user_id}")
|
|
async def update_user(
|
|
user_id: str = Path(...),
|
|
update: dict = Body(..., example={"name": "New Name"})
|
|
) -> UserResponse:
|
|
"""Partially update user."""
|
|
return await user_service.update(user_id, update)
|
|
```
|
|
|
|
## Error Handling
|
|
|
|
```python
|
|
from fastapi import HTTPException, status
|
|
from fastapi.responses import JSONResponse
|
|
from fastapi.exceptions import RequestValidationError
|
|
|
|
# Service layer exceptions
|
|
class ServiceError(Exception):
|
|
"""Base service exception."""
|
|
pass
|
|
|
|
|
|
class NotFoundError(ServiceError):
|
|
"""Resource not found."""
|
|
pass
|
|
|
|
|
|
class DuplicateError(ServiceError):
|
|
"""Duplicate resource."""
|
|
pass
|
|
|
|
|
|
# Convert service exceptions to HTTP exceptions
|
|
@router.post("/users")
|
|
async def create_user(user: UserCreate) -> UserResponse:
|
|
"""Create user with proper error handling."""
|
|
try:
|
|
return await user_service.create(user)
|
|
except DuplicateError as e:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_409_CONFLICT,
|
|
detail=str(e)
|
|
)
|
|
except ServiceError as e:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail="Internal server error"
|
|
)
|
|
|
|
|
|
# Global exception handlers
|
|
@app.exception_handler(RequestValidationError)
|
|
async def validation_exception_handler(request, exc):
|
|
"""Handle validation errors."""
|
|
return JSONResponse(
|
|
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
|
content={
|
|
"detail": "Validation error",
|
|
"errors": exc.errors()
|
|
}
|
|
)
|
|
|
|
|
|
@app.exception_handler(ServiceError)
|
|
async def service_exception_handler(request, exc):
|
|
"""Handle service errors."""
|
|
return JSONResponse(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
content={"detail": "Internal server error"}
|
|
)
|
|
```
|
|
|
|
## Response Models
|
|
|
|
```python
|
|
from pydantic import BaseModel
|
|
from typing import Generic, TypeVar, List
|
|
|
|
T = TypeVar('T')
|
|
|
|
|
|
class PaginatedResponse(BaseModel, Generic[T]):
|
|
"""Generic paginated response."""
|
|
items: List[T]
|
|
total: int
|
|
page: int
|
|
page_size: int
|
|
has_next: bool
|
|
|
|
|
|
class SuccessResponse(BaseModel):
|
|
"""Generic success response."""
|
|
message: str
|
|
data: Optional[dict] = None
|
|
|
|
|
|
class ErrorResponse(BaseModel):
|
|
"""Error response model."""
|
|
detail: str
|
|
code: Optional[str] = None
|
|
|
|
|
|
@router.get(
|
|
"/users",
|
|
response_model=PaginatedResponse[UserResponse]
|
|
)
|
|
async def list_users(
|
|
page: int = Query(1, ge=1),
|
|
page_size: int = Query(10, ge=1, le=100)
|
|
) -> PaginatedResponse[UserResponse]:
|
|
"""List users with pagination."""
|
|
users, total = await user_service.list_paginated(page, page_size)
|
|
return PaginatedResponse(
|
|
items=users,
|
|
total=total,
|
|
page=page,
|
|
page_size=page_size,
|
|
has_next=total > page * page_size
|
|
)
|
|
```
|
|
|
|
## Async Operations
|
|
|
|
```python
|
|
import httpx
|
|
from fastapi import BackgroundTasks
|
|
|
|
|
|
async def fetch_external_data(user_id: str) -> dict:
|
|
"""Fetch data from external service."""
|
|
async with httpx.AsyncClient() as client:
|
|
response = await client.get(f"https://api.example.com/users/{user_id}")
|
|
response.raise_for_status()
|
|
return response.json()
|
|
|
|
|
|
async def send_email(email: str, subject: str, body: str):
|
|
"""Send email asynchronously."""
|
|
# Email sending logic
|
|
pass
|
|
|
|
|
|
@router.post("/users/{user_id}/notify")
|
|
async def notify_user(
|
|
user_id: str,
|
|
background_tasks: BackgroundTasks
|
|
) -> SuccessResponse:
|
|
"""Notify user via email in background."""
|
|
user = await user_service.get(user_id)
|
|
|
|
# Add task to background
|
|
background_tasks.add_task(
|
|
send_email,
|
|
email=user.email,
|
|
subject="Notification",
|
|
body="You have a new notification"
|
|
)
|
|
|
|
return SuccessResponse(message="Notification scheduled")
|
|
```
|
|
|
|
## OpenAPI Documentation
|
|
|
|
```python
|
|
from fastapi import FastAPI
|
|
|
|
app = FastAPI(
|
|
title="My API",
|
|
description="Comprehensive API for user management",
|
|
version="1.0.0",
|
|
docs_url="/docs",
|
|
redoc_url="/redoc",
|
|
openapi_url="/openapi.json"
|
|
)
|
|
|
|
|
|
@router.post(
|
|
"/users",
|
|
summary="Create user",
|
|
description="Create a new user with email and name",
|
|
response_description="Created user object",
|
|
tags=["users"],
|
|
responses={
|
|
201: {
|
|
"description": "User created",
|
|
"content": {
|
|
"application/json": {
|
|
"example": {
|
|
"id": "usr_123",
|
|
"email": "user@example.com",
|
|
"name": "John Doe"
|
|
}
|
|
}
|
|
}
|
|
},
|
|
409: {"description": "Email already exists"}
|
|
}
|
|
)
|
|
async def create_user(user: UserCreate) -> UserResponse:
|
|
"""
|
|
Create a new user.
|
|
|
|
Parameters:
|
|
- **email**: User email address (required)
|
|
- **name**: User full name (required)
|
|
"""
|
|
return await user_service.create(user)
|
|
```
|
|
|
|
## Middleware
|
|
|
|
```python
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.middleware.gzip import GZipMiddleware
|
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
import time
|
|
|
|
# CORS
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["https://example.com"],
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"]
|
|
)
|
|
|
|
# Compression
|
|
app.add_middleware(GZipMiddleware, minimum_size=1000)
|
|
|
|
|
|
# Custom middleware
|
|
class TimingMiddleware(BaseHTTPMiddleware):
|
|
"""Log request timing."""
|
|
|
|
async def dispatch(self, request, call_next):
|
|
start_time = time.time()
|
|
response = await call_next(request)
|
|
duration = time.time() - start_time
|
|
response.headers["X-Process-Time"] = str(duration)
|
|
return response
|
|
|
|
|
|
app.add_middleware(TimingMiddleware)
|
|
```
|
|
|
|
## ❌ Anti-Patterns
|
|
|
|
```python
|
|
# ❌ No type hints
|
|
@app.get("/users")
|
|
async def get_users(): # Missing return type and parameter types
|
|
pass
|
|
|
|
# ✅ Better: full type hints
|
|
@app.get("/users")
|
|
async def get_users(
|
|
page: int = Query(1)
|
|
) -> list[UserResponse]:
|
|
pass
|
|
|
|
|
|
# ❌ No response model
|
|
@app.get("/users")
|
|
async def get_users() -> dict: # Returns dict (no validation)
|
|
return {"users": [...]}
|
|
|
|
# ✅ Better: use Pydantic response model
|
|
@app.get("/users", response_model=list[UserResponse])
|
|
async def get_users() -> list[UserResponse]:
|
|
return await user_service.list()
|
|
|
|
|
|
# ❌ Generic exception handling
|
|
@app.post("/users")
|
|
async def create_user(user: UserCreate):
|
|
try:
|
|
return await user_service.create(user)
|
|
except Exception: # Too broad!
|
|
raise HTTPException(500, "Error")
|
|
|
|
# ✅ Better: specific exception handling
|
|
@app.post("/users")
|
|
async def create_user(user: UserCreate):
|
|
try:
|
|
return await user_service.create(user)
|
|
except DuplicateError as e:
|
|
raise HTTPException(409, str(e))
|
|
except ValidationError as e:
|
|
raise HTTPException(422, str(e))
|
|
|
|
|
|
# ❌ Blocking I/O in async endpoint
|
|
@app.get("/data")
|
|
async def get_data():
|
|
data = requests.get("https://api.example.com") # Blocking!
|
|
return data.json()
|
|
|
|
# ✅ Better: use async HTTP client
|
|
@app.get("/data")
|
|
async def get_data():
|
|
async with httpx.AsyncClient() as client:
|
|
response = await client.get("https://api.example.com")
|
|
return response.json()
|
|
```
|
|
|
|
## Best Practices Checklist
|
|
|
|
- ✅ Use `APIRouter` for organizing endpoints
|
|
- ✅ Define Pydantic models for requests and responses
|
|
- ✅ Add `response_model` to all endpoints
|
|
- ✅ Use appropriate HTTP status codes
|
|
- ✅ Document endpoints with docstrings
|
|
- ✅ Handle service exceptions and convert to HTTP exceptions
|
|
- ✅ Use dependency injection with `Depends()`
|
|
- ✅ Add validation to query/path/body parameters
|
|
- ✅ Use async/await for all I/O operations
|
|
- ✅ Add OpenAPI documentation
|
|
- ✅ Use background tasks for long-running operations
|
|
- ✅ Implement proper error responses
|
|
|
|
## Auto-Apply
|
|
|
|
When creating FastAPI endpoints:
|
|
1. Define request/response Pydantic models
|
|
2. Use `APIRouter` with prefix and tags
|
|
3. Add type hints to all parameters
|
|
4. Specify `response_model` and `status_code`
|
|
5. Document with docstring
|
|
6. Handle exceptions properly
|
|
7. Use `Depends()` for authentication and services
|
|
|
|
## References
|
|
|
|
For comprehensive examples, see:
|
|
- [Python Patterns Guide](../../../docs/python-patterns.md#fastapi-endpoints)
|
|
- [Pydantic Models Skill](../pydantic-models/SKILL.md)
|
|
- [Async/Await Patterns Skill](../async-await-checker/SKILL.md)
|
|
|
|
## Related Skills
|
|
|
|
- pydantic-models - For request/response models
|
|
- async-await-checker - For async endpoint patterns
|
|
- structured-errors - For error handling
|
|
- docstring-format - For endpoint documentation
|