Request / Response Modeling
In FastAPI, schemas are not just validation helpers. They are the public contract of your API. Once request DTOs, internal commands, ORM entities, and response DTOs collapse into one type, versioning, security, lazy loading, and serialization costs all get tangled together.
Quick takeaway: treat `request model != response model != ORM model` as the default. Explicit inputs and explicit outputs produce APIs that evolve much more safely.
The Boundary Model
Baseline Pattern
from datetime import datetime
from typing import Annotated
from uuid import UUID
from fastapi import Query
from pydantic import BaseModel, ConfigDict, EmailStr, Field
class CreateUserRequest(BaseModel):
email: EmailStr
name: str = Field(min_length=1, max_length=80)
class UserResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: UUID
email: EmailStr
name: str
created_at: datetime
PageSize = Annotated[int, Query(ge=1, le=100)]Why Request and Response Models Should Differ
- Create requests may contain write-only fields like passwords or invitation codes.
- Responses need server-owned fields such as IDs, timestamps, and status.
- ORM records may have persistence-only fields like
password_hash, soft-delete columns, or version counters.
response_model Is Both Filter and Contract
@router.post(
"/users",
response_model=UserResponse,
status_code=201,
)
async def create_user(payload: CreateUserRequest) -> UserResponse:
...In FastAPI, `response_model` is not just for documentation. It shapes serialization and narrows the public contract. That is why returning an explicit response DTO is safer than leaking raw ORM entities.
Design Rules for Durable APIs
Split input and output
Do not reuse one schema for create, update, and read if the field meaning differs.
Keep server-owned fields explicit
IDs, timestamps, and version counters usually belong in responses, not requests.
Fix pagination and error shapes early
Stable list and error envelopes save a lot of migration pain later.
Stop ORM leakage
Build DTOs explicitly so serialization never depends on hidden lazy loads.
Practical Rules
- Use dedicated
CreateXRequest,UpdateXRequest, andXResponsemodels. - Keep response DTOs stable even if persistence columns change.
- Use
AnnotatedplusQuery,Path, andHeaderto make constraints visible in the function signature. - Keep list responses consistent with
items + page info.