FastAPI Response Model Basics
FastAPI uses Pydantic models to validate and serialize response data. Declaring a response_model enables automatic validation, serialization, and OpenAPI schema generation in one step.
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class UserOut(BaseModel):
id: int
name: str
email: str
# Note: password field NOT included — filtered by response_model
class UserIn(UserOut):
password: str
@app.post('/users/', response_model=UserOut, status_code=201)
async def create_user(user: UserIn):
db_user = save_to_db(user)
return db_user # FastAPI filters fields via response_model
Use response_model_exclude_unset=True to omit fields that were not explicitly set, useful for PATCH responses:
@app.patch('/users/{id}', response_model=UserOut, response_model_exclude_unset=True)
async def update_user(id: int, user: UserIn):
...
Status Code Declarations
FastAPI provides status constants from fastapi (which re-exports starlette.status):
from fastapi import FastAPI, status
app = FastAPI()
@app.delete('/users/{id}', status_code=status.HTTP_204_NO_CONTENT)
async def delete_user(id: int):
db.delete(id)
# Return None — FastAPI sends 204 with empty body
@app.post('/items/', status_code=status.HTTP_201_CREATED)
async def create_item(item: ItemIn) -> ItemOut:
return db.create(item)
To return a dynamic status code at runtime (not declared at decorator level), use Response directly:
from fastapi import Response
@app.put('/users/{id}')
async def upsert_user(id: int, user: UserIn, response: Response):
existing = db.find(id)
if existing:
db.update(id, user)
response.status_code = 200
else:
db.create(id, user)
response.status_code = 201
return user
HTTPException and Custom Exceptions
Raise HTTPException for expected errors:
from fastapi import HTTPException, status
@app.get('/users/{id}')
async def get_user(id: int):
user = db.find(id)
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail='User not found',
)
return user
For custom domain exceptions, register an exception handler:
from fastapi import Request
from fastapi.responses import JSONResponse
class InsufficientFundsError(Exception):
def __init__(self, balance: float, required: float):
self.balance = balance
self.required = required
@app.exception_handler(InsufficientFundsError)
async def insufficient_funds_handler(request: Request, exc: InsufficientFundsError):
return JSONResponse(
status_code=402,
content={
'error': 'insufficient_funds',
'balance': exc.balance,
'required': exc.required,
},
)
Background Tasks and 202 Accepted
202 Accepted signals that a request was received but processing is not complete. FastAPI's BackgroundTasks integrates naturally:
from fastapi import BackgroundTasks, status
from fastapi.responses import JSONResponse
def send_email_task(email: str, body: str):
# Runs after response is sent
mailer.send(email, body)
@app.post('/notifications/', status_code=status.HTTP_202_ACCEPTED)
async def send_notification(payload: NotificationIn, bg: BackgroundTasks):
bg.add_task(send_email_task, payload.email, payload.body)
return {'message': 'Notification queued', 'status': 'accepted'}
File and Streaming Responses
from fastapi.responses import FileResponse, StreamingResponse
import io
@app.get('/download/{filename}')
async def download_file(filename: str):
path = f'/files/{filename}'
return FileResponse(
path,
media_type='application/octet-stream',
filename=filename,
)
@app.get('/stream/csv')
async def stream_csv():
async def generate():
yield 'id,name\n'
async for row in db.stream_all():
yield f'{row.id},{row.name}\n'
return StreamingResponse(
generate(),
media_type='text/csv',
headers={'Content-Disposition': 'attachment; filename=export.csv'},
)
OpenAPI Documentation Integration
Document all possible responses in the OpenAPI schema using responses:
from fastapi import FastAPI
from pydantic import BaseModel
class ErrorDetail(BaseModel):
detail: str
@app.get(
'/users/{id}',
response_model=UserOut,
responses={
404: {'model': ErrorDetail, 'description': 'User not found'},
403: {'model': ErrorDetail, 'description': 'Forbidden'},
},
)
async def get_user(id: int):
...
These extra responses entries appear in Swagger UI and the generated OpenAPI JSON at /openapi.json, enabling client code generators to produce typed error-handling code automatically.
Global Exception Handler with Middleware
For a fully centralized error response pipeline, register a custom exception handler at the application level. This catches errors from all routes, including those raised deep inside dependency injection chains:
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException
app = FastAPI()
@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request: Request, exc: StarletteHTTPException):
return JSONResponse(
status_code=exc.status_code,
content={
'type': f'https://protocolcodes.com/http/{exc.status_code}',
'title': exc.detail,
'status': exc.status_code,
'instance': str(request.url),
},
)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
return JSONResponse(
status_code=422,
content={
'type': 'https://protocolcodes.com/http/422',
'title': 'Validation Error',
'status': 422,
'errors': exc.errors(),
},
)
Testing Status Codes with FastAPI TestClient
FastAPI's TestClient (wrapping Starlette's TestClient, which uses httpx) makes it trivial to assert on status codes:
from fastapi.testclient import TestClient
from myapp.main import app
client = TestClient(app)
def test_get_user_not_found():
response = client.get('/users/999')
assert response.status_code == 404
data = response.json()
assert data['title'] == 'Not Found'
def test_create_user_invalid():
response = client.post('/users/', json={'name': ''}) # Fails validation
assert response.status_code == 422
assert 'errors' in response.json()
Use raise_server_exceptions=False on TestClient to test 5xx responses without pytest catching the underlying exception first.