Framework Cookbooks

FastAPI Response Models and Status Codes

How to declare response models, status codes, and custom exceptions in FastAPI, including background tasks, streaming responses, and OpenAPI documentation.

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.

Giao thức liên quan

Thuật ngữ liên quan

Thêm trong Framework Cookbooks