FastAPI + HTMX : le full-stack sans étape de build
# Créez des applications web prêtes pour la production sans React ni webpack : FastAPI, HTMX, Alpine.js, Jinja2, CSS simple, modèles Bootstrap, i18n, déploiement, SEO et performances.
En bref : FastAPI + HTMX + Alpine.js + Jinja2 + du CSS simple produisent des applications web de production sans aucun outil de build, sans aucun
node_modules/, avec des scores Lighthouse parfaits. Ce guide couvre tout le système, de l’architecture au déploiement, en utilisant blakecrosley.com comme référence en production : 210 articles de blog, des composants JavaScript interactifs, 11 guides fondamentaux, 48 études de design, ainsi que l’anglais et 9 langues traduites, sans le moindre bundler, compilateur ou transpilateur.1
Le stack moderne de développement web part du principe que vous avez besoin de React, webpack, TypeScript et d’un pipeline de build. Pour une grande catégorie d’applications — sites axés sur le contenu, outils internes, applications CRUD, sites portfolio, plateformes de documentation — cette hypothèse est fausse. Le stack décrit dans ce guide élimine toute la chaîne d’outils de build frontend, tout en produisant des sites qui obtiennent 100/100/100/100 sur Lighthouse.2
Ce n’est pas du militantisme. C’est une mesure. L’architecture décrite ici fonctionne en production, sert de vrais utilisateurs dans dix langues, et les chiffres sont vérifiables.
Points clés
- Le rendu HTML côté serveur élimine trois catégories entières de problèmes : la gestion d’état côté client, les frontières de sérialisation JSON et les incohérences d’hydratation. HTMX fait des réponses du serveur la sortie finale — pas d’étape de rendu côté client.
- Zéro outil de build signifie zéro échec de build. Pas de conflits de dépendances entre pairs lors de
npm install, pas d’erreurs du compilateur TypeScript dans des fichiers que vous n’avez pas touchés, pas de PR Dependabot pour des dépendances transitives que vous n’avez jamais importées. Le pipeline de déploiement se résume àgit push. - Alpine.js gère l’état purement client que HTMX ne peut pas gérer. Les menus déroulants, les modales, les bascules de navigation mobile et tout état d’interface qui existe uniquement dans le navigateur relèvent d’Alpine.js. La frontière est claire : si l’état a besoin du serveur, utilisez HTMX. Sinon, utilisez Alpine.js.
- Le CSS pur avec propriétés personnalisées remplace Sass et Tailwind. Les propriétés personnalisées CSS se propagent en cascade, s’héritent et répondent aux media queries à l’exécution. Les variables de préprocesseur se compilent en valeurs statiques et disparaissent. Le navigateur lit directement les propriétés personnalisées — sans étape de compilation.
- Cette approche a des limites claires. Elle est inadaptée aux grandes équipes qui partagent des interfaces de composants, aux produits SaaS dotés d’un état client complexe, et aux applications qui dépendent des bibliothèques de l’écosystème npm. Le cadre de décision de la section 15 identifie précisément cette frontière.
- blakecrosley.com en est la preuve. Les patterns fondamentaux de ce guide (HTMX, Alpine.js, Jinja2, CSS pur) tournent en production sur blakecrosley.com. Les sections Bootstrap et SQLAlchemy couvrent des patterns standards de la stack qui ne sont pas utilisés sur ce site précis. Chaque affirmation s’appuie sur un chemin de fichier, un bloc de configuration ou un audit Lighthouse que vous pouvez vérifier vous-même sur PageSpeed Insights.2
Comment utiliser ce guide
Il s’agit d’une référence exhaustive. Commencez là où votre niveau d’expérience correspond :
| Expérience | Commencez ici | Puis explorez |
|---|---|---|
| Développeur Python, nouveau venu sur HTMX | La thèse no-build → Vue d’ensemble de l’architecture → HTMX en profondeur | Patterns Alpine.js, Sécurité |
| Développeur React/Vue évaluant des alternatives | La thèse no-build → Cadre de décision | Vue d’ensemble de l’architecture, Performance |
| Développeur FastAPI ajoutant de l’interactivité | HTMX en profondeur → Patterns Alpine.js | i18n et localisation, Déploiement |
| Développeur full-stack partant de zéro | Lisez de manière séquentielle à partir de Vue d’ensemble de l’architecture | Carte de référence rapide pour un usage continu |
Utilisez Ctrl+F / Cmd+F pour rechercher des patterns ou des attributs spécifiques. La Carte de référence rapide à la fin fournit un résumé parcourable.
La thèse no-build
La thèse est étroite et précise : pour les sites pilotés par le contenu avec un développeur solo ou une petite équipe, les outils de build résolvent des problèmes que vous n’avez pas tout en en créant que vous avez.
Voici les véritables métriques de blakecrosley.com :
| Métrique | blakecrosley.com (no-build) | Projet Next.js typique3 |
|---|---|---|
| Dépendances | 17 paquets Python | 311+ paquets npm |
| Fichiers de configuration de build | 0 | 5-8 (next.config, tsconfig, postcss, tailwind, etc.) |
Taille de node_modules/ |
N’existe pas | 187 Mo de base, 250-400 Mo avec ajouts |
| Temps d’installation | pip install : 8 secondes |
npm install : 30-90 secondes |
| Étape de build | Aucune | next build : 15-60 secondes |
| Pipeline de déploiement | git push → en ligne en ~40 secondes |
Install → build → deploy : 2-5 minutes |
| Performance Lighthouse | 100 | 70-90 sans optimisation explicite4 |
Les 17 paquets Python incluent FastAPI, Jinja2, Pydantic, uvicorn, nh3, et 12 autres. Aucun n’est un outil de build. Aucun n’est un compilateur. Aucun n’est un bundler.5
Ce à quoi vous renoncez
L’honnêteté impose de lister les véritables coûts :
Pas de TypeScript. Chaque fichier .js est du JavaScript vanilla. Les erreurs de type sont attrapées par les tests et l’analyse de code, pas par un compilateur. Cela fonctionne pour un développeur solo. Cela ne fonctionnerait pas pour une équipe de 10 personnes partageant des interfaces de composants.
Pas de Hot Module Replacement. Les modifications CSS nécessitent un rafraîchissement manuel du navigateur. Le hx-boost d’HTMX rend la navigation suffisamment rapide pour que les rafraîchissements complets soient tolérables, mais lors de cycles d’itération visuelle serrés, le HMR fait gagner du temps.
Pas de Tree Shaking. Chaque octet de JavaScript que vous écrivez est livré au navigateur. Cette contrainte impose de la discipline : des fichiers petits et ciblés plutôt que de gros modules utilitaires.
Pas de bibliothèques de composants npm. Pas de Radix, pas de shadcn/ui, pas de Headless UI. Chaque élément interactif est construit à la main ou utilise les composants intégrés de Bootstrap 5.
Pas de design tokens issus de npm. Le système de design vit dans les propriétés personnalisées CSS. Il ne peut pas être importé comme paquet dans un autre projet.
Ces compromis sont acceptables pour un site piloté par le contenu avec un à trois développeurs. Ils seraient inacceptables pour un produit SaaS avec une équipe d’ingénierie de 15 personnes. La section 15 fournit le cadre de décision.
Ce que vous gagnez
Zéro échec de build. Aucun npm install ne peut échouer à cause de conflits de dépendances entre pairs. Aucun next build ne peut échouer à cause d’une erreur TypeScript dans un fichier que vous n’avez pas touché.6
Debug via View Source. Le JavaScript qui s’exécute dans le navigateur est le JavaScript que vous avez écrit. Pas besoin de source maps.
Démarrage local instantané. uvicorn app.main:app --reload démarre en moins de 2 secondes.
Cascade de requêtes concrète. Une première visite charge : un document HTML (~15 Ko gzippés), un fichier CSS (~8 Ko), HTMX (~16 Ko, mis en cache), Alpine.js (~15 Ko, mis en cache) et le JS interactif de la page (~4-8 Ko). Total : environ 55-65 Ko à la première visite.1
Frontend pérenne. Le code côté client utilise HTML, CSS et JavaScript — des standards qui ont maintenu une rétrocompatibilité pendant 30 ans.7 Pas de migration Webpack 4 → 5, pas de dépréciation de Create React App, pas de migration vers le App Router de Next.js.
Comparaison de stacks
Comment la stack no-build se compare aux alternatives courantes sur des dimensions mesurables :
| Dimension | FastAPI+HTMX (ce guide) | Next.js (React) | Astro | 11ty |
|---|---|---|---|---|
| JS livré au navigateur | 35-40 Ko (HTMX+Alpine+petits scripts de page) | 85-250 Ko+ (runtime React) | 0 Ko par défaut, îlots opt-in | 0 Ko par défaut |
| Étape de build | Aucune | Requise (webpack/turbopack) | Requise (Vite) | Requise (personnalisée) |
| Fichiers de configuration | 0 | 5-8 (next.config, tsconfig, etc.) | 1-3 (astro.config, tsconfig) | 1-2 (.eleventy.js) |
| Pipeline de déploiement | git push (40 s) |
Install+build+deploy (2-5 min) | Install+build+deploy (1-3 min) | Install+build+deploy (1-2 min) |
| Interactivité côté serveur | Native (HTMX) | Routes API + fetch client | Limitée (actions de formulaire) | Aucune (sortie statique) |
| Gestion d’état client | Alpine.js (15 Ko) | State/context/Redux React | Îlots de framework | JS manuel |
| Langage backend | Python | JavaScript/TypeScript | JavaScript/TypeScript | JavaScript |
| Approche i18n | Côté serveur (middleware) | next-intl ou paquet similaire | @astrojs/i18n | Manuelle |
| Performance Lighthouse | 100 (mesurée) | 70-90 typique4 | 95-100 typique | 95-100 typique |
| Idéal pour | Sites de contenu, CRUD, tableaux de bord | SPA complexes, grandes équipes | Sites de contenu, marketing | Blogs statiques, docs |
Astro et 11ty sont les concurrents les plus proches pour les sites de contenu. Tous deux produisent une excellente sortie statique mais nécessitent une étape de build et une chaîne d’outils JavaScript. La stack FastAPI+HTMX échange la performance des sites statiques contre une interactivité côté serveur (filtrage par catégorie, gestion de formulaires, recherche en temps réel) sans ajouter d’étape de build. Si votre site est purement statique sans interactions serveur, Astro ou 11ty seront peut-être le meilleur choix.
Vue d’ensemble de l’architecture
Flux des requêtes
Chaque requête suit un chemin unique à travers quatre couches :
Browser FastAPI Jinja2 HTMX/Alpine
| | | |
|--- GET /about ------>| | |
| |-- render template ->| |
| | |-- base.html ------->|
| | | + about.html |
| |<-- full HTML -------| |
|<--- HTML response ---| | |
| |
|--- hx-get /search ------------------------------------------------>|
| |<-- HTMX request ----| |
| |-- render partial -->| |
| | |-- _results.html |
| |<-- HTML fragment ---| |
|<--- HTML fragment ---| | |
|--- DOM swap -------------------------------------------------------->|
Les chargements de page complets renvoient des documents HTML entiers (template de base + template de page). Les requêtes HTMX renvoient des fragments HTML (partiels). Le serveur décide quoi afficher en fonction du type de requête. Alpine.js gère l’état côté client qui ne touche jamais le serveur.
Rôles des composants
| Composant | Rôle | Portée |
|---|---|---|
| FastAPI | Routage, logique métier, accès aux données, validation | Serveur |
| Jinja2 | Rendu des templates, héritage, macros | Serveur |
| HTMX | Interactivité pilotée par le serveur (formulaires, pagination, recherche) | Client ↔ Serveur |
| Alpine.js | État côté client uniquement (menus déroulants, modales, bascules) | Client uniquement |
| Bootstrap 5 | Système de grille, classes utilitaires, mise en page responsive | Client (CSS) |
| CSS pur | Propriétés personnalisées, styles de composants, design tokens | Client (CSS) |
| Pydantic | Validation des requêtes/réponses, paramètres | Serveur |
Structure du projet
app/
├── main.py # FastAPI app, middleware, templates
├── config.py # Pydantic settings management
├── routes/
│ ├── pages.py # Page routes (HTML responses)
│ └── api.py # API routes (JSON/HTML fragment responses)
├── content.py # Markdown loading, blog post parsing
├── security/
│ ├── headers.py # CSP, HSTS, security headers middleware
│ ├── csrf.py # HMAC-signed CSRF tokens
│ ├── rate_limit.py # 3-tier rate limiting
│ └── logging.py # Security event logging
├── i18n/
│ ├── config.py # Supported locales, mappings
│ ├── middleware.py # URL-based locale detection
│ ├── jinja.py # Translation functions for templates
│ └── d1_client.py # Cloudflare D1 translation storage
├── cache_assets.py # Content-hash asset versioning
└── templates/
├── base.html # Base layout with Alpine.js state
├── components/ # Reusable partials (_language_switcher.html, etc.)
└── pages/ # Page templates (home.html, about.html, etc.)
content/
├── blog/ # Markdown blog posts with YAML frontmatter
└── guides/ # Multi-section guide markdown
static/
├── css/ # Plain CSS (no preprocessors)
├── js/ # Vanilla JavaScript (no bundlers)
│ └── vendor/ # Self-hosted HTMX, Alpine.js
└── images/ # Optimized images with WebP srcset
Cette structure repose sur un principe unique : chaque répertoire contient un seul type d’élément. Les routes se trouvent dans routes/. Les templates dans templates/. Les ressources statiques dans static/. Aucune étape de build ne transforme l’un en l’autre.
Comparaison avec l’architecture SPA
Dans un projet React + Next.js, la structure équivalente inclurait :
src/
├── components/ # React components (JSX)
├── pages/ # Route handlers (also JSX)
├── api/ # API routes (also in pages/)
├── hooks/ # Custom React hooks
├── context/ # React context providers
├── lib/ # Utility functions
├── styles/ # CSS modules or Tailwind config
└── types/ # TypeScript type definitions
# Plus build configuration
next.config.js
tsconfig.json
postcss.config.js
tailwind.config.js
eslint.config.js
package.json
package-lock.json
node_modules/ # 187+ MB of dependencies
L’architecture SPA nécessite une coordination au moment du build entre ces répertoires. TypeScript compile les fichiers .tsx en JavaScript. PostCSS transforme les directives Tailwind en CSS. Webpack (ou Turbopack) regroupe le résultat en chunks. Chaque étape peut échouer indépendamment.
L’architecture sans build ne nécessite aucune coordination. Le template référence un fichier CSS. Ce fichier CSS existe dans static/css/. Le navigateur le charge directement. Si vous renommez un fichier, la référence dans le template échoue au moment de l’exécution — pas au moment du build. Cela déplace les erreurs de la compilation vers l’exécution, ce qui constitue un véritable compromis. Pour un développeur solo utilisant uvicorn --reload pendant le développement, les erreurs d’exécution apparaissent immédiatement dans le navigateur. Pour une grande équipe, les erreurs de compilation détectées par TypeScript préviennent une catégorie de bugs que les erreurs d’exécution ne peuvent pas intercepter.
Patterns FastAPI
Configuration de l’application
L’application s’initialise dans main.py avec un ordre explicite des middlewares :
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from starlette.middleware.gzip import GZipMiddleware
app = FastAPI(
title="Blake Crosley",
docs_url=None, # Disable docs in production
redoc_url=None,
openapi_url=None, # Prevent /openapi.json exposure
)
# Middleware order matters: last added = first executed
app.add_middleware(SecurityHeadersMiddleware)
app.add_middleware(GZipMiddleware, minimum_size=500)
app.add_middleware(LocaleMiddleware)
app.add_middleware(RateLimitMiddleware)
app.add_middleware(SecurityLogMiddleware, site_name="blakecrosley.com")
# Static files
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
# Templates
templates = Jinja2Templates(directory=TEMPLATES_DIR)
Trois choix de conception comptent ici. Premièrement, docs_url=None et openapi_url=None désactivent les endpoints automatiques de documentation API. Un site de contenu destiné au public n’a pas besoin d’exposer /docs ou /openapi.json sur Internet.8 Deuxièmement, l’ordre des middlewares est important : la journalisation de sécurité s’exécute en premier (car elle est ajoutée en dernier), afin de capturer chaque requête, y compris celles rejetées par la limitation du débit. Troisièmement, GZipMiddleware compresse toutes les réponses de plus de 500 octets, ce qui réduit généralement de 70 à 80 % la taille des transferts HTML.
Routage
Les routes se répartissent en deux catégories : les routes de pages renvoient des documents HTML complets, tandis que les routes API renvoient des fragments JSON ou HTML.
# routes/pages.py — full HTML responses
from fastapi import APIRouter, Request
router = APIRouter()
@router.get("/about")
async def about(request: Request):
templates = request.app.state.templates
return templates.TemplateResponse("pages/about.html", {
"request": request,
"page_title": "About — Blake Crosley",
"page_description": "Designer, developer, dad.",
})
# routes/api.py — JSON or HTML fragment responses
@router.get("/api/quiz/{quiz_id}/step")
async def quiz_step(request: Request, quiz_id: str, answers: str = ""):
# Parse answers, compute next question or result
question = get_next_question(quiz_id, answers)
templates = request.app.state.templates
return templates.TemplateResponse("components/_quiz_step.html", {
"request": request,
"question": question,
"answers": answers,
"step": len(answers.split(",")) if answers else 0,
})
Cette distinction est importante pour HTMX. Les routes de pages complètes renvoient des documents qui étendent base.html. Les routes API renvoient des fragments HTML que HTMX insère dans des éléments DOM existants. Le même moteur de templates Jinja2 effectue les deux rendus : aucune couche API distincte n’est nécessaire.
Injection de dépendances
Le système Depends() de FastAPI sépare proprement les gestionnaires de routes de la logique partagée :
from fastapi import Depends, Request
def get_templates(request: Request):
"""Get templates from app state."""
return request.app.state.templates
def get_current_locale(request: Request) -> str:
"""Get locale from middleware-set request state."""
return getattr(request.state, "locale", "en")
@router.get("/blog/{slug}")
async def blog_post(
request: Request,
slug: str,
templates=Depends(get_templates),
locale: str = Depends(get_current_locale),
):
post = load_post_by_slug(slug)
if not post:
raise HTTPException(404, "Post not found")
return templates.TemplateResponse("pages/blog/post.html", {
"request": request,
"post": post,
"locale": locale,
})
Les dépendances sont composables. Une dépendance get_db peut dépendre de get_current_locale, qui dépend elle-même de la requête. FastAPI résout automatiquement cette chaîne.
Paramètres Pydantic
La configuration utilise BaseSettings de Pydantic, avec priorité aux variables d’environnement :
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
D1_WORKER_URL: str = ""
D1_AUTH_SECRET: str = ""
CLOUDFLARE_ACCOUNT_ID: str = ""
CLOUDFLARE_API_TOKEN: str = ""
ANALYTICS_PASSKEY: str = ""
class Config:
env_file = ".env"
env_file_encoding = "utf-8"
settings = Settings()
Les variables d’environnement remplacent les valeurs du fichier .env. En production (Railway), les secrets sont définis sous forme de variables d’environnement. En local, un fichier .env fournit les valeurs par défaut. La classe Settings valide les types au démarrage : si un champ obligatoire est absent, l’application échoue immédiatement plutôt qu’au moment de l’exécution.
Patterns async
Les routes FastAPI sont async par défaut. Pour les opérations limitées par les entrées-sorties (requêtes de base de données, requêtes HTTP, lectures de fichiers), async évite de bloquer la boucle d’événements :
@asynccontextmanager
async def lifespan(app: FastAPI):
# Load translations into memory cache at startup
async with httpx.AsyncClient() as client:
for locale in SUPPORTED_LOCALES:
resp = await client.post(f"{D1_URL}/query", ...)
TRANSLATIONS[locale] = resp.json()["results"]
yield
# Cleanup on shutdown (if needed)
app = FastAPI(lifespan=lifespan)
Lifespan est désormais l’unique mécanisme de démarrage et d’arrêt. Starlette a publié sa première version stable, la 1.0, en mars 2026 (1.3.1 au 12 juin) et supprimé les hooks on_event, on_startup et on_shutdown, obsolètes depuis longtemps : lifespan (ci-dessus) constitue maintenant le seul mécanisme, tandis que @app.route() et @app.websocket_route() ont cédé la place à Route et WebSocketRoute dans la liste routes. FastAPI 0.137.0 (14 juin 2026) remanie les mécanismes internes de son propre routeur : router.routes n’est plus une liste plate d’objets APIRoute, mais un arbre de nœuds intermédiaires. Considérez-le donc comme un détail interne, et non comme une structure à parcourir. L’avantage : les routes ajoutées à un routeur après include_router() sont désormais répercutées en direct, et un sous-routeur peut être inclus avant la définition de ses routes. FastAPI ne limite pas lui-même Starlette à la branche 1.x : depuis la version 0.136.3 et jusqu’à la 0.140.7, sa dépendance d’exécution se résume à un minimum, starlette>=0.46.0 ; aucune limite supérieure n’est imposée, et Starlette 0.4x satisfait toujours cette exigence. Les numéros de version 1.x mentionnés dans les notes de publication de la 0.137.0 correspondent à des mises à jour dependabot du fichier de verrouillage des tests du dépôt, et non à une contrainte d’exécution imposée à votre application.24 Rien de tout cela ne modifie les patterns de ce guide, qui utilise partout lifespan et la déclaration standard des routes. En revanche, si vous maintenez des outils qui parcourent router.routes, ou si vous exécutez encore d’anciens gestionnaires @app.on_event, la version 0.137.0 / Starlette 1.0 introduit des changements incompatibles. FastAPI 0.137.2 (18 juin 2026) ajoute ensuite iter_route_contexts(), la méthode prise en charge pour énumérer les routes maintenant que router.routes est interne. Puis FastAPI 0.138.0 (20 juin 2026) introduit app.frontend("/", directory="dist") / router.frontend(...) pour servir un frontend statique compilé : une fonctionnalité utile si vous livrez une build SPA distincte, mais sans rapport direct avec l’approche de ce guide, sans étape de build et avec rendu côté serveur (elle monte un dossier dist/ au lieu d’effectuer le rendu de HTML sur le serveur).25 FastAPI 0.139.0 (1er juillet 2026) l’enrichit avec la prise en charge des dépendances dans app.frontend() — par exemple, pour authentifier automatiquement par cookie le frontend servi — et étend ainsi au montage du frontend statique le même mécanisme Depends() que celui employé sur les routes API.26 FastAPI 0.141.0 (29 juillet 2026) ajoute app.frontend(check_dir="auto"), qui empêche fastapi dev d’échouer lorsque le dossier de build n’existe pas encore — un cas courant lorsque vous démarrez le serveur avant d’exécuter la build du frontend. Publiée le même jour, la version FastAPI 0.141.1 corrige la perte silencieuse des tâches en arrière-plan et des en-têtes de réponse provenant des dépendances de app.frontend() : lorsqu’une dépendance définissait un cookie ou planifiait une BackgroundTask, cette opération était ignorée sur le montage du frontend alors qu’elle fonctionnait normalement sur les routes API. Si vous avez adopté la prise en charge des dépendances introduite dans la version 0.139.0, la 0.141.1 est celle qui lui permet de se comporter comme le reste de l’application.28
FastAPI 0.140.0 met fin à une régression mémoire présente dans toutes les versions publiées depuis novembre 2025 — effectuez la mise à niveau. La version du 24 juillet 2026 se limite à un remaniement, mais son effet est considérable. Dependant, l’objet interne construit par FastAPI pour chaque nœud du graphe de dépendances de chaque route, avait accumulé des attributs functools.cached_property à partir de la version 0.121.0 (3 novembre 2025) — dix au total dans la 0.139.2. Une propriété mise en cache nécessite un __dict__ par instance pour y écrire son résultat ; ce coût était donc multiplié par chaque nœud de chaque graphe de l’application. La PR #16049 déplace cette logique hors de la classe vers des fonctions auxiliaires au niveau du module (_get_cache_key(), _get_oauth_scopes(), _uses_scopes()) et déclare Dependant comme @dataclass(slots=True), ce qui en fait un simple conteneur de données. D’après l’exécution CodSpeed de FastAPI sur la PR fusionnée, le benchmark mémoire test_dependency_graph passe de 17,5 Mo à 1,1 Mo, soit une réduction d’un facteur 16 ; le rapport à l’origine de ces travaux décrivait un service de production qui restait sous environ 400 Mo avec la version 0.120.4, mais atteignait une erreur OOM avec la 0.121.3. Toutes les versions recommandées depuis par ce guide — 0.137.x, 0.138.0, 0.139.2 — étaient concernées. Si votre application possède un arbre de dépendances profond ou large (Depends() imbriqués, mécanismes de sécurité, nombreux routeurs inclus), la version 0.140.0 réduit gratuitement la consommation mémoire, sans aucune modification du code de l’application.27
La version 0.140.0 n’était que le début, pas le correctif complet — épinglez la version 0.140.7 ou une version ultérieure. Trois jours après cette publication, le 27 juillet 2026, FastAPI a livré sept nouvelles versions en cinq heures et demie : de la 0.140.1 à la 0.140.7, chacune remaniant le même mécanisme de dépendances. Ces travaux se divisent en deux volets. D’abord, l’arbre de dépendances aplati : auparavant, FastAPI construisait et conservait une copie aplatie du graphe de dépendances de chaque route. La version 0.140.2 cesse de la conserver, tandis que les versions 0.140.3, 0.140.5, 0.140.6 et 0.140.7 suppriment les derniers endroits qui en reconstruisaient une — génération OpenAPI, champs de corps, paramètres de requête, puis à nouveau OpenAPI. La version 0.140.4 supprime le suivi comptable des dépendances répétées, qu’aucun code ne consultait. Ensuite, seul changement comportant un seuil visible : la version 0.140.1 fait passer le lru_cache des fonctions auxiliaires de classification des appelables dans fastapi/dependencies/models.py de 1 024 à 4 096 entrées, derrière une constante nommée _CALLABLE_CLASSIFICATION_CACHE_SIZE, car des utilisateurs signalaient que les applications comptant plus de 1 024 dépendances distinctes saturaient continuellement le cache. Rien ici ne modifie une API que vous appelez : la mise à niveau se résume donc à changer de version. Deux réserves méritent d’être formulées clairement : la cadence montre que cette branche continue d’évoluer, alors consultez les notes de publication au lieu de supposer que la version 0.140.7 en marque la fin ; par ailleurs, FastAPI a ajouté, durant la même période, les benchmarks de dépendances OpenAPI qui mesurent ces travaux (PR #16075). Les chiffres publiés couvrent donc les dernières versions, et non l’ensemble de la séquence de sept versions.29
Les opérations limitées par le CPU (rendu Markdown, extraction CSS) peuvent utiliser des fonctions synchrones. FastAPI les exécute automatiquement dans un pool de threads lorsque le gestionnaire de route n’est pas déclaré async :
# Sync function — FastAPI runs it in a thread pool
@router.get("/blog/{slug}")
def blog_post(slug: str):
post = load_post_by_slug(slug) # CPU-bound Markdown parsing
return templates.TemplateResponse(...)
La règle : si la fonction attend des entrées-sorties, rendez-la async. Si elle effectue des calculs CPU, laissez-la synchrone. Ne mélangez pas await et des appels bloquants dans une même fonction.9
Templates Jinja2
Héritage de templates
Le système d’héritage de Jinja2 remplace la composition par composants de React avec un modèle plus simple. Un template de base définit le squelette de la page. Les templates enfants remplissent des blocs nommés :
<!-- base.html — the skeleton -->
<!DOCTYPE html>
<html lang="{{ lang_attr() }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ page_title | default("Blake Crosley") }}</title>
<meta name="description" content="{{ page_description | default('...') }}">
<!-- CSS — single file, no preprocessor -->
<link rel="stylesheet" href="{{ asset('css/styles.css') }}">
<!-- JSON-LD structured data -->
<script type="application/ld+json">
{ "@context": "https://schema.org", "@graph": [...] }
</script>
{% block head %}{% endblock %}
</head>
<body>
<header class="header">...</header>
<main id="main" role="main">
{% block content %}{% endblock %}
</main>
<footer class="footer">...</footer>
<!-- Scripts deferred for performance -->
<script defer src="{{ asset('js/vendor/alpine.min.js') }}"></script>
<script defer src="{{ asset('js/vendor/htmx.min.js') }}"></script>
<script defer src="{{ asset('js/main.js') }}"></script>
{% block scripts %}{% endblock %}
</body>
</html>
<!-- pages/about.html — fills the blocks -->
{% extends "base.html" %}
{% block head %}
<script type="application/ld+json">
{ "@type": "AboutPage", "name": "About Blake Crosley", ... }
</script>
{% endblock %}
{% block content %}
<section class="hero">
<h1>About</h1>
<p>Designer, developer, dad.</p>
</section>
{% endblock %}
La directive {% extends %} établit une relation parent-enfant. Le template enfant ne définit que les blocs qu’il souhaite remplacer. Tout le reste — le <head>, l’en-tête, le pied de page, les balises script — provient de la base. C’est une composition par soustraction plutôt que par construction.
Le global asset()
Les ressources statiques utilisent le versionnage par hash de contenu pour l’invalidation du cache :
# cache_assets.py
def build_asset_map(static_dir: Path) -> dict[str, str]:
"""Compute MD5 hashes of all static files at startup."""
asset_map = {}
for filepath in static_dir.rglob("*"):
if filepath.is_file():
rel_path = str(filepath.relative_to(static_dir))
content_hash = hashlib.md5(filepath.read_bytes()).hexdigest()[:10]
asset_map[rel_path] = content_hash
return asset_map
def make_asset_url(asset_map: dict, path: str) -> str:
"""Generate versioned URL: /static/css/styles.css?v=a3f8b2c1d0"""
clean_path = path.lstrip("/")
version = asset_map.get(clean_path, "0")
return f"/static/{clean_path}?v={version}"
Dans le template : {{ asset('css/styles.css') }} produit /static/css/styles.css?v=a3f8b2c1d0. Le hash change lorsque le fichier est modifié, invalidant ainsi le cache CDN. Cela remplace la stratégie de noms de fichiers [contenthash] de webpack par 30 lignes de Python calculées au démarrage.
Include pour les partiels réutilisables
Les composants qui se répètent sur plusieurs pages utilisent {% include %} :
<!-- base.html -->
{% include "components/_language_switcher.html" %}
<!-- components/_language_switcher.html -->
{%- set current = current_locale() -%}
{%- set locales = all_locales() -%}
<div class="language-switcher"
x-data="{ open: false }"
@click.away="open = false">
<button @click="open = !open" :aria-expanded="open">
{{ current_locale_native() }}
</button>
<ul class="language-switcher-menu"
:class="{ 'is-open': open }"
x-cloak>
{% for locale in locales %}
<li>
<a href="{{ locale_url(request.url.path, locale.code) }}"
hreflang="{{ locale.code }}">
{{ locale.native }}
</a>
</li>
{% endfor %}
</ul>
</div>
Le préfixe underscore (_language_switcher.html) est une convention indiquant un partiel — un fragment de template qui n’est pas destiné à être rendu de manière autonome. Ce composant utilise à la fois Alpine.js (pour le basculement du menu déroulant) et Jinja2 (pour la liste des locales). La frontière est nette : Alpine.js gère l’état ouvert/fermé, Jinja2 gère les données.
Macros pour les composants réutilisables
Les macros sont les fonctions de Jinja2 — des blocs de template réutilisables avec des paramètres :
<!-- components/_macros.html -->
{% macro card(title, description, href, badge=None) %}
<article class="card">
<a href="{{ href }}" class="card__link">
{% if badge %}
<span class="card__badge">{{ badge }}</span>
{% endif %}
<h3 class="card__title">{{ title }}</h3>
{% if description %}
<p class="card__description">{{ description }}</p>
{% endif %}
</a>
</article>
{% endmacro %}
{% macro optimized_image(image_config, loading="lazy") %}
{% if image_config.get("svg") %}
<img src="{{ image_config.svg }}"
width="{{ image_config.width }}"
height="{{ image_config.height }}"
alt="{{ image_config.alt }}">
{% else %}
<picture>
<source type="image/webp"
srcset="{{ image_config.webp_srcset }}"
sizes="(max-width: 768px) 100vw, 50vw">
<img src="{{ image_config.fallback }}"
width="{{ image_config.width }}"
height="{{ image_config.height }}"
alt="{{ image_config.alt }}"
loading="{{ loading }}">
</picture>
{% endif %}
{% endmacro %}
Importez et utilisez les macros dans les templates de page :
{% from "components/_macros.html" import card, optimized_image %}
<section class="projects">
{% for project in projects %}
{{ card(
title=project.title,
description=project.description,
href=project.link,
badge="New" if project.is_new else None
) }}
{% endfor %}
</section>
Les macros remplacent les composants React pour les modèles de présentation. Elles acceptent des paramètres, prennent en charge les valeurs par défaut et se composent avec d’autres macros. La différence : les macros s’exécutent une seule fois côté serveur et produisent du HTML statique. Les composants React s’exécutent côté client et maintiennent un état. Pour l’affichage de contenu, les macros sont l’outil approprié.
Contexte de template et globals
Les globals Jinja2 sont des fonctions disponibles dans chaque template sans passage explicite :
# In main.py — register globals
templates.env.globals["asset"] = lambda path: make_asset_url(_asset_map, path)
templates.env.globals["csrf_token"] = generate_csrf_token
templates.env.globals["analytics_script"] = analytics.tracking_script
Le global asset() génère des URL versionnées. Le global csrf_token() génère des jetons CSRF à la volée. Le global analytics_script() injecte le snippet de suivi. Ces fonctions sont appelables dans n’importe quel template sans que le gestionnaire de route ne les transmette explicitement.
Pour l’i18n, la mise en place est plus élaborée — les fonctions de traduction ont besoin d’accéder à la locale de la requête en cours :
# i18n/jinja.py
def setup_i18n_jinja(env):
"""Register translation functions as Jinja2 globals."""
env.globals["_"] = get_translation # _('ui.nav.about')
env.globals["locale_prefix"] = get_locale_prefix # '/ja' or ''
env.globals["current_locale"] = get_current_locale
env.globals["all_locales"] = get_all_locales
env.globals["alternate_urls"] = get_alternate_urls
env.globals["lang_attr"] = get_lang_attr # 'ja' for HTML lang
env.globals["og_locale"] = get_og_locale # 'ja_JP' for og:locale
env.globals["jsonld_lang"] = get_jsonld_lang # 'ja-JP' for JSON-LD
Chaque fonction lit la locale depuis la variable de contexte de requête définie par le middleware de locale. Le template appelle {{ _('ui.nav.about') }} et obtient la chaîne traduite pour la locale de la requête en cours, sans aucun paramètre de locale explicite.
Blocs conditionnels
Le système de blocs de Jinja2 prend en charge les remplacements conditionnels :
<!-- base.html -->
{% block head %}{% endblock %}
<!-- pages/blog/post.html -->
{% block head %}
<script type="application/ld+json">
{
"@type": "Article",
"headline": "{{ post.meta.title }}",
"author": { "@id": "https://blakecrosley.com/#person" },
"datePublished": "{{ post.meta.date.isoformat() }}",
"dateModified": "{{ post.meta.updated.isoformat() if post.meta.updated else post.meta.date.isoformat() }}"
}
</script>
{% if post.meta.scripts %}
{% for script in post.meta.scripts %}
<script defer src="{{ asset(script.lstrip('/static/')) }}"></script>
{% endfor %}
{% endif %}
{% if post.meta.styles %}
{% for style in post.meta.styles %}
<link rel="stylesheet" href="{{ asset(style.lstrip('/static/')) }}">
{% endfor %}
{% endif %}
{% endblock %}
Les articles de blog déclarent leurs dépendances dans le frontmatter YAML (scripts: ["/static/js/boids.js"]). Le template les inclut de manière conditionnelle. Les pages qui n’ont pas besoin de scripts ou de styles supplémentaires n’en embarquent aucun — pas de code mort, pas d’imports inutilisés.
Filtres personnalisés
Les filtres Jinja2 transforment les données lors du rendu. Le filtre sanitize prévient les attaques XSS dans le contenu généré par les utilisateurs :
import nh3
ALLOWED_TAGS = {"a", "b", "blockquote", "br", "code", "em", "h1", "h2",
"h3", "h4", "h5", "h6", "hr", "i", "img", "li", "ol",
"p", "pre", "span", "strong", "table", "td", "th", "tr", "ul"}
def sanitize_html(value: str) -> str:
"""Sanitize HTML to prevent XSS attacks."""
if not value:
return ""
return nh3.clean(
value,
tags=ALLOWED_TAGS,
attributes={"a": {"href", "title"}, "img": {"src", "alt"}},
link_rel="noopener noreferrer",
)
templates.env.filters["sanitize"] = sanitize_html
Dans les templates : {{ user_content | sanitize }}. La bibliothèque nh3 est un assainisseur HTML écrit en Rust — rapide et sûr. Elle supprime toute balise ou tout attribut absent de la liste autorisée, empêchant ainsi les attaques XSS stockées même si le contenu provient d’une source non fiable.10
Plongée dans HTMX
HTMX permet à n’importe quel élément HTML d’émettre des requêtes HTTP et d’injecter la réponse dans le DOM. L’idée architecturale clé est la suivante : le HTML rendu côté serveur constitue l’API. Le serveur renvoie la représentation finale. Pas de rendu côté client, pas de sérialisation JSON, pas d’hydratation.
Attributs fondamentaux
| Attribut | Rôle | Exemple |
|---|---|---|
hx-get |
Émettre une requête GET | hx-get="/search?q=term" |
hx-post |
Émettre une requête POST | hx-post="/contact" |
hx-target |
Où placer la réponse | hx-target="#results" |
hx-swap |
Comment insérer la réponse | hx-swap="innerHTML" (par défaut), outerHTML, beforeend |
hx-trigger |
Ce qui déclenche la requête | hx-trigger="click", keyup changed delay:300ms, load |
hx-indicator |
Élément à afficher pendant la requête | hx-indicator="#spinner" |
hx-push-url |
Mettre à jour l’URL du navigateur | hx-push-url="true" |
hx-replace-url |
Remplacer l’URL sans entrée dans l’historique | hx-replace-url="true" |
Pattern 1 : Quiz interactif (état serveur multi-étapes)
blakecrosley.com intègre un quiz interactif qui guide les utilisateurs dans le choix d’un outil. L’intégralité de l’état du quiz réside sur le serveur — aucune gestion d’état côté client :
<!-- _quiz_container.html — chargement initial -->
<div hx-get="/api/quiz/claude-vs-codex/step?answers="
hx-trigger="load"
hx-swap="innerHTML"
id="quiz-wrapper">
<p>Loading quiz...</p>
</div>
<!-- _quiz_step.html — chaque question -->
<div class="quiz-step" id="quiz-container">
<p>Question {{ step }} of {{ total }}</p>
<h3>{{ question.question }}</h3>
<div class="quiz-step__options">
{% for opt in question.options %}
<button class="quiz-step__btn"
hx-get="/api/quiz/claude-vs-codex/step?answers={{ answers }},{{ opt.value }}"
hx-target="#quiz-container"
hx-swap="outerHTML">
{{ opt.label }}
</button>
{% endfor %}
</div>
</div>
Chaque clic sur un bouton envoie les réponses accumulées sous forme de paramètre de requête. Le serveur calcule la question suivante ou le résultat final en fonction de l’historique des réponses. L’état s’accumule dans l’URL — ni cookies, ni sessions, ni JavaScript côté client. Le quiz progresse via des swaps outerHTML : chaque réponse remplace l’intégralité de l’élément d’étape du quiz.
Pattern 2 : Liste d’articles paginée
La page d’écriture utilise HTMX pour une pagination fluide qui met à jour l’URL :
<!-- Pagination link -->
<a href="/writing?page=2&category=Engineering"
hx-get="/writing?page=2&category=Engineering"
hx-target="#writing-content"
hx-swap="innerHTML"
hx-replace-url="true"
hx-indicator="#writing-loading"
aria-label="Go to page 2">
2
</a>
Quatre attributs travaillent de concert :
hx-getémet la requête vers la même URL que lehref(amélioration progressive — fonctionne sans JavaScript)hx-targetplace la réponse dans le conteneur#writing-contenthx-replace-url="true"met à jour l’URL du navigateur sans ajouter d’entrée dans l’historiquehx-indicatoraffiche un indicateur de chargement pendant la requête
Le serveur détecte les requêtes HTMX via l’en-tête HX-Request et renvoie uniquement le fragment de la liste d’articles au lieu de la page complète. C’est pourquoi le middleware des en-têtes de sécurité ajoute Vary: HX-Request — afin que les caches CDN stockent séparément la page complète et le fragment.11
Pattern 3 : Recherche avec temporisation
<input type="search" name="q"
hx-get="/api/search"
hx-trigger="keyup changed delay:300ms"
hx-target="#results"
hx-indicator="#search-spinner" />
<div id="results"></div>
L’attribut hx-trigger combine trois modificateurs :
keyupse déclenche au relâchement de la touchechangedne se déclenche que si la valeur a réellement changé (évite les requêtes dupliquées dues aux touches de modification)delay:300msapplique une temporisation — attend 300 ms après le dernier keyup avant de déclencher la requête
Le serveur renvoie un fragment HTML rendu :
@router.get("/api/search")
async def search(request: Request, q: str = ""):
results = search_content(q)
return templates.TemplateResponse("components/_search_results.html", {
"request": request,
"results": results,
"query": q,
})
Aucun état côté client. Aucune bibliothèque de temporisation. Aucun useEffect. Le template rend les résultats, HTMX les injecte, et le serveur reste l’unique source de vérité.
Pattern 4 : Swaps hors bande (OOB)
Parfois, une seule action serveur doit mettre à jour plusieurs éléments du DOM. Le mécanisme de swap hors bande de HTMX gère cela sans orchestration côté client :
<!-- Server returns multiple elements in one response -->
<!-- Primary target: swapped normally via hx-target -->
<div id="cart-items">
<ul>
<li>Widget A — $29.99</li>
<li>Widget B — $14.99</li>
</ul>
</div>
<!-- OOB target: swapped independently via hx-swap-oob -->
<span id="cart-count" hx-swap-oob="true">2 items</span>
<span id="cart-total" hx-swap-oob="true">$44.98</span>
L’attribut hx-swap-oob="true" indique à HTMX de trouver l’élément par son id n’importe où dans le DOM et de le remplacer, indépendamment du hx-target. Cela remplace le pattern React « remonter l’état » — le serveur calcule tous les états dérivés et envoie le HTML final pour chaque élément en une seule réponse.
Un formulaire de contact illustre bien ce mécanisme : la soumission du formulaire pourrait remplacer le corps du formulaire par un message de succès tout en mettant simultanément à jour un badge de notification via un swap OOB.
Pattern 5 : Liens boostés
HTMX peut « booster » les liens de navigation standard pour utiliser AJAX au lieu de chargements de page complets :
<nav hx-boost="true">
<a href="/about">About</a>
<a href="/writing">Writing</a>
<a href="/guides">Guides</a>
</nav>
Avec hx-boost="true", cliquer sur un lien récupère la page via AJAX, remplace le contenu du <body> et met à jour l’URL — sans rechargement complet de la page. L’historique du navigateur fonctionne normalement (boutons précédent/suivant). Si JavaScript échoue, les liens fonctionnent comme une navigation standard.
L’avantage réside dans la performance perçue : la navigation boostée semble instantanée car le navigateur n’a pas besoin de ré-analyser le CSS, de réévaluer les scripts ni de recalculer la mise en page. Seul le contenu du <body> change. Les liens boostés conviennent parfaitement aux éléments de navigation principale, ce qui confère aux transitions de page l’apparence d’une application monopage sans l’architecture SPA.
Pattern 6 : En-têtes de requête HTMX
HTMX envoie des en-têtes personnalisés avec chaque requête :
| En-tête | Valeur | Cas d’utilisation |
|---|---|---|
HX-Request |
true |
Détecter les requêtes HTMX côté serveur |
HX-Target |
ID de l’élément | Savoir quel élément recevra la réponse |
HX-Trigger |
ID de l’élément | Savoir quel élément a déclenché la requête |
HX-Current-URL |
URL complète | Connaître la page actuelle de l’utilisateur |
Le serveur peut utiliser HX-Request pour renvoyer des réponses différentes :
@router.get("/writing")
async def writing(request: Request, page: int = 1, category: str = None):
posts = load_all_posts(page=page, category=category)
context = {"request": request, "posts": posts, "current_page": page}
# HTMX request: return only the post list fragment
if request.headers.get("HX-Request"):
return templates.TemplateResponse(
"pages/writing/_post_list.html", context
)
# Normal request: return the full page
return templates.TemplateResponse("pages/writing/index.html", context)
Ce pattern de double réponse est au cœur de l’architecture. Un chargement de page complet renvoie le document entier (template de base + contenu de la page). Une navigation HTMX renvoie uniquement le contenu modifié. C’est le serveur qui décide, pas le client.
Pattern 7 : Amélioration progressive
Chaque lien HTMX sur blakecrosley.com inclut un attribut href standard :
<a href="/writing?page=2"
hx-get="/writing?page=2"
hx-target="#writing-content"
hx-swap="innerHTML">
Next Page
</a>
Si JavaScript ne parvient pas à se charger, le href fonctionne comme un lien classique. Si HTMX se charge, il intercepte le clic et effectue un swap AJAX. C’est l’amélioration progressive : le site fonctionne sans JavaScript, et HTMX enrichit l’expérience lorsqu’il est disponible.
Pattern 8 : États de chargement
<button hx-post="/api/contact"
hx-target="#form-result"
hx-indicator="#submit-spinner">
<span id="submit-spinner" class="htmx-indicator">Sending...</span>
<span>Send Message</span>
</button>
HTMX ajoute la classe htmx-request à l’élément déclencheur pendant les requêtes. L’attribut hx-indicator pointe vers un élément qui devient visible pendant la requête. Le style se gère en CSS :
.htmx-indicator {
display: none;
}
.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator {
display: inline;
}
Aucune gestion d’état de chargement. Aucun useState(false). Aucun setLoading(true). CSS gère la visibilité, HTMX gère le basculement de classe.
Patterns Alpine.js
Alpine.js comble le vide laissé par HTMX : l’état côté client qui n’a jamais besoin de communiquer avec le serveur. Lorsqu’un utilisateur clique sur un menu déroulant et que celui-ci s’ouvre, cet état n’existe que dans le navigateur. Alpine.js le gère avec des attributs HTML.
La règle de frontière
La frontière entre HTMX et Alpine.js est précise :
| Type d’état | Outil | Exemple |
|---|---|---|
| Nécessite des données serveur | HTMX | Résultats de recherche, validation de formulaire, pagination |
| N’existe que dans le navigateur | Alpine.js | Ouverture/fermeture de menu déroulant, bascule du menu mobile, visibilité de modale |
| Combine les deux | Les deux | Sélecteur de langue (bascule Alpine.js, navigation de type HTMX) |
Navigation mobile
Le template de base enveloppe l’ensemble de l’en-tête dans un composant Alpine.js :
<div x-data="{ navOpen: false, langOpen: false }"
@keydown.escape.window="navOpen = false; langOpen = false">
<!-- Mobile hamburger button -->
<button @click="navOpen = !navOpen; langOpen = false"
:aria-expanded="navOpen"
:class="navOpen ? 'nav__toggle is-open' : 'nav__toggle'"
aria-label="Toggle navigation">
<span class="nav__toggle-icon">
<span class="nav__toggle-bar"></span>
<span class="nav__toggle-bar"></span>
<span class="nav__toggle-bar"></span>
</span>
</button>
<!-- Mobile menu panel -->
<div class="mobile-menu" x-show="navOpen" x-cloak>
<nav class="mobile-menu__nav">
<a href="/about" @click="navOpen = false">About</a>
<a href="/#work" @click="navOpen = false">Work</a>
<a href="/writing" @click="navOpen = false">Writing</a>
</nav>
</div>
</div>
Patterns Alpine.js clés :
x-datadéclare la portée du composant et l’état initialx-showbascule la visibilité selon l’état (utilise CSSdisplay: none)x-cloakmasque l’élément jusqu’à l’initialisation d’Alpine.js (évite le flash de contenu non stylisé)@clicklie des gestionnaires de clic avec des expressions:aria-expanded(raccourci pourx-bind:aria-expanded) définit dynamiquement les attributs@keydown.escape.windowécoute la touche Échap globalement pour fermer les panneaux
Composant menu déroulant
Le sélecteur de langue utilise Alpine.js pour l’état de bascule avec @click.away pour la fermeture au clic extérieur :
<div x-data="{ open: false }"
@click.away="open = false"
@keydown.escape.window="open = false">
<button @click="open = !open"
:aria-expanded="open"
aria-haspopup="listbox">
English
<svg :class="{ 'rotated': open }">...</svg>
</button>
<ul :class="{ 'is-open': open }"
:aria-hidden="!open"
role="listbox"
x-cloak>
<li role="option">
<a href="/ja/about">日本語</a>
</li>
<!-- more languages -->
</ul>
</div>
Le modificateur @click.away ferme le menu déroulant lors d’un clic à l’extérieur. Alpine.js gère cela avec un seul attribut — pas d’enregistrement d’écouteur d’événements, pas de nettoyage, pas de gestion de références.
Quand utiliser Alpine.js plutôt que du JavaScript natif
Alpine.js est approprié lorsque :
- L’état est limité à un seul élément DOM (menu déroulant, modale, bascule)
- Les interactions sont binaires ou simples (ouvrir/fermer, afficher/masquer, basculer)
- Plusieurs éléments doivent réagir au même changement d’état
- Les attributs d’accessibilité doivent rester synchronisés avec la visibilité
Le JavaScript natif est approprié lorsque :
- L’interaction implique des calculs complexes (visualisations, simulations)
- Le composant possède sa propre boucle de rendu (canvas, animation)
- La performance est critique (Alpine.js ajoute une surcharge par composant
x-data) - La logique dépasse 20-30 lignes d’expressions Alpine.js
blakecrosley.com utilise Alpine.js pour la navigation, le changement de langue et les bascules de contenu. Les 20 composants interactifs du blog (simulation de boids, visualiseur de code de Hamming, etc.) utilisent du JavaScript natif car ils nécessitent du rendu canvas et des machines à états complexes.
Exemple de bout en bout : filtrage par catégorie sur /writing
Cette section retrace une fonctionnalité réelle du code de production à travers chaque couche : route, template, interaction HTMX, sécurité, mise en cache et rendu final. La fonctionnalité : des onglets de catégories sur la page d’écriture qui filtrent les articles de blog sans rechargement complet de la page.
La route (app/routes/pages.py:508)
async def writing_listing(request: Request, page: int = 1, category: str | None = None):
"""Writing page — blog posts and external publications."""
templates = get_templates(request)
markdown_posts = load_all_posts(published_only=True)
all_posts = CUSTOM_BLOG_POSTS + markdown_posts
# Filter by category if specified
if category and category in CATEGORY_MAP:
display_name = CATEGORY_MAP[category]
all_posts = [
p for p in all_posts
if _get_post_category(p).lower() == display_name.lower()
]
# Pagination
total_pages = max(1, (len(all_posts) + POSTS_PER_PAGE - 1) // POSTS_PER_PAGE)
page = max(1, min(page, total_pages))
paginated = all_posts[(page - 1) * POSTS_PER_PAGE : page * POSTS_PER_PAGE]
template_context = {
"request": request,
"posts": paginated,
"categories": categories,
"current_category": category,
"current_page": page,
"total_pages": total_pages,
# ... SEO: canonical, prev/next URLs
}
# HTMX partial: return just the post list fragment
if request.headers.get("HX-Request"):
return templates.TemplateResponse(
"pages/writing/_post_list.html",
template_context,
)
# Full page for direct navigation
return templates.TemplateResponse(
"pages/writing/index.html",
template_context,
)
La vérification de l’en-tête HX-Request constitue le pattern central : même route, mêmes données, template différent. HTMX reçoit un fragment. Les navigateurs reçoivent la page complète.
Les onglets de catégorie (HTMX)
<!-- Category filter tabs -->
<nav class="writing-categories">
<a href="/writing"
hx-get="/writing"
hx-target="#post-list"
hx-push-url="true"
class="category-tab {% if not current_category %}active{% endif %}">
All ({{ total_posts }})
</a>
{% for cat in categories %}
<a href="/writing?category={{ cat.slug }}"
hx-get="/writing?category={{ cat.slug }}"
hx-target="#post-list"
hx-push-url="true"
class="category-tab {% if current_category == cat.slug %}active{% endif %}">
{{ cat.name }} ({{ cat.count }})
</a>
{% endfor %}
</nav>
<div id="post-list">
{% include "pages/writing/_post_list.html" %}
</div>
Chaque onglet possède à la fois un href (fonctionne sans JavaScript) et un hx-get (remplace uniquement la liste d’articles). hx-push-url met à jour l’URL du navigateur afin que la vue filtrée soit partageable et mémorisable dans les favoris.
Le partial (pages/writing/_post_list.html)
Le partial s’affiche de manière identique, qu’il soit inclus au chargement de la page ou injecté par HTMX :
{% for post in posts %}
<article class="post-card">
<a href="{{ locale_prefix() }}/blog/{{ post.meta.slug }}">
<h3>{{ post.meta.title }}</h3>
<p>{{ post.meta.description }}</p>
<time>{{ post.meta.date }}</time> · {{ post.reading_time }}m
</a>
</article>
{% endfor %}
Aucun balisage HTMX spécifique dans le partial. Aucune logique de rendu côté client. Le même HTML fonctionne pour le chargement initial de la page et pour chaque filtre ultérieur.
Sécurité
Les valeurs de catégorie sont validées par rapport à CATEGORY_MAP (un dictionnaire côté serveur) avant le filtrage. Les catégories invalides sont ignorées, jamais renvoyées en écho. Aucune entrée utilisateur n’est interpolée dans du SQL ou du HTML. L’en-tête CSP bloque les scripts en ligne.
Mise en cache
Les réponses de catégorie sont dynamiques (pas de cache CDN). En revanche, les ressources statiques (CSS, HTMX, Alpine.js) sont hashées par contenu et mises en cache indéfiniment après le premier chargement. Les changements de catégorie suivants ne transfèrent que le partial HTML (~3-5 Ko) — ni CSS, ni JS, ni images ne sont re-téléchargés.
Ce que cela démontre
Une fonctionnalité, du vrai code de production, zéro outil de build. Le serveur filtre et génère le HTML. HTMX remplace la liste d’articles. Alpine.js n’intervient pas (aucun état client nécessaire). L’URL se met à jour pour le partage. Amélioration progressive : les onglets fonctionnent comme de simples liens sans JavaScript. Total de JavaScript personnalisé pour cette fonctionnalité : zéro ligne.
Extensions optionnelles
Les sections suivantes couvrent des patterns qui complètent la stack principale mais ne sont pas utilisés sur blakecrosley.com. Ils sont inclus car ils représentent les ajouts les plus courants que les équipes adoptent avec cette architecture.
Bootstrap 5 sans Sass
Remarque : blakecrosley.com utilise du CSS brut avec des propriétés personnalisées — pas de Bootstrap. Cette section présente Bootstrap 5 comme option pour les équipes souhaitant un framework utilitaire sans étape de build. Le CSS compilé de Bootstrap peut être chargé depuis un CDN ou intégré à votre feuille de styles. Les patterns ci-dessous sont génériques et fonctionnent avec l’approche HTMX + Alpine.js décrite dans les sections précédentes.
Bootstrap 5 a abandonné jQuery comme dépendance et prend en charge l’utilisation autonome du CSS. Ni Sass, ni PostCSS, ni aucun outil de build ne sont nécessaires pour utiliser le système de grille et les classes utilitaires de Bootstrap.
Auto-hébergement sans CDN
blakecrosley.com auto-héberge toutes les bibliothèques tierces :
<!-- base.html — no CDN, no external requests -->
<script defer src="{{ asset('js/vendor/alpine.min.js') }}"></script>
<script defer src="{{ asset('js/vendor/htmx.min.js') }}"></script>
L’auto-hébergement élimine les dépendances externes, empêche les pannes de CDN de casser le site et permet un cache immuable avec des URLs à empreinte de contenu. Téléchargez le CSS compilé de Bootstrap (pas les sources Sass) et placez-le dans static/css/vendor/.
Système de grille
La grille Bootstrap fonctionne avec de simples classes HTML :
<div class="container">
<div class="row">
<div class="col-12 col-md-8">
<article>Main content</article>
</div>
<div class="col-12 col-md-4">
<aside>Sidebar</aside>
</div>
</div>
</div>
Pas de mixins Sass. Pas de @include make-col(). Le CSS compilé inclut les classes de grille responsive. Pour des breakpoints personnalisés au-delà des valeurs par défaut de Bootstrap, écrivez de simples media queries en CSS.
Surcharges en CSS brut
Surchargez les valeurs par défaut de Bootstrap avec des propriétés personnalisées CSS et des sélecteurs standard :
/* Custom design tokens — no Sass, no Tailwind */
:root {
--color-bg-dark: #000000;
--color-text-primary: #ffffff;
--color-text-secondary: rgba(255, 255, 255, 0.65);
--color-text-tertiary: rgba(255, 255, 255, 0.40);
--spacing-sm: 1rem;
--spacing-md: 1.5rem;
--spacing-lg: 2rem;
--gutter: 48px;
--font-size-lg: 1.25rem;
}
/* Responsive override — the browser reads this at runtime */
@media (max-width: 768px) {
:root {
--gutter: var(--spacing-md); /* 48px → 24px on mobile */
}
}
/* Override Bootstrap's default body styles */
body {
background: var(--color-bg-dark);
color: var(--color-text-primary);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
}
Les propriétés personnalisées CSS se propagent dans le DOM, héritent des éléments parents et réagissent aux media queries à l’exécution. Les variables Sass, elles, se compilent en valeurs statiques et disparaissent. Cette distinction est cruciale pour la gestion des thèmes : modifier une seule propriété personnalisée peut mettre à jour chaque valeur dérivée sans recompilation.12
Classes utilitaires vs. CSS de composants
Utilisez les classes utilitaires Bootstrap pour les espacements et mises en page ponctuels. Utilisez le CSS de composants pour les patterns récurrents :
<!-- Bootstrap utility for one-off spacing -->
<div class="mt-4 mb-3 px-2">One-off layout</div>
<!-- Component class for repeated patterns -->
<article class="writing__item">
<h3 class="writing__item-title">Post Title</h3>
<p class="writing__item-description">Description</p>
</article>
/* Component CSS — BEM naming, reusable */
.writing__item {
padding: var(--spacing-md);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
transition: background 0.15s ease;
}
.writing__item:hover {
background: rgba(255, 255, 255, 0.03);
}
.writing__item-title {
font-size: var(--font-size-lg);
margin-bottom: 0.5rem;
}
Le principe : les utilitaires Bootstrap pour la mécanique de mise en page (marges, padding, flexbox). Le CSS personnalisé pour l’identité visuelle (couleurs, typographie, animations). Ne mélangez jamais classes utilitaires et styles de composants pour une même responsabilité.
i18n et localisation
blakecrosley.com propose du contenu en 10 langues : anglais, japonais, coréen, chinois simplifié, chinois traditionnel, allemand, français, espagnol, polonais et portugais (brésilien).
Routage de locale basé sur l’URL
La locale figure dans le chemin de l’URL : /about (anglais), /ja/about (japonais), /zh-Hans/about (chinois simplifié). L’anglais est la langue par défaut et ne comporte aucun préfixe.
# i18n/config.py
SUPPORTED_LOCALES = [
"en", "zh-Hans", "zh-Hant", "fr", "de", "ja", "ko", "pl", "pt-BR", "es"
]
DEFAULT_LOCALE = "en"
Le middleware de locale extrait la locale depuis le chemin de l’URL :
# i18n/middleware.py
class LocaleMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
path = request.url.path
# Check if path starts with a supported locale
for locale in SUPPORTED_LOCALES:
if path.startswith(f"/{locale}/") or path == f"/{locale}":
request.state.locale = locale
# Strip locale prefix for route matching
request.scope["path"] = path[len(f"/{locale}"):]
break
else:
request.state.locale = DEFAULT_LOCALE
response = await call_next(request)
return response
Le middleware supprime le préfixe de locale avant la résolution des routes. Les gestionnaires de routes n’ont donc pas besoin de chemins spécifiques à chaque locale — /about gère aussi bien l’anglais (/about) que le japonais (/ja/about), car le middleware normalise le chemin.
Fonctions de traduction dans les templates
Les variables globales Jinja2 fournissent des fonctions de traduction :
<!-- Template usage -->
<h3>{{ _('ui.footer.navigate') | default('Navigate') }}</h3>
<a href="{{ locale_prefix() }}/about">
{{ _('ui.nav.about') | default('About') }}
</a>
La fonction _() recherche une clé de traduction dans le cache mémoire. Le filtre | default() fournit le texte anglais de secours si la traduction est manquante. La fonction locale_prefix() renvoie le préfixe d’URL pour la locale courante ("" pour l’anglais, "/ja" pour le japonais).
Balises hreflang
Chaque page inclut des balises hreflang pour toutes les locales prises en charge :
<!-- Generated in base.html -->
{% for alt in alternate_urls(request.url.path) %}
<link rel="alternate" hreflang="{{ alt.hreflang }}" href="{{ alt.url }}">
{% endfor %}
Ce qui produit :
<link rel="alternate" hreflang="en" href="https://blakecrosley.com/about">
<link rel="alternate" hreflang="ja" href="https://blakecrosley.com/ja/about">
<link rel="alternate" hreflang="zh-Hans" href="https://blakecrosley.com/zh-Hans/about">
<!-- ... all 10 locales -->
<link rel="alternate" hreflang="x-default" href="https://blakecrosley.com/about">
Les moteurs de recherche utilisent hreflang pour afficher la bonne version linguistique dans les résultats. L’entrée x-default pointe vers la version anglaise comme solution de repli.13
Stockage des traductions et cache mémoire
Les traductions sont stockées dans Cloudflare D1 (SQLite en périphérie) et chargées dans un cache mémoire via le gestionnaire lifespan :
@asynccontextmanager
async def lifespan(app: FastAPI):
# Load translations into memory at startup
for locale in SUPPORTED_LOCALES:
data = await fetch_translations(locale)
TRANSLATIONS[locale] = data
yield
app = FastAPI(lifespan=lifespan)
Le cache mémoire évite les requêtes à la base de données à chaque rendu de page. Les mises à jour de traduction nécessitent un rafraîchissement du cache (déclenché via un endpoint d’administration ou un déploiement). Cette architecture sacrifie la fraîcheur au profit de la performance — les traductions changent rarement, tandis que les rendus de page surviennent à chaque requête.
Surveillance de la santé
blakecrosley.com inclut un endpoint de vérification de santé i18n qui surveille la couverture de traduction par locale :
@app.get("/health/i18n")
async def health_i18n():
cache = get_translation_cache()
result = {
"status": "healthy",
"cache_loaded": cache.is_loaded,
"locales": {},
"alerts": [],
}
# Check coverage for each locale
for locale in SUPPORTED_LOCALES:
coverage = await calculate_coverage(locale, en_count)
result["locales"][locale] = {"coverage": round(coverage, 2)}
if coverage < 99.5:
result["alerts"].append(
f"{locale}: {coverage:.1f}% coverage (threshold: 99.5%)"
)
result["status"] = "warning"
return result
Le seuil de couverture à 99,5 % détecte les traductions manquantes avant que les utilisateurs ne rencontrent des chaînes non traduites. L’endpoint de santé s’intègre au système de surveillance de Railway pour alerter lorsque la couverture diminue — par exemple, après l’ajout de nouvelles chaînes d’interface qui n’ont pas encore été traduites.
Rendu de contenu adapté à la locale
Les articles de blog et les guides prennent en charge les traductions par locale des métadonnées et du contenu :
# In route handler
translated = get_blog_translation(post.meta.slug, locale)
return templates.TemplateResponse("pages/blog/post.html", {
"request": request,
"post": post,
"translated_title": translated.title if translated else post.meta.title,
"translated_description": translated.description if translated else post.meta.description,
})
<!-- In template -->
<h1>{{ translated_title }}</h1>
<p class="post__description">{{ translated_description }}</p>
<!-- Body content falls back to English if translation unavailable -->
{{ post.html | sanitize | safe }}
Le principe est constant : privilégier le contenu traduit, puis se rabattre sur l’anglais. Cela permet une traduction partielle — un utilisateur japonais voit les titres et descriptions traduits même si le corps complet de l’article reste en anglais. Le filtre Jinja2 | default() encode ce schéma en un seul pipe :
{{ translated.title if translated else post.meta.title }}
Traduction des données de locale
Le contenu statique comme les descriptions de projets et les libellés de navigation est traduit via des fonctions utilitaires qui conservent la même structure de données tout en substituant les chaînes spécifiques à la locale :
# i18n/data.py
def translate_projects(projects: list, locale: str) -> list:
"""Return projects with translated titles and descriptions."""
if locale == "en":
return projects
translated = []
for project in projects:
t = get_translation(f"project.{project['slug']}.title", locale)
d = get_translation(f"project.{project['slug']}.description", locale)
translated.append({
**project,
"title": t or project["title"],
"description": d or project["description"],
})
return translated
Cette approche maintient la couche de traduction séparée de la couche de données. Les routes transmettent la même liste projects quelle que soit la locale. Les fonctions de traduction enveloppent les données de manière transparente.
Sitemap avec alternates hreflang
Le sitemap dynamique inclut toutes les pages dans toutes les locales avec des références croisées :
@app.get("/sitemap.xml")
async def sitemap():
for page in static_pages:
for locale in SUPPORTED_LOCALES:
# Each URL entry includes alternates for all locales
locale_path = f"/{locale}{path}" if locale != "en" else path
xml_parts.append(f"<loc>{base_url}{locale_path}</loc>")
# Add xhtml:link alternates
for alt_locale in SUPPORTED_LOCALES:
alt_path = f"/{alt_locale}{path}" if alt_locale != "en" else path
hreflang = LOCALE_TO_HREFLANG[alt_locale]
xml_parts.append(
f'<xhtml:link rel="alternate" hreflang="{hreflang}" '
f'href="{base_url}{alt_path}"/>'
)
Cela produit 10 entrées d’URL par page (une par locale), chacune avec 11 liens alternatifs (10 locales + x-default). Pour un site de 50 pages, le sitemap contient 500 entrées d’URL avec 5 500 liens hreflang. Le sitemap est généré dynamiquement et mis en cache pendant une heure.
Patterns de base de données
Note : blakecrosley.com utilise Cloudflare D1 (SQLite serverless) via HTTP pour toutes les données persistantes, et non SQLAlchemy. Cette section couvre le pattern async SQLAlchemy standard pour les projets FastAPI qui nécessitent une base de données relationnelle — la configuration de production la plus courante pour cette stack.
SQLAlchemy 2.0 Async
Pour les applications qui nécessitent une base de données relationnelle, la prise en charge async de SQLAlchemy 2.0 s’intègre proprement avec FastAPI :
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker, DeclarativeBase
engine = create_async_engine("sqlite+aiosqlite:///./data.db")
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
class Base(DeclarativeBase):
pass
Note d’installation (SQLAlchemy 2.0.50+) : depuis la version 2.0.50, la dépendance greenlet de la stack async ne s’installe plus par défaut. Utilisez l’extra asyncio pour qu’elle soit incluse, sans quoi le premier await exécuté sur l’engine échouera avec une erreur indiquant l’absence de greenlet :23
pip install "sqlalchemy[asyncio]" aiosqlite
SQLAlchemy 2.0.50 exige également Python 3.10+ (3.7–3.9 ne sont plus prises en charge) et ajoute des wheels free-threaded (3.13t).23
Injection de dépendances pour les sessions de base de données
from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession
async def get_db() -> AsyncGenerator[AsyncSession, None]:
async with async_session() as session:
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raise
@router.get("/users/{user_id}")
async def get_user(request: Request, user_id: int, db: AsyncSession = Depends(get_db)):
result = await db.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none()
if not user:
raise HTTPException(404, "User not found")
return templates.TemplateResponse("pages/user.html", {
"request": request, "user": user
})
La dépendance get_db gère le cycle de vie de la session : elle ouvre une session, la fournit au gestionnaire de route, commit en cas de succès et effectue un rollback en cas d’exception. Chaque opération de base de données utilise des requêtes paramétrées — jamais d’interpolation de chaîne.
Intégration Pydantic
Les modèles Pydantic valident les entrées à la frontière API et sérialisent les sorties pour les templates :
from pydantic import BaseModel, EmailStr
class ContactForm(BaseModel):
name: str
email: EmailStr
message: str
@router.post("/contact")
async def submit_contact(request: Request, form: ContactForm):
# form.name, form.email, form.message are validated
await send_email(form)
return templates.TemplateResponse("components/_contact_success.html", {
"request": request
})
Pydantic valide les types, les formats (e-mail, URL) et les contraintes (longueur minimale/maximale) avant l’exécution du gestionnaire de route. Une entrée invalide renvoie automatiquement une réponse 422. Cela remplace les bibliothèques de validation de formulaire côté client : le serveur valide, puis HTMX insère soit le message de succès, soit le retour d’erreur.
Migrations avec Alembic
Alembic gère les changements de schéma de base de données :
# Generate a migration from model changes
alembic revision --autogenerate -m "add user preferences table"
# Apply migrations
alembic upgrade head
# Roll back one migration
alembic downgrade -1
La fonctionnalité autogenerate compare les modèles SQLAlchemy au schéma actuel de la base de données et génère des scripts de migration. Ces scripts sont des fichiers Python versionnés qui vivent dans le dépôt :
# alembic/versions/001_add_user_preferences.py
def upgrade():
op.create_table(
"user_preferences",
sa.Column("id", sa.Integer, primary_key=True),
sa.Column("user_id", sa.Integer, sa.ForeignKey("users.id")),
sa.Column("locale", sa.String(10), default="en"),
sa.Column("theme", sa.String(20), default="dark"),
)
def downgrade():
op.drop_table("user_preferences")
Les migrations s’exécutent pendant le déploiement (avant le démarrage de l’application). Cela garantit que le schéma de base de données correspond au code de l’application. Pour blakecrosley.com, la plupart des données résident dans Cloudflare D1 (accessible via HTTP), donc les migrations Alembic s’appliquent à la base de données SQLite locale ou PostgreSQL utilisée pour les données de session et l’analytics.
Le pattern Cloudflare D1
blakecrosley.com utilise Cloudflare D1 comme base de données distante accessible via un proxy Cloudflare Worker :
class D1Client:
"""HTTP client for Cloudflare D1 via Worker proxy."""
def __init__(self, worker_url: str, auth_secret: str):
self.worker_url = worker_url
self.auth_secret = auth_secret
async def fetch_all(self, sql: str, params: list = None) -> list[dict]:
async with httpx.AsyncClient() as client:
response = await client.post(
f"{self.worker_url}/query",
json={"sql": sql, "params": params or []},
headers={"Authorization": f"Bearer {self.auth_secret}"},
)
return response.json()["results"]
Ce pattern convient aux applications qui ont besoin d’une base de données sans vouloir gérer un serveur de base de données. D1 est SQLite à l’edge de Cloudflare, accessible via HTTP. Le proxy Worker gère l’authentification et la limitation de débit. Le compromis porte sur la latence : chaque requête est une requête HTTP (~50-100ms), contre une connexion à une base de données locale (~1-5ms). Le cache en mémoire au démarrage atténue cet effet pour les charges de travail à forte lecture, comme les traductions.
Sécurité
Middleware d’en-têtes de sécurité
blakecrosley.com implémente des en-têtes de sécurité renforcés via un middleware personnalisé :
class SecurityHeadersMiddleware(BaseHTTPMiddleware):
CSP_DIRECTIVES = {
"default-src": "'self'",
"script-src": "'self' 'unsafe-inline' 'unsafe-eval'",
"style-src": "'self' 'unsafe-inline'",
"img-src": "'self' data: https:",
"connect-src": "'self'",
"frame-ancestors": "'self'",
"base-uri": "'self'",
"form-action": "'self'",
"upgrade-insecure-requests": "",
}
async def dispatch(self, request, call_next):
response = await call_next(request)
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "SAMEORIGIN"
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
response.headers["Strict-Transport-Security"] = (
"max-age=31536000; includeSubDomains"
)
response.headers["Cross-Origin-Opener-Policy"] = "same-origin"
response.headers["Content-Security-Policy"] = self.csp
response.headers["Permissions-Policy"] = self.PERMISSIONS_POLICY
return response
La CSP inclut 'unsafe-inline' et 'unsafe-eval', car Alpine.js en a besoin pour l’évaluation des expressions. L’alternative est le build compatible CSP de Alpine.js, qui comporte des limites.14 Toutes les autres fonctionnalités sont verrouillées : frame-ancestors empêche le clickjacking, form-action limite les envois de formulaires à la même origine, et upgrade-insecure-requests force HTTPS.
Sécurité du cache CDN avec HTMX
Le middleware d’en-têtes de sécurité ajoute Vary: HX-Request aux réponses HTMX :
if request.headers.get("HX-Request"):
existing_vary = response.headers.get("Vary", "")
if "HX-Request" not in existing_vary:
parts = [v.strip() for v in existing_vary.split(",") if v.strip()]
parts.append("HX-Request")
response.headers["Vary"] = ", ".join(parts)
Sans cet en-tête, un CDN pourrait mettre en cache une réponse de fragment HTMX et la servir comme page complète à une requête non HTMX (ou l’inverse). L’en-tête Vary indique au CDN de stocker des entrées de cache distinctes selon la valeur de l’en-tête HX-Request.11
Protection CSRF
Les formulaires HTMX utilisent des tokens CSRF sans état, signés par HMAC :
# csrf.py
def generate_csrf_token() -> str:
"""Token format: timestamp:random:HMAC-SHA256-signature"""
timestamp = str(int(time.time()))
random_value = secrets.token_hex(16)
payload = f"{timestamp}:{random_value}"
signature = hmac.new(
CSRF_SECRET.encode(), payload.encode(), hashlib.sha256
).hexdigest()
return f"{payload}:{signature}"
def validate_csrf_token(token: str) -> bool:
"""Verify signature and check expiration (1 hour)."""
timestamp, random_value, signature = token.split(":")
if int(time.time()) - int(timestamp) > 3600:
return False
expected = hmac.new(
CSRF_SECRET.encode(),
f"{timestamp}:{random_value}".encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
Le token est généré dans le template via une globale Jinja2 et inclus dans les requêtes de formulaire HTMX :
<form hx-post="/contact" hx-target="#form-result">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<!-- form fields -->
</form>
Les tokens sans état éliminent le stockage de session côté serveur. La signature HMAC garantit que le token a été généré par le serveur. L’horodatage empêche les attaques par rejeu. hmac.compare_digest empêche les attaques temporelles.15
Nettoyage de HTML
Le contenu généré par les utilisateurs passe par nh3 avant le rendu :
templates.env.filters["sanitize"] = sanitize_html
# In templates: {{ content | sanitize }}
La bibliothèque nh3 supprime les balises et attributs absents de l’allowlist. Les liens reçoivent automatiquement rel="noopener noreferrer". Cette défense est indépendante de la CSP : elle empêche le XSS stocké au niveau du rendu, tandis que la CSP empêche les scripts injectés au niveau du navigateur. Défense en profondeur.
Validation des entrées
Les modèles Pydantic valident toutes les entrées à la frontière API :
from pydantic import BaseModel, Field, EmailStr
class ContactRequest(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
email: EmailStr
message: str = Field(..., min_length=10, max_length=5000)
FastAPI renvoie automatiquement 422 Unprocessable Entity pour les entrées invalides. Associé aux requêtes de base de données paramétrées (SQLAlchemy n’interpole jamais de chaînes), cela empêche l’injection SQL et garantit la sécurité des types aux frontières.
Performance
Lighthouse 100/100/100/100
blakecrosley.com obtient 100 dans les quatre catégories Lighthouse : Performance, Accessibilité, Bonnes pratiques et SEO. Vérifiez avec PageSpeed Insights.2
Les optimisations clés :
Stratégie de chargement de CSS
blakecrosley.com charge CSS avec une seule balise <link> et des URL avec hash de contenu pour une mise en cache immuable :
<link rel="stylesheet" href="{{ asset('css/styles.css') }}">
Le helper asset() ajoute un hash de contenu (?v=a3b2c1d4) afin que le navigateur mette le fichier en cache indéfiniment, jusqu’à ce que le contenu change. Pas d’extraction critique de CSS, pas d’astuce print-media, pas de chargement basé sur JavaScript. Le fichier CSS fait environ 8 Ko gzipé : assez petit pour que l’approche à requête unique obtienne 100 en Performance Lighthouse sans gymnastique d’optimisation.
Compression GZip
app.add_middleware(GZipMiddleware, minimum_size=500)
Les réponses de plus de 500 octets sont compressées. HTML se compresse de 70 à 80 %, réduisant un document de 15 Ko à 3-4 Ko.
Mise en cache immuable des ressources statiques
# In security headers middleware
if request.url.path.startswith("/static/"):
if os.environ.get("RAILWAY_ENVIRONMENT"):
response.headers["Cache-Control"] = "public, max-age=31536000, immutable"
Les ressources statiques avec des URL à hash de contenu (?v=a3f8b2c1d0) sont mises en cache pendant un an avec immutable. Le hash change quand le fichier change, ce qui force les navigateurs et les CDN à récupérer la nouvelle version.
Chargement différé des scripts
<script defer src="{{ asset('js/vendor/alpine.min.js') }}"></script>
<script defer src="{{ asset('js/vendor/htmx.min.js') }}"></script>
<script defer src="{{ asset('js/main.js') }}"></script>
L’attribut defer télécharge les scripts en parallèle de l’analyse de HTML, mais les exécute après l’analyse du document. Cela évite de bloquer le rendu sans la complexité du chargement async et de la gestion de l’ordre d’exécution.
Optimisation des images
Les images utilisent WebP avec un srcset responsive et des dimensions explicites :
OPTIMIZED_IMAGES = {
"vision-sprint": {
"webp_srcset": (
"/static/images/optimized/vision-sprint-400w.webp 400w, "
"/static/images/optimized/vision-sprint-800w.webp 800w, "
"/static/images/optimized/vision-sprint-1200w.webp 1200w"
),
"fallback": "/static/images/optimized/vision-sprint-fallback.jpg",
"width": 1200,
"height": 1045,
},
}
<picture>
<source type="image/webp"
srcset="{{ image.webp_srcset }}"
sizes="(max-width: 768px) 100vw, 50vw">
<img src="{{ image.fallback }}"
width="{{ image.width }}"
height="{{ image.height }}"
alt="{{ image.alt }}"
loading="lazy">
</picture>
Les attributs explicites width et height empêchent le Cumulative Layout Shift (CLS). L’attribut loading="lazy" diffère les images hors écran. WebP fournit des fichiers 25 à 35 % plus petits que JPEG à qualité équivalente.16
Early Hints
# In main.py
app.state.preload_links = [
f'<{make_asset_url(_asset_map, "css/styles.css")}>; rel=preload; as=style',
]
# In security headers middleware
if "text/html" in content_type:
preload_links = getattr(request.app.state, "preload_links", [])
if preload_links:
response.headers["Link"] = ", ".join(preload_links)
L’en-tête Link avec rel=preload indique à Cloudflare d’envoyer une réponse 103 Early Hints, ce qui permet au navigateur de commencer à récupérer CSS avant que le serveur ait fini de générer la réponse HTML.17
JavaScript minimal
L’empreinte totale de JavaScript :
| Bibliothèque | Taille (minifiée + gzipée) |
|---|---|
| HTMX | ~16 KB |
| Alpine.js | ~15 KB |
| JS spécifique à la page | 4-8 KB |
| Total | 35-39 KB |
Une application React typique livre 100 à 300 Ko de JavaScript de framework avant le code applicatif.18 L’approche sans build livre moins de JavaScript, parce qu’il y a moins de JavaScript à livrer.
Déploiement
Railway
blakecrosley.com est déployé sur Railway via git push :
# railway.toml
[build]
builder = "nixpacks"
[deploy]
startCommand = "uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-8000}"
healthcheckPath = "/health"
healthcheckTimeout = 300
restartPolicyType = "ON_FAILURE"
restartPolicyMaxRetries = 10
Le builder Nixpacks de Railway détecte le projet Python à partir de requirements.txt, installe les dépendances et exécute la commande de démarrage. Aucun fichier Docker n’est requis. Le point de terminaison de contrôle d’intégrité vérifie que l’application répond avant de recevoir du trafic :
@app.get("/health")
async def health():
return {"status": "healthy"}
Pipeline de déploiement
git push origin main
→ Railway detects push
→ Nixpacks installs Python + requirements.txt (cached)
→ uvicorn starts
→ Health check passes
→ Traffic routes to new deployment
→ ~40 seconds total
Pas de npm install. Pas de npm run build. Pas de compilation webpack. Pas de compilation TypeScript. La seule étape d’installation est pip install -r requirements.txt, qui est mise en cache entre les déploiements.
Procfile
web: uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-8000}
Le Procfile fournit une alternative compatible avec Heroku. Railway prend en charge railway.toml et Procfile. La syntaxe ${PORT:-8000} utilise le port fourni par la plateforme ou, par défaut, le port 8000 pour le développement local.
Configuration d’Uvicorn en production
Pour les déploiements à fort trafic, utilisez plusieurs workers :
uvicorn app.main:app \
--host 0.0.0.0 \
--port ${PORT:-8000} \
--workers 4 \
--loop uvloop \
--http httptools
--workers 4exécute quatre processus workers (règle générale : 2 * nombre de cœurs CPU + 1)--loop uvlooputilise la boucle d’événements uvloop, plus rapide (remplacement direct d’asyncio)--http httptoolsutilise le parseur HTTP httptools, plus rapide
Chaque worker est un processus distinct qui conserve sa propre copie de l’application ; la mémoire par processus est donc multipliée par le nombre de workers — c’est précisément là que le correctif du graphe de dépendances de FastAPI 0.140.0 porte ses fruits : sur une application comportant de nombreuses dépendances, quatre workers sous 0.139.2 subissent quatre fois l’ancien surcoût de Dependant.27
Pour le développement, --reload surveille les modifications apportées aux fichiers :
uvicorn app.main:app --reload --port 8000
Alternative avec Docker
Pour les plateformes qui nécessitent Docker :
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
L’image de base slim permet de conserver un conteneur léger. --no-cache-dir empêche pip de stocker les paquets téléchargés dans la couche de l’image.
CDN Cloudflare
blakecrosley.com utilise Cloudflare pour la mise en cache CDN, le DNS et les Workers :
# Cache headers for HTML pages (set in security middleware)
response.headers["Cache-Control"] = (
"public, max-age=300, s-maxage=3600, "
"stale-while-revalidate=86400"
)
max-age=300— mise en cache par le navigateur pendant 5 minutess-maxage=3600— mise en cache par le CDN pendant 1 heurestale-while-revalidate=86400— diffusion du contenu périmé pendant sa revalidation durant 24 heures
Les ressources statiques reçoivent max-age=31536000, immutable, car les URL contenant un hachage du contenu garantissent leur fraîcheur.
Cadre de décision
Avez-vous besoin d’outils de build ?
Répondez à quatre questions :
1. Plus de cinq développeurs partagent-ils des interfaces JavaScript ? Si oui, la vérification des types à la compilation de TypeScript évite des bugs d’intégration que les tests à l’exécution détectent trop tard. Ajoutez une étape de build.
2. Votre application gère-t-elle un état complexe côté client ? Si le glisser-déposer, la collaboration en temps réel ou les données disponibles hors ligne sont des fonctionnalités essentielles (et non de simples bonus), un framework comme React ou Svelte justifie sa complexité. Ajoutez une étape de build.
3. Plusieurs produits utilisent-ils une bibliothèque de composants commune ? Si oui, cette bibliothèque nécessite un paquet npm, un versionnage sémantique et du tree shaking. Ajoutez une étape de build.
4. Dépendez-vous de bibliothèques de l’écosystème npm qui supposent l’utilisation d’un bundler ? Si Radix, Framer Motion, TanStack Query ou des bibliothèques similaires sont au cœur du produit, un pipeline de build est indispensable.
Si les quatre réponses sont « non », l’approche sans build est viable. Si au moins une réponse est « oui », les outils de build résolvent un véritable problème. L’erreur consiste à ajouter des outils de build lorsque les quatre réponses sont « non » : vous résolvez alors des problèmes que vous n’avez pas, tout en créant une charge de gestion des dépendances bien réelle.1
Comparaison des stacks
| Catégorie | Sans build (ce guide) | React + outils de build |
|---|---|---|
| Idéal pour | Sites de contenu, portfolios, outils internes, applications CRUD | Produits SaaS, SPA complexes, applications utilisant un design system |
| Taille de l’équipe | 1 à 5 développeurs | 5 à 50 développeurs ou plus |
| Gestion de l’état | Serveur (HTMX) + client (Alpine.js) | Client (état React, Redux, Zustand) |
| Sûreté des types | À l’exécution (Pydantic côté serveur) | À la compilation (TypeScript) |
| Réutilisation des composants | Inclusions + macros Jinja2 | Paquets npm, bibliothèques partagées |
| SEO | Rendu côté serveur par défaut | Nécessite une configuration SSR/SSG |
| Niveau minimal de performance | Élevé (JS minimal, rendu côté serveur) | Variable (surcoût du framework) |
| Plafond de complexité | Plus bas (pas de mode hors ligne ni d’état client riche) | Plus élevé (toute interaction côté client est possible) |
| Dépendances | 17 paquets Python | Plus de 300 paquets npm |
| Temps de build | 0 seconde | 15 à 60 secondes |
Quand HTMX n’est pas adapté
HTMX remplace l’état client par des allers-retours avec le serveur. Cette approche fonctionne jusqu’à ce que la latence devienne déterminante :
- Interfaces par glisser-déposer — un aller-retour serveur de 200 ms pour chaque événement de déplacement est inacceptable
- Collaboration en temps réel — un état piloté par WebSocket nécessite une résolution des conflits côté client
- Applications conçues pour fonctionner hors ligne — sans serveur, pas de HTMX
- Animations complexes liées à l’état — Framer Motion et React Spring reposent sur le modèle de réconciliation de React
- Applications Canvas/WebGL — la boucle de rendu s’exécute par nature côté client
Pour ces cas d’usage, un framework côté client est l’outil approprié. L’approche sans build ne cherche pas à les remplacer.
Aide-mémoire
FastAPI
# Development
source venv/bin/activate
uvicorn app.main:app --reload --port 8000
# Production
uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-8000}
# Testing
python -m pytest -v --cov=app
# Database migrations
alembic upgrade head
alembic revision --autogenerate -m "description"
Attributs HTMX
hx-get="/url" <!-- GET request -->
hx-post="/url" <!-- POST request -->
hx-target="#element" <!-- Where to put response -->
hx-swap="innerHTML" <!-- How to insert (innerHTML, outerHTML, beforeend) -->
hx-trigger="click" <!-- What triggers request -->
hx-trigger="keyup changed delay:300ms" <!-- Debounced input -->
hx-trigger="load" <!-- Fire on element load -->
hx-indicator="#spinner" <!-- Show during request -->
hx-push-url="true" <!-- Update browser URL -->
hx-replace-url="true" <!-- Replace URL (no history) -->
Attributs Alpine.js
x-data="{ open: false }" <!-- Component scope + state -->
x-show="open" <!-- Toggle visibility -->
x-cloak <!-- Hide until Alpine inits -->
@click="open = !open" <!-- Event handler -->
@click.away="open = false" <!-- Outside click -->
@keydown.escape="open = false" <!-- Keyboard event -->
:class="{ 'active': open }" <!-- Dynamic class -->
:aria-expanded="open" <!-- Dynamic attribute -->
x-text="count" <!-- Dynamic text content -->
x-init="fetchData()" <!-- Run on init -->
Propriétés personnalisées CSS
:root {
--color-bg: #000000;
--color-text: #ffffff;
--spacing-sm: 1rem;
--spacing-md: 1.5rem;
--font-size-lg: 1.25rem;
}
@media (max-width: 768px) {
:root { --gutter: 24px; }
}
En-têtes de sécurité
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-eval'
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Referrer-Policy: strict-origin-when-cross-origin
Cross-Origin-Opener-Policy: same-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Checklist de configuration du projet
[ ] FastAPI app with Jinja2Templates
[ ] Security headers middleware (CSP, HSTS, X-Frame-Options)
[ ] CSRF token generation and validation
[ ] GZip middleware (minimum_size=500)
[ ] Content-hash asset versioning (cache busting)
[ ] HTMX self-hosted in /static/js/vendor/
[ ] Alpine.js self-hosted in /static/js/vendor/
[ ] CSS custom properties for design tokens
[ ] Health check endpoint (/health)
[ ] Error handlers (404, 500)
[ ] robots.txt, sitemap.xml, llms.txt
[ ] JSON-LD structured data in base template
[ ] Hreflang tags for i18n (if multi-language)
[ ] HTML sanitization filter (nh3)
[ ] Rate limiting middleware
[ ] Deferred script loading
FAQ
HTMX est-il prêt pour de véritables applications web en production ?
Oui. HTMX est stable depuis 2020 et utilisé en production dans de nombreux secteurs. Carson Gross, son créateur, fait de la rétrocompatibilité un principe de conception fondamental — la documentation de HTMX indique que la bibliothèque ne rendra pas les applications existantes incompatibles au sein d’une même version majeure.19 La bibliothèque ne pèse qu’environ 16 Ko après minification et compression gzip, ne comporte aucune dépendance et respecte le versionnage sémantique. blakecrosley.com utilise HTMX en production depuis trois ans sans rencontrer le moindre bug lié à HTMX.20
Puis-je utiliser TypeScript sans étape de build ?
En partie. Le typage des fichiers TypeScript peut être vérifié avec tsc --noEmit sans générer de fichiers de sortie, ce qui permet d’effectuer une vérification à la compilation comme avec un linter. Toutefois, les navigateurs ne peuvent pas exécuter directement les fichiers .ts : une étape de build reste donc nécessaire pour distribuer du TypeScript. Une autre solution consiste à utiliser des annotations de type JSDoc dans de simples fichiers .js, que TypeScript peut vérifier sans compilation. Vous bénéficiez ainsi de la sûreté du typage pendant le développement tout en distribuant du JavaScript standard.
Comment cette approche se compare-t-elle à Astro ou 11ty ?
Astro et 11ty sont des générateurs de sites statiques qui produisent du HTML simple avec un minimum de JavaScript côté client, mais ils nécessitent une étape de build (Node.js, npm install et une commande de build). L’approche sans build supprime cette étape : le serveur génère le HTML à chaque requête. En contrepartie, Astro et 11ty produisent des pages statiques plus rapides, car aucun calcul serveur n’est nécessaire, tandis que FastAPI + HTMX prend nativement en charge le contenu dynamique — données propres à chaque utilisateur, envoi de formulaires et mises à jour en temps réel — sans couche API distincte.
Qu’en est-il du rendu côté serveur (SSR) avec React ?
Le SSR de Next.js et l’approche FastAPI + HTMX partagent le même objectif : envoyer au navigateur du HTML généré par le serveur. La différence réside dans ce qui se passe après le rendu initial. Next.js hydrate la page avec React, en envoyant au client le runtime du framework et le code des composants. FastAPI + HTMX n’effectue aucune hydratation : le HTML constitue le résultat final. HTMX gère les interactions suivantes en demandant de nouveaux fragments HTML au serveur. Résultat : FastAPI + HTMX envoie environ 35 à 40 Ko de JavaScript au total, contre 100 à 300 Ko pour une application Next.js.18
Comment gérer la validation des formulaires avec cette stack ?
Côté serveur. Pydantic valide les données saisies lors de l’envoi du formulaire. Si la validation échoue, le serveur renvoie le formulaire accompagné des messages d’erreur. HTMX remplace le contenu du DOM par la réponse :
<form hx-post="/contact" hx-target="#form-container" hx-swap="outerHTML">
<input type="email" name="email" required>
<button type="submit">Send</button>
</form>
@router.post("/contact")
async def contact(request: Request, email: str = Form(...)):
if not validate_email(email):
return templates.TemplateResponse("components/_contact_form.html", {
"request": request,
"error": "Please enter a valid email address",
"email": email, # Preserve input
})
await send_email(email)
return templates.TemplateResponse("components/_contact_success.html", {
"request": request
})
Le serveur effectue la validation, génère les états d’erreur et HTMX insère le résultat. Aucune bibliothèque de validation côté client n’est nécessaire. L’attribut HTML required fournit une validation élémentaire au niveau du navigateur comme première ligne de défense.
Puis-je ajouter des fonctionnalités en temps réel (WebSockets) ?
Oui. FastAPI prend nativement en charge les WebSocket :
from fastapi import WebSocket
@app.websocket("/ws/notifications")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
while True:
data = await get_notification()
await websocket.send_text(render_notification_html(data))
HTMX propose une extension WebSocket (hx-ws) qui connecte des éléments aux endpoints WebSocket :
<!-- HTMX 2.x WebSocket extension syntax -->
<div hx-ext="ws" ws-connect="/ws/notifications">
<div id="notifications" ws-send></div>
</div>
Remarque : HTMX 1.x utilisait la syntaxe
hx-ws="connect:...". Dans HTMX 2.x, la prise en charge des WebSocket a été déplacée vers une extension distincte (htmx-ext-ws) qui utilise les attributsws-connectetws-sendprésentés ci-dessus. Si vous utilisez HTMX 1.x, l’ancienne syntaxehx-wsfonctionne toujours.Cycle bêta de HTMX 4.0 : htmx 4.0.0-beta6 est désormais disponible sous le tag npm
next, avec la documentation de la version 4.0 (bêta 6 publiée le 23 juillet 2026), tandis que le guide de démarrage rapide de htmx.org et le tag npmlatestrestent sur la version 2.0.10. Ce guide cible toujours HTMX 2.x, qui demeure la version recommandée pour la production jusqu’à la stabilisation de la version 4.0 ; la migration de 2.x vers 4.x représente un changement de génération, et non une simple mise à jour mineure de 2.x. Le modèle de versionnage de big-skies-software ignore les versions majeures impaires : 4.0 succède donc directement à 2.x.2122Points à suivre dans la documentation de la version 4.0. Deux nouveautés méritent une attention particulière lors des revues de sécurité et d’architecture avant la version GA de 4.0 : la nouvelle extension
hx-liveintroduit des expressions réactives au DOM, réévaluées lorsque l’état référencé change, tandis que la nouvelle extensionhx-nonceconditionne le traitement des attributs htmx à la présence de nonces CSP. Le guide de migration vers la version 4.0 déplace également plusieurs concepts de configuration, rétablit ou modifie certains comportements liés aux événements et à l’historique, et retire du cœur certains utilitaires JavaScript. Considérez la version 4.0 comme un véritable projet de migration, et non comme un correctif 2.x applicable tel quel.21
Les messages envoyés par le serveur sont insérés dans le DOM selon les mêmes mécanismes de ciblage et de remplacement que les réponses HTTP. Le serveur envoie des fragments HTML via la WebSocket, puis HTMX les insère.
Comment cette stack gère-t-elle le SEO ?
Le HTML généré côté serveur est naturellement adapté au SEO, car les robots d’indexation reçoivent l’intégralité du contenu de la page sans exécuter de JavaScript. blakecrosley.com ajoute plusieurs couches de SEO :
- Données structurées JSON-LD dans
<head>pour chaque page (schémas Person, Article, WebSite et FAQPage) - Sitemap dynamique avec des variantes hreflang pour les 10 locales
- Flux RSS à l’adresse
/blog/feed.xml llms.txtà la racine pour faciliter la découverte par les robots d’exploration d’IA- URL canoniques et balises Open Graph dans le template de base
- HTML sémantique :
<article>,<section>,<main>et hiérarchie correcte des titres
Aucune configuration SSR n’est nécessaire. Aucun getStaticProps. Aucun ISR. Le HTML est généré à chaque requête : il s’agit du comportement par défaut, pas d’une optimisation.
Quelle est la courbe d’apprentissage par rapport à React ?
Pour les développeurs Python, la courbe d’apprentissage est nettement plus accessible. Vous connaissez déjà le langage. Les gestionnaires de routes de FastAPI renvoient des réponses à partir de templates — le même modèle mental que les vues Flask ou Django. HTMX ajoute une poignée d’attributs HTML (hx-get, hx-target, hx-swap). Alpine.js en ajoute quelques autres (x-data, x-show, @click). Vous n’avez à apprendre ni JSX, ni DOM virtuel, ni système de hooks, ni bibliothèque de gestion d’état, ni configuration d’outil de build.
La documentation de HTMX tient sur une seule longue page. Celle de Alpine.js tient sur quelques pages. La documentation de React s’étend sur des centaines de pages consacrées aux hooks, au contexte, aux refs, aux effets, à suspense, aux composants serveur et au SSR en streaming.
Pour les développeurs JavaScript/React, le changement est davantage conceptuel que syntaxique. L’idée fondamentale est que le serveur détient l’état et génère le HTML. La gestion de l’état côté client devient alors une gestion des routes côté serveur. La récupération des données côté client est remplacée par des attributs HTMX sur les éléments HTML. La syntaxe est plus simple, mais ce modèle mental exige d’abandonner l’hypothèse propre aux SPA selon laquelle le client est responsable du rendu.
Journal des modifications
| Date | Modification | Source |
|---|---|---|
| 2026-07-29 | FastAPI 0.141.0 + 0.141.1 (toutes deux publiées le 29 juillet). La version 0.141.0 ajoute app.frontend(check_dir="auto") afin que fastapi dev n’échoue plus lorsque le répertoire de compilation est absent — cas courant lorsque le serveur démarre avant l’exécution de la compilation frontend. Quelques heures plus tard, la version 0.141.1 corrige la perte des tâches en arrière-plan et des en-têtes de réponse provenant des dépendances dans app.frontend() ; une dépendance qui définissait un cookie ou planifiait une BackgroundTask voyait ce travail ignoré sur le montage frontend, alors qu’il fonctionnait correctement sur les routes API. Cette correction comble donc une véritable lacune dans la prise en charge des dépendances ajoutée avec la version 0.139.0. Ces deux changements s’intègrent à la présentation existante de app.frontend() plutôt que de faire l’objet d’une nouvelle section, car l’approche de rendu côté serveur de ce guide ne monte aucun répertoire dist/. La version 0.141.1 documente également FASTAPI_ENV dans le guide CLI de FastAPI (documentation uniquement, sans modification du corps du guide). |
28 |
| 2026-07-27 | FastAPI a publié les versions 0.140.1 à 0.140.7 en cinq heures et demie le 27 juillet — soit sept versions, toutes consacrées à la refonte du mécanisme de dépendances amorcée avec la version 0.140.0. Deux axes : la copie aplatie du graphe de dépendances que FastAPI construisait et conservait a disparu (0.140.2), tout comme chacun des derniers emplacements qui en reconstruisaient une — génération OpenAPI (0.140.3, 0.140.7), champs du corps (0.140.5), paramètres de requête (0.140.6) — tandis que la version 0.140.4 supprime une comptabilité illisible servant au suivi des répétitions. Le seul changement présentant un seuil visible concerne la version 0.140.1 : le lru_cache des fonctions auxiliaires de classification des appelables dans fastapi/dependencies/models.py passe de 1 024 à 4 096 entrées (constante nommée _CALLABLE_CLASSIFICATION_CACHE_SIZE), après le signalement d’applications dépassant 1 024 dépendances distinctes et provoquant un renouvellement incessant du cache. Aucun changement de API ; la recommandation du paragraphe sur la mémoire des dépendances passe de la version 0.140.0 à la version 0.140.7 ou ultérieure, avec une note précisant que cette branche continue d’évoluer et que les benchmarks des dépendances OpenAPI (PR #16075) n’ont été intégrés que dans la dernière version de la série. |
29 |
| 2026-07-25 | FastAPI 0.140.0 (24 juillet, 21 h 16 UTC) corrige une régression de consommation mémoire dans le système de dépendances, présente depuis la version 0.121.0 (3 novembre 2025). La PR #16049 retire dix attributs functools.cached_property de Dependant, les déplace dans des fonctions auxiliaires au niveau du module et transforme la classe en @dataclass(slots=True) ; l’exécution officielle de CodSpeed sur la PR fusionnée indique que le benchmark mémoire test_dependency_graph passe de 17,5 Mo à 1,1 Mo (÷16), tandis que le signalement initial décrivait des erreurs OOM en production avec la version 0.121.3, là où la version 0.120.4 restait sous environ 400 Mo. Ajout d’un passage sur la version 0.140.0 dans les modèles async et d’une ligne sur la mémoire des workers dans la configuration de production d’Uvicorn. Correction également d’une erreur préexistante : le guide affirmait que la version 0.137.0 « verrouille Starlette sur la branche 1.x » — ce n’est pas le cas. L’exigence d’exécution de FastAPI est starlette>=0.46.0 (une version minimale sans plafond, toujours satisfaite par Starlette 0.4x) pour l’ensemble des versions 0.136.3, 0.137.0, 0.138.0, 0.139.2 et 0.140.0 ; les numéros 1.x mentionnés dans les notes de la version 0.137.0 correspondent à des mises à jour dependabot du fichier de verrouillage des tests du dépôt (la PR #15722 ne modifie que uv.lock). L’affirmation dans le corps du guide et 24 ont toutes deux été corrigées. Deux non-changements signalés : les éléments internes de Dependant constituent désormais une rupture pour les outils (oauth_scopes, cache_key, _uses_scopes, _is_security_scheme ont disparu en tant qu’attributs, remplacés par les fonctions de module _get_oauth_scopes() / _get_cache_key() / _uses_scopes(), et slots=True empêche le monkey-patching des instances) — il s’agit d’une API interne non documentée que ce guide ne référence jamais, dans la même catégorie que le changement de router.routes de la version 0.137.0 ; par ailleurs, la documentation officielle de FastAPI utilise désormais par défaut des projets uv plutôt que pip/venv dans 30 fichiers, dont le README, index.md, virtual-environments.md ainsi que les pages Docker/déploiement (PR #16032, fusionnée le 21 juillet). Ce changement de documentation est sans effet sur votre code, mais le guide enseigne partout pip install -r requirements.txt et diverge désormais de la voie d’entrée recommandée en amont — une décision éditoriale future, délibérément reportée lors de cette révision. |
27 |
| 2026-07-24 | htmx 4.0.0-beta6 remplace beta5 en tant que tag npm next (publiée le 23 juillet 2026 ; version GitHub publiée le même jour). Principales nouveautés de cette version bêta : nouvelle extension hx-multipart (réponses multipart/mixed/multipart/parallel diffusées en continu avec des en-têtes d’action HX-* pour chaque partie), restauration de la position de défilement de l’historique via la API Navigation avec une solution de repli pour Firefox, renommage d’un événement interne à la bêta de htmx:swap:finally en htmx:finally:swap, déclenchement désormais postérieur au swap des événements provenant de l’en-tête de réponse HX-Trigger, méthodes de requête personnalisées et réécriture de hx-ws avec transmission de protocols. Recommandation inchangée — la production reste sur HTMX 2.x (latest = 2.0.10) jusqu’à la version GA de 4.0 ; ce renommage ne constitue une rupture qu’au sein de la branche bêta 4.0. La note sur le suivi de la bêta et 21 ont été mises à jour. FastAPI 0.139.2, Uvicorn 0.51.0, Alpine.js 3.15.12, Starlette 1.3.1 et Jinja2 3.1.6 ont tous été vérifiés et restent inchangés ; aucun avis de sécurité sur cette période. |
|
| 2026-07-17 | FastAPI 0.139.1 + 0.139.2 (16 juillet) : correction des chemins contenant des points pour les solutions de repli de app.frontend() (/users/john.doe, PR #16011) et construction thread-safe des routes du routeur pour les tests exécutés dans des threads parallèles (PR #16013) — aucun changement de API visible par l’application. Uvicorn 0.49.0 → 0.51.0 : l’ancienne implémentation websockets est obsolète et auto utilise désormais websockets-sansio par défaut (0.50.0), l’implémentation par défaut exige websockets>=13.0 (0.50.2), et la version 0.51.0 (8 juillet) ajoute le redémarrage des workers par SIGHUP avec chevauchement, pour des rechargements presque sans interruption ; le dépôt se trouve désormais sous Kludex/uvicorn. HTMX (2.0.10 / 4.0.0-beta5 next), Alpine.js 3.15.12, Starlette 1.3.1, Pydantic 2.13.4, SQLAlchemy 2.0.51 et Bootstrap 5.3.8 ont tous été vérifiés et restent inchangés ; aucun avis de sécurité sur cette période. |
|
| 2026-07-07 | htmx 4.0.0-beta5 correspond désormais au tag npm next (publiée le 26 juin 2026), remplaçant beta4 ; la note de suivi de la bêta 4.0 de HTMX et [^22] ont été mises à jour en conséquence. Recommandation inchangée — les environnements de production restent sur HTMX 2.x (latest = 2.0.10) jusqu’à la version GA de 4.0. Vérification effectuée à partir des dist-tags npm de htmx.org. |
|
| 2026-07-02 | FastAPI 0.139.0 (1er juillet). app.frontend() prend désormais en charge les dépendances — par exemple, l’authentification automatique par cookie pour le frontend servi (PR #15908) — en étendant le montage du frontend statique de la version 0.138.0 avec le mécanisme standard Depends() ; cette fonctionnalité reste indépendante de l’approche de rendu côté serveur défendue par ce guide et figure dans le même paragraphe de comparaison. Aucun autre mouvement dans la pile : HTMX 2.0.10, Alpine.js 3.15.12, Bootstrap 5.3.8 et SQLAlchemy 2.0.51 restent inchangés. |
26 |
| 2026-06-22 | FastAPI 0.138.0 + 0.137.2. La version 0.138.0 (20 juin) ajoute app.frontend("/", directory="dist") / router.frontend(...) pour servir un frontend statique compilé (sortie SPA dist/) — une fonctionnalité indépendante de l’approche de rendu côté serveur sans compilation de ce guide, mentionnée à titre de comparaison dans la section consacrée aux modèles async. La version 0.137.2 (18 juin) ajoute iter_route_contexts() comme méthode prise en charge pour énumérer les routes, maintenant que router.routes est interne (depuis la version 0.137.0). Il s’agit dans les deux cas d’ajouts de fonctionnalités, sans rupture ; Starlette (1.3.1), Pydantic (2.13.4), HTMX (2.0.10), Alpine.js (3.15.12), Bootstrap (5.3.8) et SQLAlchemy (2.0.51) restent tous inchangés. |
25 |
| 2026-06-16 | FastAPI 0.137.0/0.137.1 + Starlette 1.0→1.3.1. FastAPI 0.137.0 (14 juin) remanie les éléments internes du routeur : router.routes est désormais un arbre interne, et non une liste plate de APIRoute (ce qui entraîne une rupture pour tout code qui la parcourt), tout en permettant l’ajout de routes après include_router() et en introduisant les nouveaux hooks APIRouter.matches()/.handle() ; la version 0.137.1 (15 juin) corrige le typage de APIRoute et les routeurs sans préfixe dont le chemin est vide. Starlette a publié sa première version stable 1.0 (22 mars) et atteint désormais la version 1.3.1 (12 juin), qui supprime les hooks obsolètes on_event/on_startup/on_shutdown ainsi que les décorateurs @app.route()/@app.websocket_route() — seules les voies lifespan et Route/WebSocketRoute subsistent. (Cette entrée indiquait initialement que FastAPI 0.137.0 verrouillait Starlette sur la version 1.3.1 — correction du 2026-07-25 : ce n’est pas le cas ; l’exigence d’exécution est starlette>=0.46.0, sans plafond.) Ajout d’une note sur lifespan et le routeur à la section consacrée aux modèles async. SQLAlchemy 2.0.51 (15 juin) ne contient que des corrections de bugs. |
24 |
| 2026-06-08 | Modification de l’installation async de SQLAlchemy 2.0.50. Depuis SQLAlchemy 2.0.50, la dépendance greenlet de la pile async n’est plus installée par défaut — installez l’extra sqlalchemy[asyncio] (sinon le premier await exécuté sur le moteur échouera avec une erreur signalant l’absence de greenlet). La version 2.0.50 exige également Python 3.10 ou version ultérieure (abandon des versions 3.7 à 3.9) et ajoute des wheels pour la version free-threaded 3.13t. Ajout d’une note d’installation à la section SQLAlchemy 2.0 Async. Aucun changement dans le corps du guide pour le reste de la pile : la dernière version de FastAPI reste la 0.136.3 (2026-05-23, aucune version en juin), la version stable de htmx reste la 2.0.10 (4.0.0-beta4 « The Fetchening » est en bêta, avec une version stable visée vers le début de 2027 environ, et n’est pas encore recommandée pour la production), Alpine.js reste en version 3.15.12 et Bootstrap en version 5.3.x. Recommandation de production inchangée : HTMX 2.x jusqu’à la version stable de 4.0.23 |
|
| 2026-05-24 | Vérification de maintenance : l’inventaire local du contenu compte toujours 210 articles de blog, 11 guides principaux, 48 études de design et 10 locales prises en charge, dont l’anglais. La dernière version de FastAPI est la 0.136.3 (2026-05-23) ; la seule refonte visible par l’application signalée dans les notes de version concerne un traitement plus strict des en-têtes comportant des traits de soulignement lorsque convert_underscores=True, tandis que la version 0.136.2 valide les champs Server-Sent Event afin d’éviter les données d’événement corrompues. La version stable de htmx reste la 2.0.10, tandis que le tag npm next et la documentation 4.0 pointent désormais vers la version 4.0.0-beta4 ; la dernière version de SQLAlchemy 2.0 est la 2.0.50 ; la dernière version de Pydantic reste la 2.13.4. La recommandation de production demeure inchangée : utilisez HTMX 2.x jusqu’à ce que la version 4.0 devienne stable.122 |
|
| 2026-05-18 | Actualisation de l’inventaire du site : l’inventaire local du contenu compte désormais 210 articles de blog, 11 guides principaux, 48 études de design et 10 locales prises en charge, dont l’anglais. La dernière version de FastAPI reste la 0.136.1 ; la version stable de htmx reste la 2.0.10, avec le tag npm next sur la version 4.0.0-beta3 ; la dernière version npm de Alpine.js reste la 3.15.12. La recommandation de production demeure inchangée : utilisez HTMX 2.x jusqu’à ce que la version 4.0 devienne stable.12021 |
|
| 2026-05-15 | Vérification de maintenance : la dernière version de FastAPI reste la 0.136.1 ; cet environnement local du site importe FastAPI 0.128.0 et Starlette 0.50.0 ; la version stable de htmx reste la 2.0.10 et le tag npm next correspond désormais à la version 4.0.0-beta3 ; la dernière version npm de Alpine.js est la 3.15.12 ; la dernière version de Bootstrap est la 5.3.8 ; la dernière version de SQLAlchemy 2.0 est la 2.0.49 ; la dernière version de Pydantic est la 2.13.4. Recommandation de production inchangée : utilisez HTMX 2.x jusqu’à ce que la version 4.0 devienne stable.2021 |
|
| 2026-05-09 | Suivi de htmx 4.0.0-beta3 (8 mai 2026) : htmx 4.0.0-beta3 est disponible sous le tag npm next et dans la documentation 4.0, tandis que le tag npm latest reste associé à la version 2.0.10. Principales nouveautés à surveiller avant la version GA : nouvelle extension hx-live (expressions réactives au DOM), nouvelle extension hx-nonce (protection par nonce CSP des attributs htmx) et modifications apportées par le guide de migration à la configuration, à l’historique, aux événements et aux fonctions auxiliaires principales de JavaScript. Recommandation de production inchangée : htmx 2.x reste associé au dernier tag npm et demeure la version recommandée jusqu’à la version GA de 4.0.21 |
|
| 2026-05-07 | Vérification de maintenance : la dernière version de FastAPI reste la 0.136.1 ; la version stable de htmx est la 2.0.10 et la v4 reste en bêta, avec une sortie prévue pour l’été 2026 ; la dernière version npm de Alpine.js est la 3.15.12 ; la dernière version de Bootstrap est la 5.3.8 ; la dernière version de SQLAlchemy 2.0 est la 2.0.49 ; la dernière version de Pydantic est la 2.13.4. Les métriques locales du site ont été actualisées : 182 articles de blog, 11 guides, dix locales prises en charge et 17 dépendances Python. Recommandation de migration inchangée : utilisez HTMX 2.x en production jusqu’à ce que la version 4.0 devienne stable.20 | |
| 2026-04-25 | FastAPI 0.136.1 (23 avril 2026) : nettoyage des éléments obsolètes de Pydantic v2 (aucun changement de comportement pour le code applicatif). Suivi du calendrier de HTMX 4.0 : htmx 4.0.0-beta1 (6 avril) et 4.0.0-beta2 (14 avril) ont été publiées. Recommandation de migration inchangée — htmx 2.x reste associé au dernier tag npm jusqu’à ce que la version 4.0 soit stable ; les correctifs de sécurité se poursuivent, sans urgence de mise à niveau. Principaux changements de la version 4.0 à prendre dès maintenant en compte dans la conception : (1) fetch() remplace XMLHttpRequest comme infrastructure ajax principale, (2) l’héritage des attributs devient explicitement activé par défaut, (3) la prise en charge de l’historique émet une requête réseau pour le contenu restauré (aucun instantané local du DOM). FastAPI 0.135.4 (16 avril) a supprimé le décorateur du poisson d’avril @app.vibe() introduit dans la version 0.135.3. |
|
| 2026-04-16 | Ajout d’une mention anticipée de HTMX 4.0-beta. Prise en charge des builds free-threaded Python 3.14t par FastAPI 0.136.0. Fonctionnalités de Pydantic 2.13.x (fabriques par défaut des attributs privés ayant accès aux données validées du modèle, espace de noms pydantic.v1 mis à jour vers la version 1.10.26 avec prise en charge de la version 3.14). Correctifs de Alpine.js 3.15.11 : modificateur x-anchor.noflip, avertissement de x-for en présence de plusieurs éléments racines, correction d’une régression de morphing de $refs. |
|
| 2026-03-24 | Publication initiale |
Références
Ce guide couvre l’intégralité du système utilisé pour construire blakecrosley.com. Le manifeste No-Build en expose les fondements philosophiques. L’article consacré au score Lighthouse parfait retrace le parcours d’optimisation des performances. L’article Vibe Coding ou ingénierie examine la place du développement assisté par l’IA dans ce workflow.
-
Métriques de production de blakecrosley.com au 18 mai 2026. Le site compte 210 articles de blog, des composants JavaScript interactifs, 11 guides principaux, 48 études de design, une version anglaise ainsi que 9 langues traduites, un minimum de dépendances Python et aucun outil de build. Données vérifiées à partir de l’inventaire local du contenu, de
app/i18n/config.pyet derequirements.txt. ↩↩↩↩↩ -
Google PageSpeed Insights (pagespeed.web.dev) exécute des audits Lighthouse sur n’importe quelle URL publique. En mars 2026, blakecrosley.com obtient un score de 100/100/100/100 (performances, accessibilité, bonnes pratiques, SEO). Les résultats sont publiquement vérifiables. Consultez De 76 à 100 : obtenir un score Lighthouse parfait pour découvrir l’intégralité du parcours d’optimisation. ↩↩↩
-
Une nouvelle installation de
npx create-next-app@latest(Next.js 15, testé en février 2026) installe 311 paquets dansnode_modules/, pour un total de 187 Mo. Les projets de production comportant des dépendances supplémentaires ont tendance à dépasser ce volume. Chaque projet peut varier. Source : tests de l’auteur, documentés dans Le manifeste No-Build. ↩ -
La documentation de Vercel sur les performances de Next.js recommande des optimisations précises — optimisation des images, chargement des polices et découpage du code — pour obtenir des scores supérieurs à 90. Consultez nextjs.org/docs/app/building-your-application/optimizing. La plage de 70 à 90 correspond aux paramètres par défaut avant l’application de ces optimisations. ↩↩
-
Liste complète des dépendances vérifiée dans le fichier
requirements.txtde blakecrosley.com en mai 2026. Ce fichier contient actuellement 17 entrées de dépendances Python et aucun outil de build, compilateur ou bundler. ↩ -
D’après l’expérience de l’auteur dans la maintenance de projets Next.js entre 2021 et 2024, l’écosystème JavaScript génère chaque mois 15 à 25 PR Dependabot pour les projets actifs. La plupart mettent à jour des dépendances transitives que le développeur n’a jamais importées directement. ↩
-
Tim Berners-Lee a énoncé la rétrocompatibilité comme principe de conception du Web : « un navigateur doit être rétrocompatible ». Une page datant de 1996 s’affiche dans Chrome 2026. Consultez w3.org/DesignIssues/Principles. ↩
-
OWASP recommande de désactiver les points de terminaison de documentation API en production afin de réduire la surface d’attaque. Le point de terminaison
/openapi.jsonexpose la définition de toutes les routes, leurs paramètres et leurs modèles de réponse. ↩ -
Documentation de FastAPI sur les gestionnaires async et synchrones : fastapi.tiangolo.com/async/. Combiner
awaitavec des appels bloquants dans des fonctionsasyncprive la boucle d’événements de ressources. ↩ -
nh3 est un assainisseur HTML basé sur Rust qui succède à la bibliothèque Bleach. Maintenu par le projet PyO3, il assure l’assainissement HTML à partir d’une liste d’éléments autorisés. Consultez github.com/messense/nh3. ↩
-
L’en-tête
Varyest défini dans la section 12.5.5 de la RFC 9110. Il demande aux caches de stocker des réponses distinctes en fonction des valeurs indiquées dans les en-têtes de requête. SansVary: HX-Request, un CDN pourrait servir un fragment HTMX comme réponse de page complète. Consultez httpwg.org/specs/rfc9110.html#field.vary. ↩↩ -
Les propriétés personnalisées CSS (variables CSS) sont prises en charge par plus de 97 % des navigateurs dans le monde. Elles suivent la cascade, héritent de leurs valeurs et réagissent aux media queries à l’exécution — des capacités dont les variables de préprocesseur sont dépourvues. Source : caniuse.com/css-variables. ↩
-
Documentation de Google sur hreflang : developers.google.com/search/docs/specialty/international/localized-versions. La valeur
x-defaultdésigne la page de secours destinée aux utilisateurs dont la langue ne figure pas dans la liste hreflang. ↩ -
Alpine.js nécessite
'unsafe-eval'dans la Content Security Policy pour son moteur d’évaluation des expressions. Le build compatible avec CSP (@alpinejs/csp) évite cette exigence, mais présente certaines limitations. Consultez alpinejs.dev/advanced/csp. ↩ -
Les jetons CSRF basés sur HMAC suivent le modèle « Signed Double-Submit Cookie » décrit dans l’OWASP CSRF Prevention Cheat Sheet.
hmac.compare_digestutilise une comparaison en temps constant pour empêcher les attaques par canal auxiliaire fondées sur le temps d’exécution. Consultez cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html. ↩ -
À qualité visuelle équivalente, WebP produit des fichiers 25 à 35 % plus petits que JPEG. Étude de Google sur WebP : developers.google.com/speed/webp/docs/webp_study. ↩
-
103 Early Hints permet au serveur — ou au CDN — d’envoyer une réponse préliminaire contenant des indications de préchargement avant que la réponse finale soit prête. Cloudflare prend en charge Early Hints pour les en-têtes
Linkavecrel=preload. Consultez developer.chrome.com/blog/early-hints. ↩ -
React 18 et ReactDOM pèsent environ 42 Ko après minification et compression gzip. Avec un routeur, une bibliothèque de gestion d’état et le runtime d’un framework de build, les applications React classiques livrent entre 100 et 300 Ko de JavaScript de framework. Source : bundlephobia.com/package/react-dom@18.2.0. ↩↩
-
La politique de versionnement et l’engagement de HTMX en matière de rétrocompatibilité sont documentés sur htmx.org/migration-guide-htmx-1/. Carson Gross a énoncé ce principe de rétrocompatibilité dans Hypermedia Systems (2023), de Gross, Stepinski et Cotter : hypermedia.systems. ↩
-
Vérification de maintenance du 15 mai 2026. La page PyPI et les notes de version de FastAPI indiquent la version 0.136.1 ; la vérification de l’import local a renvoyé FastAPI 0.128.0 et Starlette 0.50.0 pour l’environnement de ce site ; htmx.org indique la version 2.0.10 dans son guide de démarrage rapide ;
npm view htmx.org version dist-tagsa renvoyélatest=2.0.10etnext=4.0.0-beta3;npm view alpinejs versionetnpm view @alpinejs/csp versionont renvoyé3.15.12; le blog officiel de Bootstrap et les métadonnées du paquet npm indiquent la version 5.3.8 ; la page PyPI et la documentation de SQLAlchemy indiquent la version 2.0.49 ; la page PyPI de Pydantic indique la version 2.13.4. ↩↩↩↩ -
htmx 4.0.0-beta6 correspond actuellement au tag npm
next(publié le 23 juillet 2026 ; la série bêta est passée de beta3 le 8 mai 2026 à beta4, puis beta5 et beta6), tandis que le tag npmlatestreste en version 2.0.10. La documentation de la version 4.0 sur four.htmx.org suit le buildnext, l’index des extensions 4.0 répertoriehx-liveethx-nonce, et le guide de migration 4.0 présente les changements à examiner avant de faire migrer des applications de production depuis la version 2.x. Informations vérifiées le 24 juillet 2026 à partir des dist-tags npm dehtmx.org. ↩↩↩↩↩↩ -
Vérification de maintenance du 24 mai 2026. Les commandes d’inventaire local ont recensé 210 articles de blog au format Markdown, 11 fichiers de guides de premier niveau et 48 fichiers d’études de design. Les notes de version de FastAPI indiquent la version 0.136.3 au 2026-05-23, avec une gestion plus stricte des traits de soulignement dans les en-têtes lorsque
convert_underscores=True; la version 0.136.2 valide les champs Server-Sent Event.python3 -m pip index versions fastapia renvoyé la dernière version0.136.3;python3 -m pip index versions sqlalchemya renvoyé la dernière version2.0.50;python3 -m pip index versions pydantica renvoyé la dernière version2.13.4.npm view htmx.org dist-tags version time.modified --jsona renvoyélatest=2.0.10,next=4.0.0-beta4ettime.modified=2026-05-22T15:56:21.948Z; la documentation d’installation de four.htmx.org indiquehtmx.org@4.0.0-beta4. ↩↩ -
Journal des modifications de SQLAlchemy 2.0.50 et article de publication, version publiée le 2026-05-24. La dépendance asyncio
greenletn’est plus installée par défaut ; la cible d’installationsqlalchemy[asyncio]est désormais nécessaire pour l’inclure. La version 2.0.50 abandonne également Python 3.7/3.8/3.9 — la version 3.10 ou ultérieure est maintenant requise —, ajoute des wheels Python pour l’exécution sans GIL et ajoute un paramètre de cadre de fenêtreover(..., exclude=...). Dernière version vérifiée sur PyPI au 2026-06-08. htmx 4.0.0-beta4 (« The Fetchening », 2026-05-22) reste en version bêta, avec une version stable prévue début 2027 ; FastAPI 0.136.3 (2026-05-23), Alpine.js 3.15.12 et Bootstrap 5.3.x restent inchangés sur cette période. ↩↩↩ -
Notes de version de FastAPI : la version 0.137.0 (2026-06-14) remanie les composants internes du routeur. Ainsi,
router.routesn’est plus une liste plate d’objetsAPIRoute, mais une arborescence d’objets intermédiaires — à considérer comme interne. Cette version permet également d’ajouter des routes aprèsinclude_router(), d’inclure un sous-routeur avant la définition de ses routes, évite de copier les routes et ajouteAPIRouter.matches()/.handle(). Elle ne fixe pas Starlette à la version 1.x : la dépendance d’exécution de FastAPI eststarlette>=0.46.0— une version minimale sans limite supérieure —, de façon identique dans les versions 0.136.3, 0.137.0, 0.138.0, 0.139.2 et 0.140.0, comme l’ont confirmé les métadonnéesrequires_distde l’API JSON PyPI le 2026-07-25. La ligne « bump starlette from 1.1.0 to 1.2.1 » (PR #15722) des notes de la version 0.137.0 correspond à une mise à jour Dependabot classée dans la section Internal et ne modifie que le fichier de verrouillage de testuv.lockdu dépôt. (Une limite supérieure existait auparavant : les versions 0.120.4 et 0.121.0 imposaientstarlette<0.50.0,>=0.40.0, mais elle avait disparu dans la version 0.136.3.) Correction appliquée le 2026-07-25 ; la formulation précédente de cette note et l’affirmation correspondante dans le corps du guide étaient erronées. La version 0.137.1 (2026-06-15) corrige le typage de APIRoute et un chemin vide dans un routeur sans préfixe. Notes de version de Starlette : la version 1.0.0 (2026-03-22), sa première version stable en environ 8 ans, a suppriméon_startup/on_shutdown/on_event()ainsi que les décorateurs@app.route()/@app.websocket_route()— utilisezlifespanetRoute/WebSocketRoute; la dernière version est la 1.3.1 (2026-06-12). SQLAlchemy 2.0.51 (journal des modifications, 2026-06-15) apporte uniquement des corrections de bugs, sans incidence sur async ni sur l’installation. Informations vérifiées via PyPI et les notes de version officielles le 2026-06-16. ↩↩↩ -
Notes de version de FastAPI : la version 0.138.0 (2026-06-20) ajoute
app.frontend("/", directory="dist")etrouter.frontend("/", directory="dist")pour servir un frontend statique préalablement construit (PR #15800 ; documentation Frontend) — une fonctionnalité destinée à servir une SPA statique depuisdist/, et non un modèle de rendu côté serveur ; aucun changement incompatible. La version 0.137.2 (2026-06-18) ajouteiter_route_contexts()pour les usages avancés qui parcouraient auparavantrouter.routes— devenu interne depuis la version 0.137.0 ; aucun changement incompatible. Aucune version ultérieure à la 0.138.0 au 2026-06-22. Starlette 1.3.1, Pydantic 2.13.4, Uvicorn 0.49.0, SQLAlchemy 2.0.51, HTMX 2.0.10, Alpine.js 3.15.12 et Bootstrap 5.3.8 restent tous inchangés. Informations vérifiées via PyPI et les notes de version officielles le 2026-06-22. ↩↩ -
Notes de version 0.139.0 de FastAPI, 1er juillet 2026 : « prise en charge des dépendances dans
app.frontend(), par exemple pour l’authentification automatique du frontend par cookie » (PR #15908). Le reste de la version comprend des traductions, de la documentation et des mises à jour de dépendances ; aucun changement incompatible. Vérification effectuée pendant la session le 2 juillet 2026 (PST) : la version 0.139.0 est la plus récente sur la page des versions GitHub. ↩↩ -
Notes de version 0.140.0 de FastAPI, publiées le 24 juillet 2026 à 21:16 UTC (
upload_time_iso_8601PyPI :2026-07-24T21:16:42Z). L’unique entrée de remaniement est « ⚡️ Réduction de l’utilisation de la mémoire dans les dépendances. PR #16049 » (fusionnée le 2026-07-24T21:07:52Z). La régression a été introduite par la PR #14262 — fusionnée le 2025-11-03 — et publiée le même jour dans la version 0.121.0 ; elle ajoutaitfunctools.cached_propertyàDependant.cache_key. Dans la version 0.139.2, la classe comportait dix définitions@cached_property. Dans la version 0.140.0,fastapi/dependencies/models.pydéclare@dataclass(slots=True) class Dependant, tandis que la logique est transférée vers les fonctions de module_get_cache_key(),_get_oauth_scopes(),_uses_scopes()et_is_security_scheme()— source vérifiée dans le tag 0.140.0. Sur la PR fusionnée, le bot CodSpeed indique pour le benchmark mémoiretest_dependency_graphun passage de 17,5 Mo (base) à 1,1 Mo (branche), soit « une amélioration des performances de ×16 » ; la version 0.140.0 ajoute également un benchmark mémoire à la CI (PR #16046) afin d’éviter toute nouvelle régression. Le signalement initial figure dans la discussion #14742, où la version 0.120.4 restait sous environ 400 Mo, tandis que la version 0.121.3 provoquait une erreur OOM en production. Remarque à l’attention des auteurs d’outils :Dependant.oauth_scopes,.cache_key,._uses_scopeset._is_security_schemen’existent plus en tant qu’attributs, etslots=Trueempêche la modification dynamique des instances — un élément API interne non documenté que ce guide n’utilise pas, de la même catégorie que le changement apporté àrouter.routesdans la version 0.137.0. Tous les faits ont été vérifiés de nouveau le 2026-07-25 à partir de PyPI, du API GitHub et du code source associé au tag. ↩↩↩ -
FastAPI 0.141.0 (2026-07-29, 14:47 UTC) a ajouté
app.frontend(check_dir="auto")pour le développement local avecfastapi dev(PR #16102). FastAPI 0.141.1 (2026-07-29, 17:17 UTC) a corrigé la prise en charge des tâches d’arrière-plan et des en-têtes provenant des dépendances dansapp.frontend()(PR #16105) et documentéFASTAPI_ENVdans le guide CLI de FastAPI (PR #16104). Les deux versions sont signées @tiangolo. La version 0.141.1 a été confirmée comme la plus récente sur PyPI le 2026-07-29. ↩↩ -
Les versions 0.140.1 à 0.140.7 de FastAPI ont toutes été publiées le 2026-07-27 entre 12:07 et 17:34 UTC (
upload_time_iso_8601PyPI : 0.140.112:07:51Z, 0.140.214:15:38Z, 0.140.315:30:52Z, 0.140.415:46:49Z, 0.140.516:02:53Z, 0.140.616:31:48Z, 0.140.717:34:47Z). Le descriptif de chaque version ne contient qu’une seule entrée dans la rubrique Refactors : 0.140.1, « Mise à jour de la limite lru_cache pour les dépendances afin de tenir compte des grandes applications » (PR #16062) ; 0.140.2, « Arrêt de la conservation des arborescences de dépendances aplaties » (PR #16065) ; 0.140.3, « Évitement des aplatissements répétés des dépendances dans OpenAPI » (PR #16067) ; 0.140.4, « Abandon du suivi inutilisé des répétitions de dépendances » (PR #16069) ; 0.140.5, « Évitement de l’aplatissement des dépendances pour les champs du corps de requête » (PR #16071) ; 0.140.6, « Évitement de l’aplatissement des dépendances pour les paramètres de requête, principalement pour OpenAPI » (PR #16073) ; 0.140.7, « Évitement de l’aplatissement des dépendances pour OpenAPI » (PR #16076). La valeur du cache provient du diff de la PR #16062, qui remplace trois décorateurs@lru_cache(maxsize=1024)dansfastapi/dependencies/models.pypar@lru_cache(maxsize=_CALLABLE_CLASSIFICATION_CACHE_SIZE)et met à jourtests/test_dependency_models.pypour vérifier quecache_info.maxsize == 4096; le descriptif de la PR indique : « Certains utilisateurs ont signalé un nombre de dépendances supérieur à 1 024 ; cette valeur devrait convenir aux applications plus importantes. » La version 0.140.2 ajoute également un benchmark mémoire (PR #16064) et la version 0.140.7 ajoute des benchmarks de dépendances OpenAPI (PR #16075). La couverture par les benchmarks est donc postérieure à la majeure partie de cette série. Informations vérifiées le 2026-07-27 à partir des versions API GitHub, des diffs des PR et de PyPI ; la version 0.140.7 était la plus récente au moment de la rédaction. ↩↩