FastAPI Mastery
22 topics ยท Comprehensive course
Topic 6 / 22 โ€” Responses
Topic 6 ยท Responses
6.1 โ€” Response Models
FastAPI lets you declare exactly what shape your response will have using response_model. This gives you automatic filtering, validation, and OpenAPI documentation of your output โ€” not just your input.
๐Ÿ’ก Analogy
response_model is like an airport security X-ray for your outgoing data. Even if your function returns a huge User object with passwords and internal fields, the response model acts as a filter โ€” only approved fields make it through to the client.
6.1.1 response_model
๐Ÿ“
Declaring response_model on an Endpoint
โ–ผ

Add response_model=YourSchema to any route decorator. FastAPI will then validate the return value against that schema, filter out extra fields, and generate correct OpenAPI docs for the response body.

Handler returns dict/ORM/model
โ†’
response_model filter
โ†’
Serialized JSON to client
python
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

# Full internal model (has sensitive data)
class UserInDB(BaseModel):
    id: int
    name: str
    email: str
    hashed_password: str  # NEVER send this to client!
    is_superuser: bool

# Public response schema (safe subset)
class UserOut(BaseModel):
    id: int
    name: str
    email: str

# FastAPI filters the return value through UserOut
@app.get("/users/{user_id}", response_model=UserOut)
async def get_user(user_id: int):
    # Return the full DB object โ€” FastAPI strips hashed_password & is_superuser
    return UserInDB(
        id=user_id,
        name="Alice",
        email="alice@x.com",
        hashed_password="$2b$12$abc...",
        is_superuser=False
    )

# Client receives: {"id": 1, "name": "Alice", "email": "alice@x.com"}
# hashed_password and is_superuser are NEVER sent!
โœ…
This is the recommended pattern in production: keep separate Input, DB, and Output schemas. Never accidentally expose sensitive fields.
๐Ÿ“‹
response_model with Lists and Optional
โ–ผ

response_model works with any type โ€” lists, Optional types, Union types, or nested models.

python
from typing import Optional
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class ItemOut(BaseModel):
    id: int
    name: str
    price: float

# List response
@app.get("/items", response_model=list[ItemOut])
async def list_items():
    return [
        {"id": 1, "name": "Laptop", "price": 75000, "secret": "ignored"},
        {"id": 2, "name": "Phone",  "price": 30000},
    ]

# Optional response (may return None โ†’ 200 with null)
@app.get("/items/{item_id}", response_model=Optional[ItemOut])
async def get_item(item_id: int):
    if item_id == 999:
        return None
    return {"id": item_id, "name": "Laptop", "price": 75000}

# status_code โ€” change the default 200
@app.post("/items", response_model=ItemOut, status_code=201)
async def create_item(item: ItemOut):
    return item  # 201 Created
6.1.2 Include Fields / Exclude Fields
๐Ÿ”Ž
response_model_include and response_model_exclude
โ–ผ

Instead of defining a separate output schema, you can use response_model_include and response_model_exclude on the route to dynamically control which fields appear in the response. Useful for quick one-offs, but a dedicated UserOut schema is cleaner for production.

python
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class User(BaseModel):
    id: int
    name: str
    email: str
    role: str
    hashed_password: str

# Only include specific fields
@app.get(
    "/users/{uid}/public",
    response_model=User,
    response_model_include={"id", "name"}
)
async def get_user_public(uid: int):
    return User(id=uid, name="Alice", email="a@x.com",
                 role="admin", hashed_password="secret")
# Response: {"id": 1, "name": "Alice"} โ€” only id and name

# Exclude specific fields
@app.get(
    "/users/{uid}",
    response_model=User,
    response_model_exclude={"hashed_password"}
)
async def get_user(uid: int):
    return User(id=uid, name="Alice", email="a@x.com",
                 role="admin", hashed_password="secret")
# Response: {"id":1,"name":"Alice","email":"a@x.com","role":"admin"}

# Exclude fields that are None (clean up optional fields)
@app.get("/profile", response_model=User, response_model_exclude_none=True)
async def get_profile():
    pass
