Module 3: Advanced Response Models
Introduction to Module 3: Advanced Response Models
Overview
Your modular API from Module 2 works: it has routers split by domain, logging and timing middleware, and dependency injection to share logic. But there's a problem that becomes obvious as soon as you think about who consumes your API. An admin hitting GET /tasks needs to see every field — including created_at, internal_notes, internal counters. A public user hitting that same endpoint shouldn't see any of that. Right now, your API returns exactly the same thing to both. There's no control over what data goes out, or in what format.
This module hands you total control over your API's responses. You'll learn to use response_model in advanced ways: excluding sensitive fields, including only what's needed, handling PATCH without sending unnecessary defaults. Then you'll go beyond JSON: StreamingResponse for exporting large CSVs without loading everything into memory, FileResponse for serving files, and JSONResponse with custom headers to communicate metadata without polluting the body. By the end, your API will speak exactly the language each client needs.
The difference between an API that "returns data" and a professional API lives in the control of its responses. A professional API returns only what the consumer needs, in the format they need, with the headers they need. That's exactly what you'll build here.
Where are we in the guide?
You're in Module 3 of 6 of the FastAPI Advanced Features guide:
Module 1: Dependency Injection ✅ (done)
→ Depends(), sub-dependencies, yield patterns, class-based dependencies
Module 2: APIRouter and Middleware ✅ (done)
→ Modular routers, custom middleware, startup/shutdown events
Module 3: Advanced Response Models ← YOU ARE HERE
→ Multiple schemas, StreamingResponse, FileResponse, custom responses
Module 4: Background Tasks
Module 5: WebSockets and File Uploads
Module 6: Project — Complete Task Manager API
Cumulative progression
Module 1: DI for DRY, testable code
↓ (injecting shared logic)
Module 2: + Modular routers + cross-cutting middleware
↓ (professional structure)
Module 3: + Total control of responses ← HERE
↓ (professional responses)
Module 4: + Background processing
↓ (asynchronous operations)
Module 5: + WebSockets + file uploads
↓ (real-time communication + files)
Module 6: Complete integrative project
Module 2 gave you the modular structure. Module 3 gives you fine-grained control over what each endpoint returns and how it returns it.
What you already know vs what's new
What you already know
From Modules 1 and 2 you bring:
- ✅ Depends() to inject dependencies into endpoints
- ✅ Sub-dependencies and yield dependencies
- ✅ APIRouter with prefixes, tags and dependencies
- ✅ Custom middleware for logging and timing
- ✅ Lifespan events (startup/shutdown)
- ✅ Modular structure:
app/routers/,app/dependencies.py - ✅ Basic
response_modelwith Pydantic (from FastAPI Fundamentals) - ✅ Pydantic v2:
BaseModel,Field,model_dump,model_validate
What's new in this module
- 🆕
response_model_includeandresponse_model_exclude— controlling which fields go out - 🆕
response_model_exclude_unset— essential for PATCH - 🆕 Multiple schemas per endpoint — Public vs Admin views
- 🆕
Uniontypes in response_model — returning different shapes - 🆕
StreamingResponse— exporting large datasets without loading them into memory - 🆕
FileResponse— serving files for download - 🆕
JSONResponsewith custom headers - 🆕
RedirectResponse,HTMLResponse,ORJSONResponse - 🆕 Cookies from responses
- 🆕 Custom response classes
- 🆕 The
responsesparameter in decorators — documenting multiple responses in OpenAPI
From "returns JSON" to "controls every response"
| Aspect | Before (your API today) | After (this module) |
|---|---|---|
| Fields returned | All of them, always | Only what's needed for the context |
| Format | Always JSON | JSON, CSV streaming, files, HTML |
| Headers | Defaults only | Custom headers, cookies, content-disposition |
| Documentation | One response type | Multiple responses documented in /docs |
| Large datasets | Load everything into memory | Streaming with generators |
| PATCH responses | Includes defaults that weren't sent | Only the fields that changed |
Why does response control matter?
Implicit security
Without response control, it's easy to leak sensitive data by accident:
# ❌ No response_model — EVERYTHING goes out
@app.get("/users/{user_id}")
def get_user(user_id: int):
return user # includes password_hash, internal_id, etc.
# ✅ With response_model — only what you define
@app.get("/users/{user_id}", response_model=UserPublic)
def get_user(user_id: int):
return user # FastAPI filters automatically
response_model acts as a data firewall. It doesn't matter what you return internally — FastAPI only sends the fields the model allows. A password_hash field you forget to exclude manually gets excluded automatically if it isn't in UserPublic.
Different clients, different needs
One API serves multiple consumers:
Web frontend → needs UserPublic (name, email, avatar)
Admin dashboard → needs UserAdmin (everything + created_at, last_login, roles)
Mobile app → needs UserMobile (name, avatar — no email)
CSV export → needs streaming for 100K records
PDF report → needs FileResponse
A single GET /users/{id} endpoint can serve them all with the right response control.
Performance with large datasets
When you export 100,000 records as CSV, you can't load everything into memory and then send it. StreamingResponse with a generator solves this:
# ❌ Load everything into memory (crashes with large datasets)
csv_content = generate_full_csv(100_000_records)
return Response(content=csv_content)
# ✅ Streaming (uses constant memory)
return StreamingResponse(csv_generator(100_000_records))
Module objective
By the time you finish this module you'll be able to:
- ✅ Use
response_model_includeandresponse_model_excludeto control fields - ✅ Apply
response_model_exclude_unsetcorrectly in PATCH endpoints - ✅ Create multiple response schemas (Public, Admin, Summary) for one resource
- ✅ Use
Uniontypes for endpoints that return different shapes of data - ✅ Implement
StreamingResponsewith generators to export large CSVs - ✅ Serve files with
FileResponseand the right download headers - ✅ Create
JSONResponsewith custom headers and cookies - ✅ Use
RedirectResponse,HTMLResponseandORJSONResponse - ✅ Create custom response classes
- ✅ Document multiple responses with the
responsesparameter in decorators - ✅ Wire it all into your Task Manager API with professional responses
Prerequisites
For this module you need:
- Module 2 completed — a modular app with routers and middleware working
- Solid Pydantic v2 — BaseModel, Field, model_dump (from FastAPI Fundamentals)
- uvicorn running with
--reload
Quick check
cd fastapi-advanced
source venv/bin/activate
uvicorn app.main:app --reload
Open http://localhost:8000/docs and verify that:
- The tasks and users routers show up with their tags
- The logging middleware records requests in the console
- You can create and list tasks
If that works, you're ready for Module 3.
What if you don't meet a prerequisite?
| Missing prerequisite | What to do |
|---|---|
| You didn't do Module 2 | Complete the Module 2 capsules first — you need the modular structure |
| You don't remember Pydantic | Review the Pydantic capsule in FastAPI Fundamentals — you need BaseModel and Field |
| Your app doesn't start | Check the imports in app/main.py and that you're including the routers correctly |
Module roadmap
| Capsule | Topic | What you'll learn |
|---|---|---|
| 01 | Introduction (this capsule) | Context, why it matters, roadmap |
| 02 | Advanced response_model | Include/exclude, exclude_unset, multiple schemas, Union types |
| 03 | Streaming and File Responses | StreamingResponse with generators, FileResponse, CSV export |
| 04 | Custom Responses and Headers | JSONResponse, RedirectResponse, HTMLResponse, ORJSONResponse, cookies, responses in OpenAPI |
| 05 | Project: Professional Responses | CSV export, differentiated schemas, custom error responses, OpenAPI documentation |
Learning flow
First you'll master response_model control: excluding sensitive fields, including only what's needed, handling PATCH correctly with exclude_unset, and creating multiple schemas for different views of the same resource (Capsule 02). Then you'll expand beyond JSON: StreamingResponse to export large datasets as CSV without loading everything into memory, FileResponse to serve files, and generators with yield for efficient streaming (Capsule 03). Next you'll explore the response types FastAPI offers: JSONResponse with custom headers, RedirectResponse, HTMLResponse, ORJSONResponse for performance, cookies, and how to document multiple responses in OpenAPI (Capsule 04). At the end, you'll wire it all into your Task Manager API: CSV export of tasks, differentiated schemas for listing vs detail, custom error responses, and professional documentation in /docs (Capsule 05).
The progression is: control fields → control format → control headers → integrate everything.
Capsule by capsule
Capsule 02 — Advanced response_model: You'll learn to use response_model_include to return only a subset of fields, response_model_exclude to hide sensitive fields, and response_model_exclude_unset, which is essential for PATCH endpoints. You'll create differentiated schemas — TaskPublic, TaskAdmin, TaskSummary — to serve different views of the same data. You'll see how to use Union types when an endpoint can return different shapes.
Capsule 03 — Streaming and File Responses: You'll discover StreamingResponse, which uses generators (yield) to send data chunk by chunk without loading everything into memory. You'll implement a CSV export for your API. You'll learn FileResponse for serving static or generated files, with content-disposition headers to force a download.
Capsule 04 — Custom Responses and Headers: You'll explore JSONResponse for total control over headers and status codes, RedirectResponse for redirects, HTMLResponse for serving HTML directly, and ORJSONResponse for faster JSON serialization. You'll learn to set cookies from responses. And you'll document multiple responses in OpenAPI using the responses parameter in decorators.
Capsule 05 — Project: Professional Responses: Your Task Manager API gets professional responses: a CSV export endpoint for tasks, different schemas for listing (summary) vs detail (full), consistent custom error responses, and complete OpenAPI documentation describing every possible response of every endpoint.
The concrete problem you're solving
Picture this real situation: your Task Manager API is consumed by a web frontend, an admin dashboard, and a script that exports data to Excel every night.
Web frontend:
GET /tasks → needs: id, title, status, due_date
(does NOT need: internal_notes, created_by_admin, audit_log)
Admin dashboard:
GET /tasks → needs: ALL the fields
(including internal_notes, created_by_admin, timestamps)
Export script:
GET /tasks/export → needs: CSV with 50,000 tasks
(can't load 50K records into memory as JSON)
Without response control, you have three bad options:
- Return everything, always → the frontend gets data it doesn't need (and sensitive data it shouldn't see)
- Create separate endpoints → you duplicate logic for each consumer
- Filter manually in every endpoint → repetitive, error-prone code
With what you'll learn in this module, the solution is elegant:
# Public view — safe fields only
@router.get("/tasks", response_model=list[TaskPublic])
# Admin view — every field
@router.get("/tasks/admin", response_model=list[TaskAdmin])
# CSV export — streaming without loading into memory
@router.get("/tasks/export")
def export_tasks():
return StreamingResponse(csv_generator(), media_type="text/csv")
Same data, three ways to serve it. That's response control.
Connection with the project
Your Task Manager API from Module 2 is going to evolve in this module. Here's what the transformation looks like:
Before (Module 2)
GET /tasks → returns every field of every task (always)
POST /tasks → returns the created task with every field
PATCH /tasks/{id} → returns the updated task including defaults that weren't sent
Format: always JSON
Headers: FastAPI's defaults only
Documentation: one response type per endpoint
After (Module 3)
GET /tasks → returns TaskPublic (no internal fields)
GET /tasks/admin → returns TaskAdmin (every field)
PATCH /tasks/{id} → returns only the fields that changed (exclude_unset)
GET /tasks/export → StreamingResponse with CSV
GET /tasks/{id}/report → FileResponse for download
Headers: X-Total-Count, custom Content-Disposition
Documentation: multiple responses documented in /docs
How to use this guide in Module 3
A hands-on approach
Every capsule includes complete code you should write and run. The cycle is:
- Read the explanation — Understand the concept and why it exists
- Write the code — Don't copy/paste; write it to internalize it
- Try it in
/docs— Verify that each response type works - Inspect the headers — Use
curl -vto see the response headers - Break things on purpose — What happens if the streaming fails halfway? If the file doesn't exist?
Estimated time
- Introduction capsule (this one): 15-20 minutes
- Technical capsules (02-04): 30-45 minutes each
- Project (05): 45-60 minutes
- Module 3 total: 2-2.5 hours
Signs of success
By the end of this module (all 5 capsules), you'll know you succeeded if:
- ✅ You can create differentiated schemas (Public, Admin, Summary) for one resource
- ✅ Your PATCH endpoints use
response_model_exclude_unsetcorrectly - ✅ You can export data as CSV using
StreamingResponsewithout loading everything into memory - ✅ You can serve files with
FileResponseand download headers - ✅ You know when to use
JSONResponse,RedirectResponse,HTMLResponse - ✅ You can set cookies and custom headers on responses
- ✅ Your documentation in
/docsshows every possible response of every endpoint - ✅ Your Task Manager API has professional responses for every use case
Summary
- This is Module 3 of 6, focused on total control of responses
- Your API's responses are your public contract — they define the consumer's experience
response_modelwith include/exclude controls which fields go outresponse_model_exclude_unsetis essential for a correct PATCH- Multiple schemas (Public, Admin, Summary) serve different consumers
StreamingResponsewith generators lets you export large datasets without loading them into memoryFileResponseserves files with the right download headersJSONResponse,RedirectResponse,HTMLResponse,ORJSONResponsecover a range of formats- The
responsesparameter in decorators documents multiple responses in OpenAPI - The implicit security of
response_modelprevents accidental leaks of sensitive data
Additional resources
- FastAPI - Response Model — The official tutorial on response_model and its parameters
- FastAPI - Additional Responses — Documenting multiple responses in OpenAPI
- FastAPI - Custom Response — JSONResponse, HTMLResponse, StreamingResponse, FileResponse
- FastAPI - Response Directly — Returning responses directly without response_model
- Pydantic v2 - Model Config — Configuring Pydantic v2 models
- OpenAPI Specification - Responses — How responses are documented in OpenAPI
Next capsule: Advanced response_model — You'll learn to control exactly which fields each endpoint returns using include, exclude, exclude_unset, and multiple schemas per resource.