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_model with Pydantic (from FastAPI Fundamentals)
  • ✅ Pydantic v2: BaseModel, Field, model_dump, model_validate

What's new in this module

  • 🆕 response_model_include and response_model_exclude — controlling which fields go out
  • 🆕 response_model_exclude_unset — essential for PATCH
  • 🆕 Multiple schemas per endpoint — Public vs Admin views
  • 🆕 Union types in response_model — returning different shapes
  • 🆕 StreamingResponse — exporting large datasets without loading them into memory
  • 🆕 FileResponse — serving files for download
  • 🆕 JSONResponse with custom headers
  • 🆕 RedirectResponse, HTMLResponse, ORJSONResponse
  • 🆕 Cookies from responses
  • 🆕 Custom response classes
  • 🆕 The responses parameter in decorators — documenting multiple responses in OpenAPI

From "returns JSON" to "controls every response"

AspectBefore (your API today)After (this module)
Fields returnedAll of them, alwaysOnly what's needed for the context
FormatAlways JSONJSON, CSV streaming, files, HTML
HeadersDefaults onlyCustom headers, cookies, content-disposition
DocumentationOne response typeMultiple responses documented in /docs
Large datasetsLoad everything into memoryStreaming with generators
PATCH responsesIncludes defaults that weren't sentOnly 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_include and response_model_exclude to control fields
  • ✅ Apply response_model_exclude_unset correctly in PATCH endpoints
  • ✅ Create multiple response schemas (Public, Admin, Summary) for one resource
  • ✅ Use Union types for endpoints that return different shapes of data
  • ✅ Implement StreamingResponse with generators to export large CSVs
  • ✅ Serve files with FileResponse and the right download headers
  • ✅ Create JSONResponse with custom headers and cookies
  • ✅ Use RedirectResponse, HTMLResponse and ORJSONResponse
  • ✅ Create custom response classes
  • ✅ Document multiple responses with the responses parameter 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:

  1. The tasks and users routers show up with their tags
  2. The logging middleware records requests in the console
  3. You can create and list tasks

If that works, you're ready for Module 3.

What if you don't meet a prerequisite?

Missing prerequisiteWhat to do
You didn't do Module 2Complete the Module 2 capsules first — you need the modular structure
You don't remember PydanticReview the Pydantic capsule in FastAPI Fundamentals — you need BaseModel and Field
Your app doesn't startCheck the imports in app/main.py and that you're including the routers correctly

Module roadmap

CapsuleTopicWhat you'll learn
01Introduction (this capsule)Context, why it matters, roadmap
02Advanced response_modelInclude/exclude, exclude_unset, multiple schemas, Union types
03Streaming and File ResponsesStreamingResponse with generators, FileResponse, CSV export
04Custom Responses and HeadersJSONResponse, RedirectResponse, HTMLResponse, ORJSONResponse, cookies, responses in OpenAPI
05Project: Professional ResponsesCSV 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:

  1. Return everything, always → the frontend gets data it doesn't need (and sensitive data it shouldn't see)
  2. Create separate endpoints → you duplicate logic for each consumer
  3. 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:

  1. Read the explanation — Understand the concept and why it exists
  2. Write the code — Don't copy/paste; write it to internalize it
  3. Try it in /docs — Verify that each response type works
  4. Inspect the headers — Use curl -v to see the response headers
  5. 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_unset correctly
  • ✅ You can export data as CSV using StreamingResponse without loading everything into memory
  • ✅ You can serve files with FileResponse and download headers
  • ✅ You know when to use JSONResponse, RedirectResponse, HTMLResponse
  • ✅ You can set cookies and custom headers on responses
  • ✅ Your documentation in /docs shows 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_model with include/exclude controls which fields go out
  • response_model_exclude_unset is essential for a correct PATCH
  • Multiple schemas (Public, Admin, Summary) serve different consumers
  • StreamingResponse with generators lets you export large datasets without loading them into memory
  • FileResponse serves files with the right download headers
  • JSONResponse, RedirectResponse, HTMLResponse, ORJSONResponse cover a range of formats
  • The responses parameter in decorators documents multiple responses in OpenAPI
  • The implicit security of response_model prevents accidental leaks of sensitive data

Additional resources

  1. FastAPI - Response Model — The official tutorial on response_model and its parameters
  2. FastAPI - Additional Responses — Documenting multiple responses in OpenAPI
  3. FastAPI - Custom Response — JSONResponse, HTMLResponse, StreamingResponse, FileResponse
  4. FastAPI - Response Directly — Returning responses directly without response_model
  5. Pydantic v2 - Model Config — Configuring Pydantic v2 models
  6. 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.