python 39 lines · 5 steps

Content negotiation in FastAPI

One endpoint serves the same report as JSON or CSV based on the client's Accept header.

Explained by highlit
1import csv
2import io
3from typing import Annotated
4 
5from fastapi import APIRouter, Depends, Header, Response
6from fastapi.responses import JSONResponse, StreamingResponse
7 
8router = APIRouter()
9 
10 
11def negotiate_media_type(accept: Annotated[str, Header()] = "application/json") -> str:
12 if "text/csv" in accept:
13 return "text/csv"
14 return "application/json"
15 
16 
17def rows_to_csv(rows: list[dict]) -> str:
18 buffer = io.StringIO()
19 writer = csv.DictWriter(buffer, fieldnames=list(rows[0].keys()))
20 writer.writeheader()
21 writer.writerows(rows)
22 return buffer.getvalue()
23 
24 
25@router.get("/reports/daily-sales")
26async def daily_sales(
27 media_type: Annotated[str, Depends(negotiate_media_type)],
28 sales_repo: Annotated[SalesRepository, Depends(get_sales_repo)],
29) -> Response:
30 rows = await sales_repo.aggregate_by_day()
31 
32 if media_type == "text/csv":
33 return StreamingResponse(
34 iter([rows_to_csv(rows)]),
35 media_type="text/csv",
36 headers={"Content-Disposition": "attachment; filename=daily-sales.csv"},
37 )
38 
39 return JSONResponse(content={"results": rows})
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Reading the Accept header lets a single endpoint serve multiple representations of the same data.
  2. 2Packaging negotiation logic in a dependency keeps the route handler focused on producing data.
  3. 3StreamingResponse with a Content-Disposition header turns generated text into a downloadable file.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Content negotiation in FastAPI — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code