Cheatsheet for the output side. Long-form: Textbook Ch 4 .
response_model
class UserOut(BaseModel):
id: int
email: EmailStr
full_name: str
@app.get("/users/{id}", response_model=UserOut)
async def get_(id: int):
user = await db.get_user(id)
return user # SQLAlchemy / dict / Pydantic — Pydantic shapes it
Exclude / include
@app.get(
"/users/{id}",
response_model=UserOut,
response_model_exclude={"email"},
response_model_exclude_none=True,
response_model_exclude_unset=True,
response_model_exclude_defaults=False,
)
from_attributes (ORM)
class UserOut(BaseModel):
id: int
email: str
model_config = {"from_attributes": True}
Now FastAPI can return SQLAlchemy instances directly.
Status codes
from fastapi import status
@app.post("/users", status_code=status.HTTP_201_CREATED, response_model=UserOut)
@app.delete("/users/{id}", status_code=204) # no body
Setting headers / cookies
from fastapi import Response
@app.post("/login")
async def login(response: Response):
response.set_cookie("sid", "abc", httponly=True, secure=True, samesite="lax", max_age=86400)
response.headers["x-custom"] = "v"
return {"ok": True}
Response classes
from fastapi.responses import (
JSONResponse, ORJSONResponse, UJSONResponse,
HTMLResponse, PlainTextResponse, RedirectResponse,
StreamingResponse, FileResponse,
)
# Default
app = FastAPI(default_response_class=ORJSONResponse) # faster JSON
@app.get("/", response_class=HTMLResponse)
async def home():
return "<h1>Hi</h1>"
@app.get("/legacy")
async def redir():
return RedirectResponse(url="/", status_code=308)
Streaming
@app.get("/stream")
async def stream():
async def gen():
for i in range(10):
yield f"chunk {i}\n".encode()
return StreamingResponse(gen(), media_type="text/plain")
Server-Sent Events
@app.get("/events")
async def events(request: Request):
async def gen():
while True:
if await request.is_disconnected(): break
yield f"data: {json.dumps({'t': time.time()})}\n\n"
await asyncio.sleep(15)
return StreamingResponse(
gen(),
media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
)
File download
@app.get("/dl/{name}")
async def dl(name: str):
return FileResponse(f"/files/{name}", filename=name, media_type="application/octet-stream")
Multiple responses (OpenAPI)
@app.get(
"/users/{id}",
response_model=UserOut,
responses={
404: {"description": "Not found", "model": ErrorOut},
429: {"description": "Rate limited"},
},
)
async def get_(id: int): ...
Generic page envelope
from typing import TypeVar, Generic
T = TypeVar("T")
class Page(BaseModel, Generic[T]):
items: list[T]
next_cursor: str | None = None
has_more: bool
@app.get("/users", response_model=Page[UserOut])
async def list_(): ...
Custom field serializer
from pydantic import field_serializer
class Event(BaseModel):
when: datetime
@field_serializer("when", when_used="json")
def to_iso(self, v, _info): return v.isoformat()
Cache headers
@app.get("/posts")
async def posts(response: Response):
response.headers["Cache-Control"] = "public, max-age=60, s-maxage=120"
return await db.list_posts()
ETag
@app.get("/u/{id}")
async def u(id: int, if_none_match: str | None = Header(None)):
user = await db.get_user(id)
etag = compute_etag(user)
if if_none_match == etag:
return Response(status_code=304)
return JSONResponse(user.model_dump(), headers={"ETag": etag})
Common gotcha
Returning a SQLAlchemy User without response_model may leak fields like password_hash. Always set response_model.
Read this next
If you want my response-shape conventions doc, it’s at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .