Response Classes and Streaming

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.

The Response Family

Returning a Response Directly vs response_class

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.

StreamingResponse: CSV Export Example

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.

Setting Default Response Classes

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.

Headers, Cookies and Status on Any Response

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.

Quick Questions and Answers

Q: StreamingResponse vs FileResponse for downloads — which one?

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.

Q: Why does my streamed response arrive all at once at the end?

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.

Q: Can I mix response types in one endpoint (e.g. JSON or file)?

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.

Conclusion

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.