Réponses personnalisées avec FastAPI
- FastAPI LLM Wiki
- API de base
- Advanced FastAPI
- Authentification avec FastAPI
- Relations SQL avec FastAPI
- Gestion d’erreurs avec FastAPI
- Middleware avec FastAPI
- Tests avec FastAPI
- Docker avec FastAPI
- Notifications et communication utilisateur avec FastAPI
- Parcours de lecture
- FAQ, erreurs fréquentes et conseils pratiques
- SCHEMA
- Plan directeur
- Journal d’itération
Ce chapitre vient après Docker avec FastAPI.
Il montre comment faire évoluer une API FastAPI au-delà du simple return {...} en utilisant des réponses plus ciblées : JSON enrichi, fichier à télécharger, redirection, HTML rendu par template et classe de réponse personnalisée.
Introduction
Jusqu’ici, notre projet renvoie surtout des réponses JSON classiques.
C’est normal, parce qu’une API FastAPI vit d’abord autour de :
- routes,
- schémas,
- validation,
- erreurs,
- authentification,
- base de données.
Mais dans une vraie application, il arrive souvent qu’on ait besoin de réponses plus spécifiques :
- un JSON un peu plus riche que la sortie brute d’un schéma,
- un fichier à télécharger,
- une redirection vers une autre route,
- une page HTML ciblée,
- un format texte custom pour un besoin particulier.
C’est exactement le sujet de ce chapitre.
Ce que ce chapitre va accomplir
À la fin de ce chapitre, tu auras :
- une compréhension claire de la différence entre :
response_model,response_class,TemplateResponse,- classe
Responsepersonnalisée,
- un fichier
core/responses.py, - un dossier
templates/, - un dossier
exports/, - des exemples concrets dans
api/routers/task.pypour :JSONResponse,FileResponse,RedirectResponse,TemplateResponse,- réponse texte personnalisée,
- des vérifications avec
curl -ipour voir les headers et lescontent-type.
Pourquoi ce sujet arrive maintenant
Ce sujet est un peu différent des chapitres précédents.
Il ne change pas le socle métier autant que :
- l’auth,
- les relations SQL,
- la gestion d’erreurs,
- les tests,
- Docker.
En revanche, il améliore beaucoup la couche de sortie de l’application.
Autrement dit :
- les chapitres précédents ont surtout renforcé le backend interne,
- celui-ci renforce la manière dont l’application répond au client.
Sources d’inspiration réelles
Ce chapitre s’appuie principalement sur :
19-Custom Response/106-app.py19-Custom Response/105-shipment.py19-Custom Response/105-track.html
Ces sources montrent :
JSONResponse,FileResponse,RedirectResponse,- une classe
Responsecustom, - un rendu HTML via
Jinja2Templates.
Choix pédagogique de ce chapitre
Comme d’habitude, on ne copie pas les sources telles quelles.
La source 105-shipment.py parle d’un suivi de livraison.
Pour notre wiki, on adapte cette logique à notre domaine courant, qui reste celui d’une application de tâches.
Donc ici, on transforme les exemples source en cas plus cohérents avec notre projet :
- un JSON enrichi pour une tâche,
- un téléchargement d’exemple d’export,
- une redirection vers un aperçu HTML d’une tâche,
- un template HTML de détail de tâche,
- une réponse texte custom pour un résumé lisible.
Important : response_model ≠ response_class
Avant de coder, il faut bien distinguer deux choses.
response_model
response_model sert surtout à :
- documenter la sortie attendue,
- filtrer/normaliser les données,
- faire apparaître un schéma clair dans OpenAPI.
response_class
response_class sert surtout à :
- choisir comment la réponse est renvoyée,
- contrôler le
content-type, - renvoyer un fichier,
- renvoyer une redirection,
- renvoyer du HTML,
- ou appliquer un rendu personnalisé.
En pratique :
response_modelparle surtout de la forme métier/documentée,response_classparle surtout du transport concret de la réponse.
Architecture visée après ce chapitre
Après ce chapitre, le projet peut ressembler à ceci :
fastapi-base-api/
├── main.py
├── config.py
├── requirements.txt
├── .env
├── .env.example
├── core/
│ ├── __init__.py
│ ├── security.py
│ ├── exceptions.py
│ ├── middleware.py
│ └── responses.py
├── api/
│ ├── __init__.py
│ ├── router.py
│ ├── dependencies.py
│ ├── routers/
│ │ ├── __init__.py
│ │ ├── task.py
│ │ └── user.py
│ └── schemas/
│ ├── __init__.py
│ ├── task.py
│ └── user.py
├── database/
│ ├── __init__.py
│ ├── models.py
│ ├── session.py
│ └── redis.py
├── services/
│ ├── __init__.py
│ ├── task.py
│ └── user.py
├── templates/
│ └── task_detail.html
└── exports/
└── task-api-example.txt
Repères utiles avant le code
Avant de commencer, retiens ceci :
1. Le JSON classique reste la norme
Une API FastAPI reste d’abord une API JSON. Les réponses personnalisées servent à ajouter des cas utiles, pas à remplacer toute l’API.
2. JSONResponse devient utile quand tu veux emballer davantage de contexte
Par exemple :
- un message,
- des liens,
- des métadonnées,
- une structure de réponse que tu veux imposer.
3. FileResponse attend un vrai fichier sur disque
Si le fichier n’existe pas, la route échouera. Donc il faut penser en amont à :
- où stocker le fichier,
- s’il est statique ou généré,
- s’il faut le protéger.
4. TemplateResponse a besoin du request
C’est un détail très important.
Sans request dans le context, le rendu du template cassera.
5. Les classes Response custom sont puissantes, mais à utiliser avec parcimonie
Si tout peut être fait proprement avec :
JSONResponse,FileResponse,RedirectResponse,TemplateResponse,
alors il ne faut pas créer une classe custom juste pour faire "plus avancé".
Étape 1 — vérifier les dépendances utiles
Notre projet a déjà scalar-fastapi dans les chapitres précédents.
Pour le rendu HTML avec templates, ajoute jinja2 si ce n’est pas déjà fait.
Dans requirements.txt :
fastapi
uvicorn[standard]
sqlmodel
sqlalchemy
asyncpg
pydantic-settings
scalar-fastapi
passlib[bcrypt]
PyJWT
python-multipart
email-validator
pytest
pytest-asyncio
httpx
aiosqlite
jinja2
Puis installe ou rebuild selon ton contexte :
pip install -r requirements.txt
ou si tu es dans la stack Docker :
docker compose up --build
Étape 2 — créer les nouveaux dossiers utiles
Crée maintenant :
mkdir -p templates exports
Étape 3 — créer un fichier d’export d’exemple
Pour illustrer FileResponse, on va d’abord utiliser un fichier simple existant sur disque.
Crée exports/task-api-example.txt :
Task API export example
=======================
This file is here to demonstrate FastAPI FileResponse.
- rich JSON responses
- HTML preview pages
- redirect endpoints
- text summary responses
Replace this static example later with a real generated export if needed.
Pourquoi commencer par un fichier statique ?
Parce que pédagogiquement, c’est plus simple.
On veut d’abord comprendre :
- comment
FileResponsefonctionne, - comment renvoyer un fichier proprement,
- quels headers sont envoyés.
Plus tard, tu pourras remplacer ce fichier par :
- un export généré à la demande,
- un CSV,
- un PDF,
- un rapport texte,
- un artefact produit par Celery.
Étape 4 — créer core/responses.py
On va y placer notre classe de réponse personnalisée.
Crée core/responses.py :
from fastapi import Response
class TaskSummaryTextResponse(Response):
media_type = "text/plain; charset=utf-8"
def render(self, content: str) -> bytes:
if content is None:
content = ""
if not isinstance(content, str):
content = str(content)
return content.upper().encode("utf-8")
Pourquoi cette classe est volontairement simple
La source montre une UpperResponse.
Ici, on garde la même idée pédagogique, mais on la renomme pour mieux coller à notre projet.
Le but n’est pas de créer une réponse métier révolutionnaire. Le but est de comprendre :
- comment hériter de
Response, - comment définir un
media_type, - comment surcharger
render().
Étape 5 — préparer le rendu HTML avec Jinja2
On va maintenant créer un petit template HTML de détail de tâche.
Crée templates/task_detail.html :
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Détail tâche #{{ task.id }}</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: Inter, Arial, sans-serif;
background: #f5f7fb;
color: #1f2937;
padding: 32px 16px;
}
.card {
max-width: 760px;
margin: 0 auto;
background: white;
border: 1px solid #e5e7eb;
border-radius: 20px;
padding: 28px;
box-shadow: 0 12px 40px rgba(15, 23, 42, 0.08);
}
.eyebrow {
color: #6b7280;
font-size: 14px;
margin-bottom: 12px;
}
h1 {
font-size: 30px;
margin-bottom: 12px;
}
.description {
color: #4b5563;
line-height: 1.6;
margin-bottom: 24px;
}
.status {
display: inline-block;
margin-bottom: 18px;
padding: 8px 14px;
border-radius: 999px;
background: #ede9fe;
color: #5b21b6;
font-weight: 600;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 16px;
}
.panel {
border: 1px solid #e5e7eb;
border-radius: 16px;
padding: 16px;
background: #fafafa;
}
.panel h2 {
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #6b7280;
margin-bottom: 10px;
}
.panel p {
font-size: 16px;
line-height: 1.5;
}
</style>
</head>
<body>
<main class="card">
<p class="eyebrow">Aperçu HTML rendu par FastAPI</p>
<span class="status">{{ status_label }}</span>
<h1>{{ task.title }}</h1>
<p class="description">{{ task.description }}</p>
<section class="grid">
<article class="panel">
<h2>ID</h2>
<p>#{{ task.id }}</p>
</article>
<article class="panel">
<h2>Statut brut</h2>
<p>{{ task.status.value }}</p>
</article>
<article class="panel">
<h2>Propriétaire</h2>
<p>{{ current_user.name }}</p>
</article>
<article class="panel">
<h2>Email</h2>
<p>{{ current_user.email }}</p>
</article>
</section>
</main>
</body>
</html>
Pourquoi ce template reste volontairement simple
La source 105-track.html est plus riche visuellement.
Ici, on garde l’idée :
- une page HTML rendue côté FastAPI,
- des données injectées par le backend,
- un affichage lisible.
Mais on évite d’introduire trop de bruit visuel dans un chapitre déjà chargé conceptuellement.
Étape 6 — faire évoluer api/routers/task.py
C’est ici qu’on va brancher les différents types de réponses.
Imports à ajouter
from pathlib import Path
from fastapi import APIRouter, Request, status
from fastapi.encoders import jsonable_encoder
from fastapi.responses import FileResponse, JSONResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from api.dependencies import CurrentUserDep, TaskServiceDep
from core.responses import TaskSummaryTextResponse
Préparer les chemins et les templates
Ajoute ensuite en haut du fichier :
BASE_DIR = Path(__file__).resolve().parent.parent.parent
TEMPLATES_DIR = BASE_DIR / "templates"
EXPORTS_DIR = BASE_DIR / "exports"
templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
Pourquoi cette approche de chemin est saine
Le routeur task.py est enfoui dans api/routers/.
Donc si on veut accéder à :
templates/exports/
il faut remonter proprement à la racine du projet.
C’est plus robuste qu’un chemin relatif fragile écrit "à la main".
Étape 7 — ajouter un endpoint JSONResponse
Ajoute cette route :
@router.get("/{task_id}/rich-json", response_class=JSONResponse)
async def get_task_rich_json(
task_id: int,
current_user: CurrentUserDep,
service: TaskServiceDep,
):
task = await service.get_task_for_user(task_id, current_user.id)
return JSONResponse(
content={
"ok": True,
"message": "Task loaded successfully",
"task": jsonable_encoder(task),
"links": {
"preview_html": f"/task/{task.id}/preview",
"summary_text": f"/task/{task.id}/summary.txt",
},
}
)
Ce que montre cet endpoint
Il montre qu’on peut renvoyer plus qu’un simple objet de tâche. On peut ajouter :
- un flag
ok, - un message,
- des liens utiles,
- une structure de réponse plus maîtrisée.
Très important : pourquoi jsonable_encoder() ?
Parce qu’un objet SQLModel/Pydantic n’est pas toujours sérialisable tel quel par JSONResponse.
jsonable_encoder() aide à convertir proprement :
- les objets Pydantic,
- certains enums,
- les dates,
- d’autres types Python utiles.
Étape 8 — ajouter un endpoint FileResponse
Ajoute maintenant :
@router.get("/export/example", response_class=FileResponse)
async def download_task_export_example(current_user: CurrentUserDep):
file_path = EXPORTS_DIR / "task-api-example.txt"
return FileResponse(
path=file_path,
media_type="text/plain",
filename="task-api-example.txt",
)
Ce que montre cet endpoint
Cette route montre comment :
- pointer vers un fichier sur disque,
- forcer un nom de téléchargement,
- renvoyer une réponse dont le contenu n’est pas généré comme JSON.
Pourquoi protéger quand même cette route ?
Même si le fichier ici n’est qu’un exemple, garder current_user: CurrentUserDep rappelle une bonne habitude :
- tout ce qui ressemble à de l’export applicatif n’est pas forcément public.
Étape 9 — ajouter un endpoint RedirectResponse
Ajoute maintenant :
@router.get(
"/{task_id}/go",
response_class=RedirectResponse,
include_in_schema=False,
)
async def redirect_to_task_preview(
task_id: int,
current_user: CurrentUserDep,
service: TaskServiceDep,
):
await service.get_task_for_user(task_id, current_user.id)
return RedirectResponse(
url=f"/task/{task_id}/preview",
status_code=status.HTTP_307_TEMPORARY_REDIRECT,
)
Pourquoi cette redirection est utile
Elle montre un cas simple mais réaliste :
- tu reçois une URL technique,
- tu renvoies ensuite vers une vue plus lisible.
Pourquoi include_in_schema=False ?
Parce qu’une route de redirection pure n’a pas toujours besoin d’encombrer la doc OpenAPI. Elle peut exister comme route auxiliaire interne ou route d’ergonomie.
Pourquoi 307 ?
307 garde mieux la sémantique de la requête que certains redirects plus anciens.
Pour un chapitre pédagogique, c’est une bonne valeur par défaut à connaître.
Étape 10 — ajouter un endpoint TemplateResponse
Ajoute maintenant :
@router.get("/{task_id}/preview", include_in_schema=False)
async def preview_task_html(
request: Request,
task_id: int,
current_user: CurrentUserDep,
service: TaskServiceDep,
):
task = await service.get_task_for_user(task_id, current_user.id)
context = {
"request": request,
"task": task,
"current_user": current_user,
"status_label": task.status.value.replace("_", " ").title(),
}
return templates.TemplateResponse(
name="task_detail.html",
context=context,
)
Ce que montre cet endpoint
Ici, FastAPI ne renvoie pas du JSON. Il rend une vraie page HTML avec :
- les données de la tâche,
- l’utilisateur courant,
- une petite transformation lisible du statut.
Pourquoi request doit être dans context ?
Parce que TemplateResponse s’appuie sur le contexte Starlette/FastAPI.
Sans la clé request, le rendu du template peut échouer.
Pourquoi ce type de route peut être utile
Même dans une API, on peut avoir des cas ciblés comme :
- une page de preview,
- une page de suivi,
- un mini écran interne,
- un rendu HTML de debug/admin,
- une vue simple pour inspection manuelle.
Étape 11 — ajouter un endpoint avec réponse texte custom
Ajoute maintenant :
@router.get("/{task_id}/summary.txt", response_class=TaskSummaryTextResponse)
async def get_task_summary_text(
task_id: int,
current_user: CurrentUserDep,
service: TaskServiceDep,
):
task = await service.get_task_for_user(task_id, current_user.id)
return (
f"task #{task.id} | {task.title} | "
f"status={task.status.value} | owner={current_user.email}"
)
Ce que montre cet endpoint
Ici, on voit concrètement le rôle de notre classe TaskSummaryTextResponse :
- la route retourne un texte simple,
- la classe custom décide ensuite comment ce texte est rendu,
- ici le rendu transforme le contenu en majuscules.
Encore une fois, le but n’est pas de dire que toutes les APIs ont besoin de ça. Le but est de comprendre comment FastAPI laisse personnaliser le rendu.
Étape 12 — ordre logique des routes dans task.py
Tu peux ranger ces routes comme ceci :
# routes JSON API classiques
# routes enrichies JSON / export / preview / summary
L’important est de garder une séparation mentale claire :
- routes CRUD cœur métier,
- routes de représentation / rendu / export.
C’est plus lisible que de tout mélanger sans logique.
Étape 13 — bonus : lien avec /scalar
La source 106-app.py montre aussi une route /scalar.
Bonne nouvelle : notre projet a déjà repris cette idée dans les chapitres précédents.
Donc ici, il n’y a rien d’obligatoire à ajouter de plus pour ce point.
L’idée importante à retenir est simplement que FastAPI peut aussi servir :
- des docs alternatives,
- du HTML,
- du fichier,
- pas seulement du JSON brut.
Vérifications pratiques avec curl
Dans les exemples suivants, remplace TOKEN par un vrai JWT obtenu au chapitre Authentification avec FastAPI.
1. Vérifier le JSON enrichi
curl -i \
-H "Authorization: Bearer TOKEN" \
http://localhost:8000/task/1/rich-json
Tu dois observer :
- un
200 OK, - un
content-type: application/json, - une structure avec
ok,message,task,links.
2. Vérifier le téléchargement du fichier
curl -i \
-H "Authorization: Bearer TOKEN" \
http://localhost:8000/task/export/example
Tu dois observer :
- un
200 OK, - un header
content-disposition, - un
content-typetexte.
3. Vérifier la redirection
curl -i \
-H "Authorization: Bearer TOKEN" \
http://localhost:8000/task/1/go
Tu dois observer :
- un
307 Temporary Redirect, - un header
location: /task/1/preview.
4. Suivre réellement la redirection
curl -i -L \
-H "Authorization: Bearer TOKEN" \
http://localhost:8000/task/1/go
Tu dois alors finir sur la page HTML.
5. Vérifier directement le rendu HTML
curl -i \
-H "Authorization: Bearer TOKEN" \
http://localhost:8000/task/1/preview
Tu dois observer :
- un
200 OK, - un
content-type: text/html.
6. Vérifier la réponse texte custom
curl -i \
-H "Authorization: Bearer TOKEN" \
http://localhost:8000/task/1/summary.txt
Tu dois observer :
- un
200 OK, - un
content-type: text/plain, - un contenu en majuscules.
Erreurs fréquentes
Erreur 1 — Object of type Task is not JSON serializable
Cause fréquente :
- tu passes directement un objet SQLModel à
JSONResponse.
Correction :
- utilise
jsonable_encoder(task).
Erreur 2 — le template casse au rendu
Cause fréquente :
- tu as oublié
requestdans lecontext.
Correction :
context = {
"request": request,
...
}
Erreur 3 — FileResponse ne trouve pas le fichier
Cause fréquente :
- mauvais chemin,
- dossier
exports/absent, - fichier non créé.
Correction :
- vérifie le chemin
EXPORTS_DIR / "task-api-example.txt".
Erreur 4 — tu pollues la doc OpenAPI avec des routes HTML auxiliaires
Cause fréquente :
- routes utilitaires laissées visibles dans le schema.
Correction :
- ajoute
include_in_schema=Falsesur les routes qui ne sont pas destinées à la doc API publique.
Erreur 5 — tu crées trop vite des classes custom
Cause fréquente :
- envie de faire "avancé" alors qu’une réponse standard suffisait.
Correction :
- garde les classes custom pour les vrais besoins de rendu.
Bonnes pratiques à retenir
1. Garde le JSON comme sortie principale
Les réponses personnalisées sont des compléments ciblés.
2. Centralise les classes custom
core/responses.py est un meilleur endroit que de disperser des classes dans plusieurs routeurs.
3. Sépare logique métier et logique de rendu
Le service récupère la donnée. La route choisit comment la renvoyer.
4. Protège les routes de preview/export si nécessaire
Le fait qu’une route renvoie du HTML ou un fichier ne veut pas dire qu’elle doit devenir publique.
5. Utilise include_in_schema=False avec intention
Très utile pour les routes utilitaires, previews, redirections techniques.
Checklist de validation
À la fin, tu dois pouvoir cocher tout ceci :
- j’ai ajouté
jinja2si nécessaire - j’ai créé
core/responses.py - j’ai créé
templates/task_detail.html - j’ai créé
exports/task-api-example.txt - je sais configurer
Jinja2Templates - je comprends la différence entre
response_modeletresponse_class - je sais quand utiliser
JSONResponse - je sais quand utiliser
FileResponse - je sais quand utiliser
RedirectResponse - je sais rendre du HTML avec
TemplateResponse - je sais créer une réponse custom simple en héritant de
Response
Résumé final
Dans ce chapitre, tu as appris que FastAPI ne se limite pas à renvoyer du JSON "par défaut".
Tu peux désormais ajouter proprement :
- un JSON enrichi,
- un téléchargement de fichier,
- une redirection,
- un rendu HTML,
- une classe de réponse personnalisée.
Le plus important à retenir est simple :
- la donnée métier reste dans les services,
- la route choisit le type de représentation,
- FastAPI te donne plusieurs manières de rendre cette représentation proprement selon le besoin.
Pour aller plus loin
- Parcours de lecture — pour replacer ce chapitre dans le bon ordre du bloc avancé
- FAQ, erreurs fréquentes et conseils pratiques — pour retrouver les pièges de rendu et d’intégration les plus probables
- Advanced FastAPI — pour garder la vue d’ensemble du bloc avancé
- Notifications et communication utilisateur avec FastAPI — pour continuer ensuite sur les flows utilisateur qui s’appuient aussi sur des templates et des sorties contrôlées