response_model. This gives you automatic filtering, validation, and OpenAPI documentation of your output โ not just your input.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.
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.
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!
response_model works with any type โ lists, Optional types, Union types, or nested models.
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
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.
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
UserOut) is preferred in real projects because it's reusable, self-documenting, and appears correctly in OpenAPI. Use include/exclude only for quick prototyping.UserInDB object that contains hashed_password. How do you prevent it from appearing in the JSON response?response_model=UserOut where UserOut doesn't have the hashed_password fieldreturn JSONResponse(content=...) and manually omit ithashed_password as private=True in PydanticJSONResponse 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.
orjson library. Drop-in replacement.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.
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"}
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.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.
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
| Feature | JSONResponse | ORJSONResponse |
|---|---|---|
| Speed | Standard | 2โ10ร faster |
| datetime support | Needs manual conversion | Native support |
| numpy/dataclasses | No | Yes |
| Extra dependency | None | pip install orjson |
| Drop-in replacement | โ | Yes |
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.
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]} )
Returns a raw string with Content-Type: text/plain. Useful for logs, health probes (Kubernetes), CSV previews, or any human-readable text output.
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"'} )
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.
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).Sends an HTTP redirect response with a Location header. The client browser (or HTTP client) will follow the redirect automatically.
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)
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).
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" )
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.JSONResponse โ wait for all tokens and return at onceFileResponse โ write tokens to a temp file, then sendStreamingResponse with an async generator yielding each tokenHTMLResponse โ render tokens inside an HTML page