Ne perds pas une semaine Ă  Ă©valuer 40 crates. Voici la stack backend 2026 tranchĂ©e que j’utilise pour chaque API Axum.

CƓur#

  • axum 0.8 : router. Extracteurs et middlewares Tower. À associer avec tokio 1.x full. Le cĂąblage complet est dĂ©taillĂ© dans le guide API REST Axum + Postgres.
  • tokio : runtime. rt-multi-thread, macros, signal (graceful shutdown !), time.
  • serde + serde_json : DTOs. #[serde(rename_all="camelCase")] Ă  la frontiĂšre API, snake_case Ă  l’intĂ©rieur.

Données#

  • sqlx 0.9 (runtime-tokio, tls-rustls-ring-webpki, postgres, macros, migrate) : Postgres async avec requĂȘtes vĂ©rifiĂ©es Ă  la compilation. query! attrape les fautes SQL au build. Attention en migrant depuis 0.8 : la feature runtime-tokio-rustls n’existe plus, le runtime et le TLS se dĂ©clarent sĂ©parĂ©ment.
  • redis 0.26 / deadpool-redis : cache, rate limit et sessions. Pas de cache dans un HashMap partagĂ© entre instances.
  • refinery ou sqlx::migrate! : migrations versionnĂ©es, jouĂ©es au boot. Jamais de CREATE TABLE Ă  la main en prod.

Plomberie HTTP#

  • tower-http : TraceLayer, CorsLayer, CompressionLayer, RequestIdLayer, TimeoutLayer. Il te faut les quatre en prod. Depuis la 0.6, TimeoutLayer::new est dĂ©prĂ©ciĂ© au profit de TimeoutLayer::with_status_code, qui te laisse choisir le code renvoyĂ© au client.
  • utoipa + utoipa-swagger-ui : OpenAPI gĂ©nĂ©rĂ© depuis le code. Tes Ă©quipes front vont t’adorer.
  • validator (derive) : #[validate(length, email, range)] sur les DTOs d’entrĂ©e. Rejette les donnĂ©es pourries Ă  la frontiĂšre.

Observabilité#

  • tracing + tracing-subscriber (env-filter, json) : logs structurĂ©s. JSON en prod, joli en dev.
  • opentelemetry + tracing-opentelemetry : traces vers Jaeger ou Tempo quand les logs ne suffisent plus.
  • metrics + metrics-exporter-prometheus : /metrics pour le RPS, les histogrammes de latence et les stats du pool.

Robustesse#

  • thiserror + anyhow : thiserror pour les erreurs d’API, anyhow pour les binaires et le dĂ©marrage. Un seul AppError mappĂ© vers HTTP, placĂ© dans src/error.rs.
  • backon ou tokio-retry : backoff exponentiel pour les appels aval. Pas de boucles de retry Ă  la main.
  • reqwest (rustls-tls, json) : HTTP sortant. Mets des timeouts ! Par dĂ©faut c’est infini, et c’est un tueur en prod — c’est exactement ce qui plombait ma p99.

Mon extrait Cargo par défaut#

axum = "0.8"
axum-extra = { version = "0.10", features = ["cookie"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sqlx = { version = "0.9", default-features = false, features = ["runtime-tokio", "tls-rustls-ring-webpki", "postgres", "macros", "migrate", "uuid", "chrono"] }
tower-http = { version = "0.6", features = ["full"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
thiserror = "2"
anyhow = "1"
validator = { version = "0.20", features = ["derive"] }
utoipa = { version = "5", features = ["axum_extras"] }

Les versions, au 17 septembre 2026#

Relevées sur crates.io le jour de la mise à jour de cet article, et toutes utilisées par le template du site, qui compile en intégration continue. Si tu lis ça dans six mois, vérifie : cargo add <crate> te dira la vérité mieux que moi.

CrateVersionÀ quoi faire attention
axum0.8.9ParamĂštres de chemin en {id} depuis la 0.8
axum-extra0.12Suit Axum de prÚs, vérifie la compatibilité à chaque bump
tokio1.53Stable depuis des années, full en dev, features ciblées en prod
sqlx0.9runtime-tokio-rustls supprimée, voir plus bas
tower-http0.7TimeoutLayer::new déprécié
serde1.0.229Rien à signaler, et c’est trùs bien
tracing-subscriber0.3.23env-filter et json ne sont pas par défaut
thiserror2.0La 2.0 a durci quelques rĂšgles de #[from]
validator0.21L’API bouge à chaque version mineure
jsonwebtoken11Grosse refonte depuis la 9
argon20.6SaltString a disparu, voir plus bas
rand0.9rand::rng() remplace thread_rng()

Trois migrations qui cassent, et leur correctif#

Ce sont celles qui m’ont coĂ»tĂ© du temps en remontant le template Ă  jour.

SQLx : le runtime et le TLS se déclarent séparément#

La feature runtime-tokio-rustls n’existe plus en 0.9. Un cargo add qui la mentionne Ă©choue avec unrecognized feature.

# Avant (0.8)
sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "postgres"] }

# AprĂšs (0.9)
sqlx = { version = "0.9", default-features = false, features = [
  "runtime-tokio", "tls-rustls-ring-webpki", "postgres", "macros", "migrate",
] }

Le default-features = false compte : les features par dĂ©faut de SQLx 0.9 incluent any, macros, migrate et json, plus large que nĂ©cessaire pour une API qui ne parle qu’à Postgres.

argon2 : le sel se génÚre tout seul#

En 0.5, il fallait construire un SaltString et le passer au hasher. En 0.6, hash_password prend le mot de passe et rien d’autre : le sel vient de la feature getrandom, active par dĂ©faut.

// argon2 0.6
use argon2::password_hash::phc::PasswordHash;
use argon2::{Argon2, PasswordHasher, PasswordVerifier};

let hash = Argon2::default()
    .hash_password(password.as_bytes())?
    .to_string();

Tout exemple qui construit un SaltString::generate(&mut OsRng) est Ă©crit pour la 0.5. Il ne compile plus, et l’erreur (no SaltString in the root) n’oriente pas vers la bonne solution.

tower-http : le timeout choisit son code de retour#

TimeoutLayer::new est dĂ©prĂ©ciĂ© en 0.7 au profit de TimeoutLayer::with_status_code, qui rend explicite ce que reçoit le client quand la requĂȘte expire.

.layer(TimeoutLayer::with_status_code(
    StatusCode::REQUEST_TIMEOUT,
    Duration::from_secs(10),
))

Ce que j’évite (pour des API)#

  • Les gros ORM magiques : les requĂȘtes SQLx explicites scalent mieux pour du backend
  • async-std pour des serveurs en 2026 : Tokio a gagnĂ©, l’écosystĂšme suit
  • Rouler ta propre auth JWT : prends jsonwebtoken + axum-login ou une auth externalisĂ©e (Clerk/Auth0) pour dĂ©marrer. Si tu la gĂšres toi-mĂȘme, suis l’auth JWT dans Axum comme il faut : refresh tokens hashĂ©s, rotation, middleware

Tu veux tout ça déjà cùblé ? Le template Axum du site utilise exactement ces crates, avec le Dockerfile et les tests. Il compile, la CI le vérifie à chaque commit.