Axum et Tokio : construire une API REST haute performance en Rust
Le marché des frameworks web Rust s'est progressivement consolidé depuis 2023, et en 2026 un nom s'impose clairement dans les équipes qui construisent des APIs en production : Axum. Développé par l'équipe Tokio chez AWS, Axum n'est pas le plus ancien framework Rust, ni celui qui affiche les benchmarks bruts les plus impressionnants. Ce qui l'a propulsé au rang de standard de fait, c'est sa cohérence avec l'écosystème Tower, son ergonomie idiomatique, et sa maintenabilité à l'échelle d'une équipe. Les résultats TechEmpower Round 22 le placent dans les cinq premiers frameworks web toutes technologies confondues, avec une latence p99 mesurée en microsecondes sur des charges de plusieurs dizaines de milliers de requêtes par seconde.
Pourquoi Axum s'impose face à Actix-web et Rocket
Comparer les frameworks web Rust revient à comparer des philosophies autant que des performances. Actix-webest le champion des benchmarks bruts : son modèle d'acteurs et son implémentation bas niveau lui permettent d'atteindre des throughputs difficilement égalables. Mais cet avantage a un coût : le code Actix-web est moins idiomatique Rust, le modèle d'erreurs est plus complexe, et l'intégration avec l'écosystème Tower — qui est devenu le standard de facto pour le middleware async en Rust — reste limitée.
Rocketoffre une ergonomie remarquable grâce à ses macros procédurales : la définition de routes ressemble à du Python Flask, et la courbe d'apprentissage est douce. Mais Rocket a longtemps traîné un retard sur l'async natif, et ses performances en charge élevée restent inférieures à celles d'Axum ou Actix-web. Pour un prototype ou une API interne à faible trafic, Rocket est une option valide. Pour un service public exposé à des pics de charge, il n'est pas le premier choix.
Axum se positionne comme le compromis optimal pour les équipes de production. Ses critères de choix sont clairs : intégration native avec Tower (middleware, rate limiting, timeout, tracing partagent tous la même interface Service), extracteurs composables qui s'intègrent naturellement dans le système de types Rust, et une architecture pensée pour la maintenabilité à long terme. Quand une équipe grandit et que la base de code évolue, Axum résiste mieux au temps qu'Actix-web.
Tokio : comprendre le runtime async sous-jacent
Axum repose entièrement sur Tokio, le runtime async de référence en Rust. Comprendre Tokio n'est pas facultatif pour utiliser Axum en production : les bugs de performance les plus courants dans les services Axum sont des bugs Tokio mal compris.
Tokio implémente un modèle de green threads(tâches légères) sur un pool de threads OS. En mode multi-thread (le défaut), le runtime maintient un worker par cœur physique. Chaque worker exécute une boucle d'événements et vole des tâches aux autres workers quand il est inactif. Ce modèle permet de gérer des centaines de milliers de connexions simultanées avec un nombre de threads fixe et prévisible.
Les primitives de coordination — select! pour attendre le premier résultat parmi plusieurs futures, join! pour les exécuter en parallèle, spawn pour créer une tâche indépendante — doivent être utilisées avec connaissance de cause. La cancellation safetyest un concept critique : quand une branche de select! est abandonnée, la future correspondante est droppée. Si cette future maintenait un état intermédiaire (un write partiel sur un socket, un lock acquis), le comportement peut être incorrect.
Deux pièges reviennent systématiquement dans les code reviews de services Axum. Le premier : exécuter du code bloquant (I/O synchrone, calcul CPU intensif) dans un contexte async sansspawn_blocking. Cela bloque le worker Tokio et dégrade la latence de toutes les requêtes en cours. Le second : maintenir un Mutexstandard acquis à travers un point d'await, ce qui peut provoquer des deadlocks. La solution est d'utilisertokio::sync::Mutexou de restructurer le code pour libérer le lock avant l'await.Tokio Console, l'outil de debugging dédié, visualise les tâches en cours, leur durée de vie et les points de blocage, ce qui rend ces diagnostics accessibles.
Structurer une API Axum en production
L'organisation d'un projet Axum qui passe en production suit un schéma qui a fait ses preuves. La séparation en couches — handlers (logique HTTP), services (logique métier),models (structures de données), errors(types d'erreurs applicatifs),middleware(couches transversales) — permet de tester chaque couche indépendamment et de maintenir la base de code à l'échelle.
Le RouterAxum supporte le nesting naturel, ce qui permet de décomposer les routes par domaine fonctionnel. L'état partagé entre handlers s'expose via Arc<AppState>injecté dans le router : Axum garantit que cet état est accessible via l'extracteurState<Arc<AppState>> dans chaque handler sans coût de clonage excessif.
Les extracteurs sont le mécanisme central d'Axum. Pathextrait les segments variables de l'URL, Query les paramètres de query string, Jsondésérialise le corps de la requête. Chaque extracteur peut échouer avec un message d'erreur typé, et Axum convertit automatiquement ces échecs en réponses HTTP appropriées. La validation métier s'intègre naturellement avec la crate validator : une structure de requête annotée avec #[derive(Validate)]et quelques contraintes déclaratives suffit à rejeter les entrées invalides avant même d'entrer dans le handler.
Middleware, logging et observabilité
La force d'Axum sur ce point tient à Tower : tous les middlewares partagent le même traitService, ce qui garantit leur composabilité. La crate tower-http fournit les middlewares essentiels : TraceLayer pour les logs de requêtes structurés,CompressionLayer pour la compression gzip/brotli transparente, CorsLayerpour la gestion des en-têtes CORS, TimeoutLayer pour les timeouts par requête.
L'observabilité repose sur la crate tracing, qui introduit les concepts de spans (unité de travail avec durée) et d'événements (log structuré). Un span ouvert dans un handler propage automatiquement son contexte à travers les appels async, ce qui produit des traces cohérentes même dans un système fortement concurrent. L'export vers OpenTelemetry (Jaeger, Tempo, Datadog) s'effectue via opentelemetry-otlp sans changer le code applicatif.
Pour les métriques, la crate metrics avec le backend metrics-exporter-prometheusexpose un endpoint /metrics au format Prometheus en quelques lignes. Un health check endpoint /healthrépondant avec un JSON contenant la version, le statut des dépendances (base de données, cache) et l'uptime est une pratique standard que les orchestrateurs (Kubernetes, Nomad) utilisent pour les sondes de liveness et readiness.
Gestion d'erreurs idiomatique en Axum
La gestion d'erreurs est souvent le premier point de friction quand on découvre Axum. Le pattern recommandé est de définir un type AppError central qui implémenteIntoResponse— l'interface Axum permettant de convertir une erreur en réponse HTTP. La crate thiserrorsimplifie la définition de ce type : chaque variante de l'enum correspond à un cas d'erreur applicatif, annoté avec le message formaté et le code HTTP approprié.
Dans les handlers, la crate anyhowpermet la propagation fluide des erreurs avec l'opérateur ?. Une conversion From<anyhow::Error> for AppErrortransforme automatiquement les erreurs inattendues en 500 avec log de l'erreur complète côté serveur et message générique côté client.
Les réponses d'erreur JSON suivent idéalement le standard RFC 7807 (Problem Details) : un objet avec type, title, status, detailet instance. Ce format est reconnu par les clients HTTP modernes et facilite le débogage. Éviter les panics en production est non négociable : un panic!non récupéré dans un handler Axum provoque l'arrêt brutal du worker Tokio concerné et une réponse 500 sans log structuré — le pire scénario pour un service en production.
Base de données avec SQLx : async, type-safe, sans ORM
SQLxest la bibliothèque de choix pour accéder à PostgreSQL (ou MySQL, SQLite) depuis Axum. Son positionnement est unique : pas d'ORM, pas de DSL propriétaire, mais une vérification de typage à la compilation via des requêtes SQL ordinaires annotées avec la macrosqlx::query!. À la compilation, SQLx contacte la base de données (ou utilise un cache offline) et vérifie que les colonnes retournées correspondent aux types Rust attendus. Les erreurs de schéma deviennent des erreurs de compilation, pas des panics en production.
Comparé à Diesel (ORM compile-time, DSL puissant mais complexe) etSeaORM(ORM async, plus proche d'Active Record), SQLx est la solution la plus légère et la plus proche du SQL natif. Pour les équipes habituées à PostgreSQL, écrire du SQL pur est un avantage : les requêtes sont lisibles, optimisables et portables.
La gestion des connexions repose sur PgPool, un pool async avec configuration du nombre minimum et maximum de connexions, timeouts d'acquisition et de connexion. Les migrations se gèrent via sqlx-cli, un outil en ligne de commande qui applique les migrations dans l'ordre et maintient un historique dans une table dédiée. Les benchmarks sur des charges représentatives montrent des throughputs 3 à 5 fois supérieurs à des services équivalents en Node.js (Fastify + pg) ou Python (FastAPI + asyncpg) sur la même infrastructure, avec une consommation mémoire 5 à 10 fois plus faible.
Benchmarks et résultats en production
Les chiffres mesurés sur des services Axum en production convergent vers un profil cohérent. Sur un serveur à 4 vCPU et 8 Go de RAM, un service Axum avec pool SQLx atteint typiquement 8 000 à 15 000 requêtes/seconde avec une latence p50 inférieure à 1 ms, p95 inférieure à 3 ms, p99 inférieure à 8 ms. Un service FastAPI équivalent sur la même infrastructure plafonne entre 1 500 et 3 000 req/s avec un p99 de 30 à 60 ms. Un service Spring Boot atteignait 2 000 à 4 000 req/s avec un p99 de 20 à 50 ms, mais avec une empreinte mémoire JVM de 400 à 600 Mo contre 20 à 40 Mo pour le binaire Rust.
Un cas réel illustre concrètement ces gains : un service de tokenisation de paiement remplacé par une implémentation Axum + SQLx. Avant : service Python, latence p99 de 85 ms, scaling horizontal nécessaire dès 500 req/s, 12 pods en production. Après : latence p99 de 6 ms, scaling nécessaire à partir de 8 000 req/s, 2 pods en production. Économie d'infrastructure : 80 %. Incidents liés à des memory leaks ou des race conditions : zéro depuis la migration — le borrow checker avait éliminé les classes entières de bugs présents dans le code Python original.
« Rust ne rend pas le développement plus rapide dans l'immédiat — il rend le déploiement plus serein dans la durée. Quand un service Axum est en production depuis six mois sans incident de sécurité mémoire ni fuite de ressources, on comprend pourquoi des équipes font ce choix. »
Axum et Tokio forment en 2026 le socle technique le plus solide pour construire des APIs Rust destinées à la production. Leur adoption ne se justifie pas uniquement par les benchmarks : elle se justifie par la cohérence de l'écosystème, la maintenabilité du code à long terme, et la confiance que le compilateur Rust permet d'avoir dans des systèmes qui n'ont pas le droit à l'erreur.