๐Ÿงช Response Filter Simulator
โ†’ Click a button to see how FastAPI filters response fields...
โš ๏ธ
response_model_include/exclude vs a dedicated schema: The dedicated schema (UserOut) is preferred in real projects because it's reusable, self-documenting, and appears correctly in OpenAPI. Use include/exclude only for quick prototyping.
๐Ÿง 
Quick Check โ€” 6.1 Response Models
โ–ผ
Your endpoint returns a UserInDB object that contains hashed_password. How do you prevent it from appearing in the JSON response?
A. Delete the field from the object before returning
B. Set response_model=UserOut where UserOut doesn't have the hashed_password field
C. Use return JSONResponse(content=...) and manually omit it
D. Mark hashed_password as private=True in Pydantic
Topic 6 ยท Responses
6.2 โ€” Response Classes
FastAPI provides a rich set of Response classes for sending different content types โ€” JSON, HTML, plain text, files, redirects, and streams. Knowing when to use each is key to building professional APIs.
๐Ÿ’ก Analogy
Think of response classes like different types of envelopes. A JSONResponse is a plain envelope with structured data inside. A FileResponse is a parcel. A StreamingResponse is a live broadcast. Each has a purpose โ€” you pick the right one for what you're sending.
6.2.0 Overview โ€” All Response Classes
๐Ÿ—‚๏ธ
Pick the Right Response Class
โ–ผ
JSONResponse
Default. Returns JSON with Content-Type: application/json
JSON
ORJSONResponse
Faster JSON using the orjson library. Drop-in replacement.
JSON ยท Fast
HTMLResponse
Returns raw HTML string. Content-Type: text/html
HTML
PlainTextResponse
Returns plain text. Content-Type: text/plain
TEXT
FileResponse
Sends a file from disk. Handles Content-Disposition, MIME type.
FILE
RedirectResponse
HTTP 302/301/307 redirect. Sends Location header.
REDIRECT
StreamingResponse
Streams data from a generator. Perfect for large files or SSE.
STREAM
6.2.1 JSONResponse
{ }
JSONResponse โ€” Default JSON Output
โ–ผ

FastAPI uses JSONResponse by default โ€” you usually never need to import it. But you can use it directly when you need custom status codes, headers, or cookies.

python
from fastapi import FastAPI
from fastapi.responses import JSONResponse

app = FastAPI()

# 1. Implicit JSONResponse (most common โ€” just return a dict)
@app.get("/items")
async def list_items():
    return {"items": ["a", "b"]}  # FastAPI wraps this in JSONResponse

# 2. Explicit JSONResponse with custom status + headers
@app.post("/items")
async def create_item():
    return JSONResponse(
        content={"message": "created", "id": 42},
        status_code=201,
        headers={"X-Item-ID": "42"}
    )

# 3. Return error with custom status
@app.get("/check")
async def check(flag: bool = True):
    if not flag:
        return JSONResponse(
            content={"error": "flag is False"},
            status_code=400
        )
    return {"status": "ok"}
โ„น๏ธ
When you return a dict or Pydantic model from a route function, FastAPI automatically wraps it in JSONResponse. You only need to import and use it explicitly when you want to set custom headers, cookies, or status codes that can't be expressed with just a return value.
6.2.2 ORJSONResponse โ€” Faster JSON
โšก
ORJSONResponse โ€” High-Performance JSON
โ–ผ

orjson is a Rust-powered JSON library that is 2โ€“10ร— faster than Python's built-in json module. Use it when your API returns large payloads or handles high traffic. Requires pip install orjson.

python
from fastapi import FastAPI
from fastapi.responses import ORJSONResponse

# Option 1: Use it on a single endpoint
app = FastAPI()

@app.get("/big-data", response_class=ORJSONResponse)
async def get_big_data():
    return {"items": list(range(10000))}

# Option 2: Set it as the DEFAULT for the entire app
app = FastAPI(default_response_class=ORJSONResponse)

@app.get("/users")
async def get_users():
    return [{"id": i, "name": f"User {i}"} for i in range(1000)]
    # Uses orjson serialization automatically
FeatureJSONResponseORJSONResponse
SpeedStandard2โ€“10ร— faster
datetime supportNeeds manual conversionNative support
numpy/dataclassesNoYes
Extra dependencyNonepip install orjson
Drop-in replacementโ€”Yes
6.2.3 HTMLResponse
๐ŸŒ
HTMLResponse โ€” Serving HTML Pages
โ–ผ

