Routage et liste d’articles & page de détail

Dans ce chapitre, vous apprendrez à utiliser APIRouter pour découper les routes, en implémentant la liste d’articles de la page d’accueil et la page de détail d’un article.


APIRouter — la solution de FastAPI pour découper les routes

APIRouter est analogue au Blueprint de Flask et à include() de Django.

Exemple

# Chemin du fichier : routers/posts.py
from fastapi import APIRouter, Depends, HTTPException, Demande
from sqlalchemy.orm import Session
from database import get_db
from models import Post, Category
from schemas import PostResponse

router = APIRouter(
    prefix="/posts",       # Toutes les routes ont automatiquement le préfixe /posts
    tags=["Article"]          # Regrouper les tags dans la documentation Swagger
)

@router.get("/{post_id}", response_model=PostResponse, name="post_detail")
def post_detail(post_id: int, db: Session = Depends(get_db)):
    """Détail de l'article (API JSON)"""
    post = db.query(Post).filter(Post.id == post_id).first()
    if not post:
        raise HTTPException(status_code=404, detail=“L'article n'existe pas”)
    return post

Monter APIRouter dans main.py :

Exemple

from routers.posts import router as posts_router
from routers.categories import router as categories_router

app.include_router(posts_router)
app.include_router(categories_router)

Afficher la page de liste des articles

Exemple

# Chemin du fichier : routers/posts.py
from fastapi import APIRouter, Depends, Request, Requête
from fastapi.templating import Jinja2Templates
from sqlalchemy.orm import Session
from database import get_db
from models import Post, Category

router = APIRouter()
templates = Jinja2Templates(directory="templates")

@router.get("/", name="index")
def index(
    request: Request,
    category: str | None = Query(None),   # Paramètres de requête : facultatifs
    db: Session = Depends(get_db)
):
    """Page d'accueil : liste des articles + filtre par catégorie"""
    # Requête de base
    posts_query = db.query(Post).order_by(Post.created_at.desc())

    # Filtre de catégorie
    if category:
        posts_query = posts_query.join(Category).filter(Category.slug == category)

    posts = posts_query.all()
    categories = db.query(Category).all()

    return templates.TemplateResponse("index.html", {
        "request": request,
        "posts": posts,
        "categories": categories,
        "category_slug": category or ""
    })

str | None = Query(None)Ceci est la syntaxe d'union de types de Python 3.10+, équivalente àOptional[str]. FastAPI, en se basant automatiquement sur les annotations de type : 1. rend le paramètre facultatif ; 2. le marque comme facultatif dans la documentation Swagger ; 3. ne lève pas d'erreur lorsqu'il n'est pas fourni.


Afficher la page de détail de l'article

Exemple

# Chemin du fichier : routers/posts.py, ajout
@router.get("/post/{post_id}", name="post_detail")
def post_detail(request: Request, post_id: int, db: Session = Depends(get_db)):
    """Page de détail de l'article"""
    post = db.query(Post).filter(Post.id == post_id).first()
    if not post:
        raise HTTPException(status_code=404, detail=“L'article n'existe pas”)
    return templates.TemplateResponse("post_detail.html", {
        "request": request,
        "post": post
    })

Notez la distinction : une même route peut fournir à la fois une API JSON (déclarant response_model) et une page HTML (renvoyant TemplateResponse). La pratique courante est la suivante : les routes API sont placées sous le préfixe /api/, tandis que les routes de page sont placées au chemin racine.


Créer les templates des pages d'accueil et de détail

Exemple

<!-- Chemin du fichier : templates/index.html -->
{% extends 'base.html' %}

{% block title %}EXAMPLE Blog - Accueil{% endblock %}

{% block content %}
<h2 class="section-title">Derniers articles</h2>

<div class="category-bar">
    <a href="/" class="{% if not category_slug %}active{% endif %}">Toutes</a>
    {% for cat in categories %}
    <a href="/?category={{ cat.slug }}"
      class="{% if category_slug == cat.slug %}active{% endif %}">
        {{ cat.name }}
    </a>
    {% endfor %}
</div>

{% if posts %}
<div class="article-grid">
    {% for post in posts %}
    <a href="/post/{{ post.id }}" class="card-link">
        <div class="article-card">
            <div class="card-content">
                <span class="card-category">{{ post.category.name }}</span>
                <h3>{{ post.title }}</h3>
                <p>{{ post.summary|truncate(80) }}</p>
                <span class="card-date">{{ post.created_at.strftime('%Y-%m-%d') }}</span>
            </div>
        </div>
    </a>
    {% endfor %}
</div>
{% else %}
<p class="empty-tip">Pas encore d'articles dans cette catégorie.</p>
{% endif %}
{% endblock %}

Exemple

<!-- Chemin du fichier : templates/post_detail.html -->
{% extends 'base.html' %}

{% block title %}{{ post.title }} - Blog EXEMPLE{% endblock %}

{% block content %}
<article class="post-view">
    <span class="category-tag">{{ post.category }}</span>
    <h1>{{ post.title }}</h1>
    <time>{{ post.created_at.strftime('%Y-%m-%d') }}</time>
    <div class="content">{{ post.content|safe }}</div>
    <a href="/" class="back-link">← Retour à l'accueil</a>
</article>
{% endblock %}

Résumé du chapitre

Dans ce chapitre, vous avez terminé le développement du routage principal de FastAPI : APIRouter divise les routes par module (préfixe + tags), les paramètres de requête sont déclarés à l'aide de Query() + annotations de type, TemplateResponse rend les pages, et la gestion des exceptions 404.

Le blog dispose maintenant de trois pages complètes : la liste des articles de la page d'accueil, le filtrage par catégorie et la page de détail.

autres extensions