By default FastAPI returns JSONResponse. But APIs also serve files, CSV exports, redirects, generated images or infinite event streams — each has a dedicated response class that controls media type and how the body is produced.
JSONResponse (default) — serializes to JSON with the standard encoder.ORJSONResponse — much faster, via orjson (pip install orjson); set as default with FastAPI(default_response_class=ORJSONResponse).PlainTextResponse — text/plain (health checks, webhooks echoes).HTMLResponse — text/html (templates, small pages).RedirectResponse — 307/302 redirect to another URL.FileResponse — efficiently streams a file from disk with proper headers (ETag, content-length).StreamingResponse — chunks from any generator/async iterator.from fastapi import FastAPI
from fastapi.responses import RedirectResponse, FileResponse, ORJSONResponse
app = FastAPI()
@app.get("/old-docs/")
def old_docs():
return RedirectResponse(url="/docs/", status_code=307)
@app.get("/reports/{report_id}")
def download_report(report_id: int):
return FileResponse(
path=f"reports/{report_id}.pdf",
media_type="application/pdf",
filename="report.pdf", # forces "Save as" with this name
)
When you return a Response object directly, FastAPI passes it through untouched — response_model is not applied and headers/status are exactly what you set.
import csv
from io import StringIO
from fastapi.responses import StreamingResponse
def rows_to_csv(rows):
buffer = StringIO()
writer = csv.writer(buffer)
for row in rows: # generator: one row in memory at a time
writer.writerow(row)
yield buffer.getvalue()
buffer.seek(0)
buffer.truncate(0)
@app.get("/export/items/")
def export_items():
return StreamingResponse(
rows_to_csv(query_all_items()), # any iterable or async generator
media_type="text/csv",
headers={"Content-Disposition": 'attachment; filename="items.csv"'},
)
Memory usage stays constant even for millions of rows — the client starts downloading immediately while the generator yields chunks. Async generators (async def ... yield) work too, e.g. streaming from a server-sent events (SSE) source: media type text/event-stream.
from fastapi.responses import ORJSONResponse
app = FastAPI(default_response_class=ORJSONResponse) # app-wide
router = APIRouter(default_response_class=ORJSONResponse) # per-router
@app.get("/txt/", response_class=PlainTextResponse) # per-endpoint
def ping():
return "pong"
Precedence: endpoint > router > app. Note that response_model still validates data before the response class serializes it.
from fastapi import FastAPI, Response
@app.get("/jobs/{job_id}")
def get_job(job_id: int, response: Response):
response.headers["X-Job-Status"] = "running"
response.set_cookie("last_job", str(job_id))
return {"job_id": job_id} # body + the headers set above
Injecting Response lets you add headers/cookies while still returning a normal object (the default serialization applies). Returning a Response directly is only needed when replacing the body generation itself.
A: FileResponse for existing files on disk — it uses efficient OS-level file serving, sets content-length and supports ETag/range semantics. StreamingResponse for data generated on the fly (DB exports, transformations) or fetched from another service.
A: Something is buffering it: a reverse proxy (Nginx proxy_buffering on), GZip middleware (needs enough bytes), or the client itself. For SSE, disable proxy buffering (X-Accel-Buffering: no header) and yield small chunks.
A: Yes — return the Response object for the file case and a dict for the JSON case. For accurate docs declare responses={200: {"content": {"application/json": {}}, "application/pdf": {}}} so the OpenAPI schema shows both media types.
Choose the response class deliberately: ORJSONResponse for speed, FileResponse for downloads, StreamingResponse for generated or unbounded data, RedirectResponse for moves. Default serialization + response_model still apply unless you take full control by returning a Response yourself.