Return raw HTML from your FastAPI endpoint. The browser will render it. Useful for simple health pages, email previews, admin dashboards, or when using Jinja2 templates.

python
from fastapi import FastAPI
from fastapi.responses import HTMLResponse

app = FastAPI()

# Method 1: response_class on the route
@app.get("/health", response_class=HTMLResponse)
async def health_page():
    return """
    <html>
      <body style="font-family:sans-serif;padding:40px;">
        <h1>โœ… Service is healthy</h1>
        <p>Version: 1.0.0</p>
      </body>
    </html>
    """

# Method 2: Return HTMLResponse directly (custom status/headers)
@app.get("/maintenance")
async def maintenance_page():
    html = "<h1>Under Maintenance</h1>"
    return HTMLResponse(content=html, status_code=503)

# Method 3: With Jinja2 templates (common in real apps)
from fastapi.templating import Jinja2Templates
from fastapi import Request

templates = Jinja2Templates(directory="templates")

@app.get("/dashboard", response_class=HTMLResponse)
async def dashboard(request: Request):
    return templates.TemplateResponse(
        request,
        "dashboard.html",
        {"user": "Alice", "items": [1, 2, 3]}
    )
6.2.4 PlainTextResponse
๐Ÿ“„
PlainTextResponse โ€” Raw Text Output
โ–ผ

Returns a raw string with Content-Type: text/plain. Useful for logs, health probes (Kubernetes), CSV previews, or any human-readable text output.

python
from fastapi import FastAPI
from fastapi.responses import PlainTextResponse

app = FastAPI()

# Simple health probe (e.g. Kubernetes liveness check)
@app.get("/ping", response_class=PlainTextResponse)
async def ping():
    return "pong"

# Return CSV as plain text
@app.get("/export/csv")
async def export_csv():
    csv_data = "id,name,price\n1,Laptop,75000\n2,Phone,30000"
    return PlainTextResponse(
        content=csv_data,
        headers={"Content-Disposition": 'attachment; filename="data.csv"'}
    )
6.2.5 FileResponse
๐Ÿ“
FileResponse โ€” Sending Files from Disk
โ–ผ

FileResponse reads a file from disk and streams it to the client. It automatically handles MIME types, Content-Disposition, byte-range requests, and efficient file streaming. Perfect for downloads, reports, images.

python
from fastapi import FastAPI
from fastapi.responses import FileResponse
import os

app = FastAPI()

# Basic file download
@app.get("/download/report")
async def download_report():
    path = "reports/monthly_report.pdf"
    return FileResponse(
        path=path,
        filename="report.pdf",       # Name the client sees
        media_type="application/pdf"  # MIME type
    )

# Dynamic file (generated on the fly)
@app.get("/export/{user_id}")
async def export_user_data(user_id: int):
    # Generate the file first
    path = f"/tmp/export_{user_id}.csv"
    with open(path, "w") as f:
        f.write(f"id,user_id\n1,{user_id}")

    return FileResponse(
        path=path,
        filename=f"user_{user_id}_export.csv",
        media_type="text/csv",
        background=None  # optionally clean up with BackgroundTask
    )

# Serve images
@app.get("/avatar/{user_id}")
async def get_avatar(user_id: int):
    path = f"avatars/{user_id}.png"
    if not os.path.exists(path):
        path = "avatars/default.png"
    return FileResponse(path, media_type="image/png")
โœ…
FileResponse uses starlette's background file streaming โ€” it doesn't load the entire file into memory. This makes it safe for large files (videos, large CSVs, zips).
6.2.6 RedirectResponse
โ†—๏ธ
RedirectResponse โ€” HTTP Redirects
โ–ผ

Sends an HTTP redirect response with a Location header. The client browser (or HTTP client) will follow the redirect automatically.

301
Moved Permanently โ€” old URL is gone forever (search engines update their index)
302
Found (Temporary) โ€” default for RedirectResponse
307
Temporary Redirect โ€” like 302 but preserves the HTTP method (POST stays POST)
308
Permanent Redirect โ€” like 301 but preserves the HTTP method
python
from fastapi import FastAPI
from fastapi.responses import RedirectResponse

