← Tous les articles • 01/02/2026 • 2 min de lecture

Construire une API REST performante en Rust avec Axum et Postgres

Construis une API REST Rust prête pour la prod avec Axum, Tokio, SQLx et Postgres : routing, state, validation, erreurs, tracing, pooling, migrations.

#axum#postgres#backend#tutorial

Oublie les tutos jouets de type “hello world”. Voici comment construire une API REST prête pour la production en Rust avec Axum + SQLx + Postgres, la stack que je recommande à chaque équipe backend en 2026.

Au programme : routing propre, state partagé, JSON validé, erreurs correctes, logs structurés, pool de connexions et migrations.

Pourquoi Axum ?

Si tu viens d’Express, FastAPI ou du chi de Go, Axum te semblera familier, avec la sécurité de Rust en plus.

1. Setup du projet

cargo new rust-tasks-api && cd rust-tasks-api
cargo add axum tokio -F full serde -F derive serde_json
cargo add sqlx -F runtime-tokio,postgres,macros,migrate
cargo add tracing tracing-subscriber tower tower-http validator -F derive
cargo add thiserror anyhow uuid -F v4,serde dotenvy

L’essentiel du Cargo.toml : axum 0.7, tokio 1.x, sqlx 0.8, tower-http pour CORS, trace et compression.

2. Une architecture qui tient la charge

src/
  main.rs        # câblage : config, pool, router, serve
  config.rs      # config d'env
  db.rs          # PgPool
  error.rs       # AppError -> HTTP
  state.rs       # AppState
  routes/
    mod.rs
    tasks.rs     # handlers
    health.rs
  models/
    task.rs

Règle : des handlers fins. La logique métier vit dans des fonctions qui prennent &PgPool et renvoient Result<T, AppError>. Tu me remercieras à 50 routes.

3. State et pool

// state.rs
use sqlx::PgPool;

#[derive(Clone)]
pub struct AppState {
    pub db: PgPool,
}
// db.rs
use sqlx::postgres::{PgPoolOptions, PgPool};

pub async fn connect(url: &str) -> anyhow::Result<PgPool> {
    let pool = PgPoolOptions::new()
        .max_connections(20)
        .min_connections(2)
        .acquire_timeout(std::time::Duration::from_secs(3))
        .connect(url).await?;
    sqlx::migrate!("./migrations").run(&pool).await?;
    Ok(pool)
}

Pourquoi max_connections(20) ? Commence petit. Postgres déteste 200 connexions inactives. Scale avec PgBouncer plus tard, pas avec un pool géant.

4. Des erreurs bien gérées

// error.rs
use axum::{http::StatusCode, response::{IntoResponse, Response}, Json};
use serde_json::json;

#[derive(Debug, thiserror::Error)]
pub enum AppError {
    #[error("not found")]
    NotFound,
    #[error("bad request: {0}")]
    BadRequest(String),
    #[error(transparent)]
    Db(#[from] sqlx::Error),
}

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        let (status, msg) = match &self {
            AppError::NotFound => (StatusCode::NOT_FOUND, self.to_string()),
            AppError::BadRequest(_) => (StatusCode::BAD_REQUEST, self.to_string()),
            AppError::Db(_) => (StatusCode::INTERNAL_SERVER_ERROR, "internal error".into()),
        };
        tracing::error!(error = ?self, "request failed");
        (status, Json(json!({ "error": msg }))).into_response()
    }
}

Ne expose jamais les détails de sqlx::Error aux clients. Loggue l’erreur complète, renvoie une 500 générique.

5. Handlers : du CRUD avec validation

// models/task.rs
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use validator::Validate;

#[derive(Serialize, sqlx::FromRow)]
pub struct Task { pub id: Uuid, pub title: String, pub done: bool }

#[derive(Deserialize, Validate)]
pub struct CreateTask {
    #[validate(length(min = 1, max = 200))]
    pub title: String,
}
// routes/tasks.rs
use axum::{extract::{Path, State}, Json};
use uuid::Uuid;
use crate::{state::AppState, error::AppError, models::task::*};

pub async fn create_task(
    State(s): State<AppState>,
    Json(input): Json<CreateTask>,
) -> Result<(axum::http::StatusCode, Json<Task>), AppError> {
    use validator::Validate;
    input.validate().map_err(|e| AppError::BadRequest(e.to_string()))?;
    let task = sqlx::query_as!(
        Task,
        "INSERT INTO tasks (id, title, done) VALUES ($1, $2, false) RETURNING id, title, done",
        Uuid::new_v4(), input.title
    ).fetch_one(&s.db).await?;
    Ok((axum::http::StatusCode::CREATED, Json(task)))
}

pub async fn get_task(
    State(s): State<AppState>,
    Path(id): Path<Uuid>,
) -> Result<Json<Task>, AppError> {
    let task = sqlx::query_as!(Task, "SELECT id, title, done FROM tasks WHERE id=$1", id)
        .fetch_optional(&s.db).await?
        .ok_or(AppError::NotFound)?;
    Ok(Json(task))
}

Avec les macros query_as! et query!, ton SQL est vérifié à la compilation. Les fautes de frappe sont caught avant la CI.

6. Router et middlewares

// main.rs (extrait)
use axum::{routing::{get, post}, Router};
use tower_http::{trace::TraceLayer, cors::CorsLayer, compression::CompressionLayer};
use std::net::SocketAddr;

let app = Router::new()
    .route("/health", get(health))
    .route("/tasks", post(create_task).get(list_tasks))
    .route("/tasks/:id", get(get_task))
    .layer(TraceLayer::new_for_http())
    .layer(CompressionLayer::new())
    .layer(CorsLayer::permissive())
    .with_state(state);

let addr = SocketAddr::from(([0, 0, 0, 0], 8080));
axum::serve(tokio::net::TcpListener::bind(addr).await?, app).await?;

Ajoute TraceLayer dès le premier jour. Débugger la prod sans trace_id ni logs structurés, c’est l’enfer.

7. Benchmark rapide

oha -c 100 -z 20s http://localhost:8080/health
# Attends-toi à 30k-60k rps sur health, 5k-15k rps sur un SELECT Postgres simple, sur un laptop

Si tu es sous 2k rps sur des lectures triviales, vérifie : taille du pool, index manquant, allocs JSON, build debug (utilise --release !).

Prochaines étapes

🚀 Tu veux le starter complet ? Récupère mon template Axum gratuit (cette structure + Dockerfile + CI + migrations) dans la newsletter. La Roadmap payante va 10x plus loin : tests, auth, observabilité, déploiement.


Ça t'a plu ? Récupère la checklist prod.

Template Axum + patterns Postgres + checklist p99. Gratuit, un email par semaine.

Je la veux →

🚀 Gratuit : la checklist Rust Backend Prod

Rejoins les devs backend. Reçois mon template Axum, ma checklist latence p99 et mes questions d'entretien. Un email pratique par semaine.

Zéro spam. Désinscription en un clic. Ce que tu vas recevoir →