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
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Reading the Accept header lets a single endpoint serve multiple representations of the same data.
- 2Packaging negotiation logic in a dependency keeps the route handler focused on producing data.
- 3StreamingResponse with a Content-Disposition header turns generated text into a downloadable file.
Related explainers
typescript
import { registerLocaleData } from '@angular/common'; import localeFr from '@angular/common/locales/fr'; import localeFrExtra from '@angular/common/locales/extra/fr'; import localeDe from '@angular/common/locales/de';
Locale-aware bootstrapping in Angular
i18n
localization
dependency-injection
Intermediate
8 steps
python
from fastapi import FastAPI, WebSocket, WebSocketDisconnect app = FastAPI()
Building a WebSocket chat with FastAPI
websockets
broadcast
connection-management
Intermediate
9 steps
typescript
import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import * as Joi from 'joi';
Validating env config at boot in NestJS
configuration
schema-validation
environment-variables
Intermediate
8 steps
java
@Component @Converter public class EncryptedStringConverter implements AttributeConverter<String, String> {
Transparent column encryption in Spring & JPA
encryption
aes-gcm
jpa-converter
Advanced
10 steps
python
import time import uuid from django.utils.deprecation import MiddlewareMixin
Attaching per-request context in Django
middleware
request lifecycle
multi-tenancy
Intermediate
7 steps
typescript
import { Inject, Injectable, Logger } from '@nestjs/common'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Cache } from 'cache-manager'; import { InjectRepository } from '@nestjs/typeorm';
A cache-aside country lookup in NestJS
cache-aside
dependency-injection
batch-lookup
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/content-negotiation-in-fastapi-explained-python-c152/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.