app = FastAPI()

# Redirect root to /docs
@app.get("/")
async def root():
    return RedirectResponse(url="/docs")  # 307 by default

# Permanent redirect (301) โ€” old URL renamed
@app.get("/old-path")
async def old_path():
    return RedirectResponse(url="/new-path", status_code=301)

# Post-login redirect
@app.post("/login")
async def login(credentials: dict):
    # After successful login, redirect to dashboard
    return RedirectResponse(url="/dashboard", status_code=303)
    # 303 See Other โ€” correct code after a POST action

# Conditional redirect
@app.get("/v1/users")
async def users_v1():
    return RedirectResponse(url="/v2/users", status_code=308)
6.2.7 StreamingResponse
๐Ÿ“ก
StreamingResponse โ€” Generators & Real-Time Output
โ–ผ

StreamingResponse takes an iterator or generator and sends data chunk by chunk โ€” the response starts immediately without waiting for everything to be ready. Essential for large data, AI token streaming, and SSE (Server-Sent Events).

python
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import asyncio

app = FastAPI()

# 1. Stream a large CSV without loading it into memory
def generate_csv():
    yield "id,name,score\n"  # header
    for i in range(1, 1000001):  # 1 million rows!
        yield f"{i},User{i},{i * 1.5}\n"

@app.get("/export/large-csv")
async def stream_large_csv():
    return StreamingResponse(
        generate_csv(),
        media_type="text/csv",
        headers={"Content-Disposition": 'attachment; filename="data.csv"'}
    )

# 2. Server-Sent Events (SSE) โ€” live updates to browser
async def event_generator():
    for i in range(10):
        yield f"data: Event {i}\n\n"  # SSE format requires \n\n
        await asyncio.sleep(1)

@app.get("/events")
async def sse_events():
    return StreamingResponse(
        event_generator(),
        media_type="text/event-stream"  # SSE content type
    )

# 3. AI token streaming (LLM-style response)
async def stream_llm_tokens(prompt: str):
    words = f"This is a streamed response for: {prompt}".split()
    for word in words:
        yield word + " "
        await asyncio.sleep(0.1)  # simulate LLM latency

@app.get("/ai/stream")
async def ai_stream(prompt: str = "hello"):
    return StreamingResponse(
        stream_llm_tokens(prompt),
        media_type="text/plain"
    )
๐Ÿ“ก StreamingResponse Demo
โ†’ Click a button to simulate a StreamingResponse...
โ„น๏ธ
Sync vs Async generators: Use async def generators (with await) when your data source is async (DB, external API). Use regular def generators for CPU-bound or file-based streaming. FastAPI handles both correctly.
6.2.8 Choosing the Right Response โ€” Decision Guide
๐ŸŽฏ
Interactive Decision Helper
โ–ผ
Which Response Class?
โ†’ Select a scenario above...
๐Ÿง 
Quick Check โ€” 6.2 Response Classes
โ–ผ
You're building an AI chat endpoint. The LLM produces tokens one-by-one and you want the client to see them appear in real time. Which response class fits best?
A. JSONResponse โ€” wait for all tokens and return at once
B. FileResponse โ€” write tokens to a temp file, then send
C. StreamingResponse with an async generator yielding each token
D. HTMLResponse โ€” render tokens inside an HTML page
๐Ÿ“š Topic 6 โ€” Responses Summary
response_model
Declare output shape on a route โ€” auto-filters & validates the return value
response_model_include
Only include these fields in the response
response_model_exclude
Remove specific fields from the response
response_model_exclude_none
Strip fields whose value is None from the response
JSONResponse
Default JSON output. Use explicitly for custom headers/status codes.
ORJSONResponse
Drop-in faster JSON via Rust-powered orjson. Set as default_response_class for best perf.
HTMLResponse
Return raw HTML. Use with Jinja2 templates for web UIs.
PlainTextResponse
Raw text โ€” health probes, CSV previews, plain messages.
FileResponse
Stream a file from disk โ€” efficient, handles MIME & headers automatically.
RedirectResponse
HTTP 301/302/307/308 redirect. Use 303 after POST actions.
StreamingResponse
Yield data chunks in real time โ€” large files, SSE, AI token streaming.