<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="fr">
    <title>Rust Backend Lab</title>
    <subtitle>Le Rust backend pratique : Axum, Postgres, microservices et performance. Des API rapides, une latence réduite, du code prêt pour la prod.</subtitle>
    <link rel="self" type="application/atom+xml" href="https://rust-backend-lab.pages.dev/atom.xml"/>
    <link rel="alternate" type="text/html" href="https://rust-backend-lab.pages.dev"/>
    <generator uri="https://www.getzola.org/">Zola</generator>
    <updated>2026-09-14T00:00:00+00:00</updated>
    <id>https://rust-backend-lab.pages.dev/atom.xml</id>
    <entry xml:lang="fr">
        <title>L&#39;auth JWT dans Axum comme il faut : login, refresh, middleware</title>
        <published>2026-09-14T00:00:00+00:00</published>
        <updated>2026-09-14T00:00:00+00:00</updated>
        
        <author>
          <name>Rust Backend Lab</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://rust-backend-lab.pages.dev/blog/jwt-auth-axum/"/>
        <id>https://rust-backend-lab.pages.dev/blog/jwt-auth-axum/</id>
        
        <content type="html" xml:base="https://rust-backend-lab.pages.dev/blog/jwt-auth-axum/">&lt;p&gt;La plupart des exemples JWT avec Axum s’arrêtent au retour d’un token signé. Celui-ci couvre le cycle complet : hash argon2 des mots de passe, JWT d’accès de 15 minutes, refresh tokens de 7 jours hashés en Postgres, rotation, middleware d’auth et extracteur &lt;code&gt;AuthUser&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;le-modele-de-tokens-acces-stateless-refresh-stocke&quot;&gt;Le modèle de tokens : accès stateless, refresh stocké&lt;/h2&gt;
&lt;p&gt;Le flow de login utilise deux types de tokens. Les tokens d’accès sont des JWT signés qui expirent en 15 minutes. Ils ne sont pas stockés en Postgres, parce que les stocker casserait l’intérêt d’un token stateless et forcerait un aller-retour BDD à chaque requête.&lt;/p&gt;
&lt;p&gt;Les refresh tokens ne sont pas des JWT. Ce sont 64 octets aléatoires, encodés en hex, hashés en SHA-256 et stockés dans une table &lt;code&gt;refresh_tokens&lt;/code&gt;. Le client reçoit la valeur en clair dans un cookie &lt;code&gt;httpOnly&lt;/code&gt;. Le serveur ne voit plus que le hash après le login, donc un snapshot Postgres qui fuite ne donne pas de refresh tokens utilisables à un attaquant.&lt;/p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Token&lt;/th&gt;&lt;th&gt;État&lt;/th&gt;&lt;th&gt;Durée de vie&lt;/th&gt;&lt;th&gt;Stockage&lt;/th&gt;&lt;th&gt;Révocation&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;JWT d’accès&lt;/td&gt;&lt;td&gt;Stateless&lt;/td&gt;&lt;td&gt;15 minutes&lt;/td&gt;&lt;td&gt;Mémoire client ou cookie sécurisé&lt;/td&gt;&lt;td&gt;Expire seul, non stocké&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Refresh token&lt;/td&gt;&lt;td&gt;Hash stocké&lt;/td&gt;&lt;td&gt;7 jours&lt;/td&gt;&lt;td&gt;&lt;code&gt;refresh_tokens.token_hash&lt;/code&gt;&lt;/td&gt;&lt;td&gt;Flag &lt;code&gt;revoked&lt;/code&gt; ou suppression de ligne&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Ne stocke pas les JWT d’accès en Postgres. Garde-les courts, et fais le coûteux travail de révocation sur le refresh token.&lt;/p&gt;
&lt;h2 id=&quot;le-piege-du-localstorage-pour-les-spa&quot;&gt;Le piège du localStorage pour les SPA&lt;/h2&gt;
&lt;p&gt;Si tu construis une SPA navigateur, le pire endroit pour un JWT c’est &lt;code&gt;localStorage&lt;/code&gt;. N’importe quel script injecté lit &lt;code&gt;localStorage.token&lt;/code&gt; aussitôt. Un refresh token dans &lt;code&gt;localStorage&lt;/code&gt;, c’est pire : il vit longtemps et laisse souvent l’attaquant se réauthentifier en silence.&lt;/p&gt;
&lt;p&gt;Le découpage classique en prod :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Token d’accès : renvoyé dans le corps JSON, gardé en mémoire JavaScript. La SPA l’envoie avec &lt;code&gt;Authorization: Bearer ...&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Refresh token : posé en cookie &lt;code&gt;httpOnly&lt;/code&gt; scopé aux routes d’auth. JavaScript ne peut pas le lire.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Le cookie &lt;code&gt;httpOnly&lt;/code&gt; protège le refresh token contre l’exfiltration XSS. En contrepartie le CSRF devient pertinent, donc mets &lt;code&gt;SameSite=Strict&lt;/code&gt; ou &lt;code&gt;Lax&lt;/code&gt;, restreins le path du cookie, et fais lire le cookie (pas un corps JSON) à l’endpoint de refresh.&lt;/p&gt;
&lt;p&gt;Si tu as déjà le squelette d’API Axum + Postgres, l’organisation du &lt;a href=&quot;/blog/axum-postgres-rest-api/&quot;&gt;guide API REST Axum Postgres&lt;/a&gt; sert de base. Pour les dépendances non auth, garde la liste courte. Les choix actuels sont couverts dans &lt;a href=&quot;/blog/best-rust-crates-api-2026/&quot;&gt;les meilleures crates Rust pour une API&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;dependances-et-schema-postgres&quot;&gt;Dépendances et schéma Postgres&lt;/h2&gt;
&lt;p&gt;Garde la stack d’auth petite : Axum, SQLx, &lt;code&gt;jsonwebtoken&lt;/code&gt;, &lt;code&gt;argon2&lt;/code&gt;, &lt;code&gt;axum-extra&lt;/code&gt; pour les cookies, &lt;code&gt;sha2&lt;/code&gt; et &lt;code&gt;hex&lt;/code&gt; pour hasher les refresh tokens.&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;toml&quot;&gt;[dependencies]
axum = &amp;quot;0.7&amp;quot;
axum-extra = { version = &amp;quot;0.9&amp;quot;, features = [&amp;quot;cookie&amp;quot;] }
jsonwebtoken = &amp;quot;9&amp;quot;
argon2 = &amp;quot;0.5&amp;quot;
sqlx = { version = &amp;quot;0.8&amp;quot;, features = [&amp;quot;runtime-tokio-rustls&amp;quot;, &amp;quot;postgres&amp;quot;, &amp;quot;uuid&amp;quot;, &amp;quot;chrono&amp;quot;, &amp;quot;migrate&amp;quot;] }
tokio = { version = &amp;quot;1&amp;quot;, features = [&amp;quot;full&amp;quot;] }
serde = { version = &amp;quot;1&amp;quot;, features = [&amp;quot;derive&amp;quot;] }
serde_json = &amp;quot;1&amp;quot;
uuid = { version = &amp;quot;1&amp;quot;, features = [&amp;quot;v4&amp;quot;, &amp;quot;serde&amp;quot;] }
chrono = { version = &amp;quot;0.4&amp;quot;, features = [&amp;quot;serde&amp;quot;] }
sha2 = &amp;quot;0.10&amp;quot;
hex = &amp;quot;0.4&amp;quot;
rand = &amp;quot;0.8&amp;quot;
thiserror = &amp;quot;1&amp;quot;
anyhow = &amp;quot;1&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Schéma pour les utilisateurs et les refresh tokens :&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;sql&quot;&gt;CREATE EXTENSION IF NOT EXISTS pgcrypto;

CREATE TABLE users (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  email text UNIQUE NOT NULL,
  password_hash text NOT NULL,
  created_at timestamptz NOT NULL DEFAULT now()
);

CREATE TABLE refresh_tokens (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
  token_hash text NOT NULL UNIQUE,
  expires_at timestamptz NOT NULL,
  revoked boolean NOT NULL DEFAULT false,
  created_at timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX idx_refresh_tokens_user_id ON refresh_tokens(user_id);
CREATE INDEX idx_refresh_tokens_token_hash ON refresh_tokens(token_hash);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Le hash du refresh token est en texte parce qu’on hashe le refresh token hex en SHA-256 et on stocke le digest hex.&lt;/p&gt;
&lt;h2 id=&quot;state-applicatif-claims-et-helpers-de-tokens&quot;&gt;State applicatif, claims et helpers de tokens&lt;/h2&gt;
&lt;p&gt;Le state applicatif garde le pool Postgres et le secret JWT. Mets le secret derrière un &lt;code&gt;Arc&amp;lt;String&amp;gt;&lt;/code&gt; pour que le middleware clone le state sans coût.&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;rust&quot;&gt;#[derive(Clone)]
pub struct AppState {
    pub db: PgPool,
    pub jwt_secret: Arc&amp;lt;String&amp;gt;,
}

const ACCESS_TTL_SECONDS: u64 = 15 * 60;
const REFRESH_TTL_DAYS: i64 = 7;

#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
    sub: String,
    email: String,
    token_type: String,
    exp: u64,
    iat: u64,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Signature du token d’accès :&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;rust&quot;&gt;fn sign_access_token(user_id: &amp;amp;Uuid, email: &amp;amp;str, state: &amp;amp;AppState) -&amp;gt; Result&amp;lt;String, AuthError&amp;gt; {
    let now = Utc::now().timestamp() as u64;

    let claims = Claims {
        sub: user_id.to_string(),
        email: email.to_owned(),
        token_type: &amp;quot;access&amp;quot;.to_owned(),
        exp: now + ACCESS_TTL_SECONDS,
        iat: now,
    };

    Ok(jsonwebtoken::encode(
        &amp;amp;Header::default(),
        &amp;amp;claims,
        &amp;amp;EncodingKey::from_secret(state.jwt_secret.as_bytes()),
    )?)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;La vérification du token d’accès contrôle l’expiration et le type :&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;rust&quot;&gt;fn decode_access_token(token: &amp;amp;str, state: &amp;amp;AppState) -&amp;gt; Result&amp;lt;Claims, AuthError&amp;gt; {
    let data = jsonwebtoken::decode::&amp;lt;Claims&amp;gt;(
        token,
        &amp;amp;DecodingKey::from_secret(state.jwt_secret.as_bytes()),
        &amp;amp;Validation::new(Algorithm::HS256),
    )?;

    if data.claims.token_type != &amp;quot;access&amp;quot; {
        return Err(AuthError::InvalidToken);
    }

    Ok(data.claims)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Les refresh tokens sont aléatoires et hashés avant de toucher la base :&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;rust&quot;&gt;fn generate_refresh_token() -&amp;gt; String {
    let mut bytes = [0u8; 64];
    rand::rngs::OsRng.fill_bytes(&amp;amp;mut bytes);
    hex::encode(bytes)
}

fn hash_refresh_token(token: &amp;amp;str) -&amp;gt; String {
    let mut hasher = Sha256::new();
    hasher.update(token.as_bytes());
    hex::encode(hasher.finalize())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;login-argon2-et-emission&quot;&gt;Login : argon2 et émission&lt;/h2&gt;
&lt;p&gt;Le login accepte email et mot de passe, vérifie le mot de passe avec argon2, puis émet un JWT d’accès court et un cookie de refresh &lt;code&gt;httpOnly&lt;/code&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;rust&quot;&gt;#[derive(Deserialize)]
pub struct LoginRequest {
    email: String,
    password: String,
}

#[derive(Serialize)]
pub struct AuthTokens {
    access_token: String,
    token_type: &amp;amp;&amp;#39;static str,
    expires_in: u64,
}

#[derive(sqlx::FromRow)]
struct UserRow {
    id: Uuid,
    email: String,
    password_hash: String,
}

async fn login(
    State(state): State&amp;lt;AppState&amp;gt;,
    mut jar: CookieJar,
    Json(payload): Json&amp;lt;LoginRequest&amp;gt;,
) -&amp;gt; Result&amp;lt;(CookieJar, Json&amp;lt;AuthTokens&amp;gt;), AuthError&amp;gt; {
    let email = payload.email.trim().to_lowercase();

    let user = sqlx::query_as::&amp;lt;_, UserRow&amp;gt;(
        &amp;quot;SELECT id, email, password_hash FROM users WHERE email = $1&amp;quot;,
    )
    .bind(&amp;amp;email)
    .fetch_optional(&amp;amp;state.db)
    .await?
    .ok_or(AuthError::InvalidCredentials)?;

    let parsed_hash = PasswordHash::new(&amp;amp;user.password_hash)
        .map_err(|_| AuthError::InvalidCredentials)?;

    Argon2::default()
        .verify_password(payload.password.as_bytes(), &amp;amp;parsed_hash)
        .map_err(|_| AuthError::InvalidCredentials)?;

    let access_token = sign_access_token(&amp;amp;user.id, &amp;amp;user.email, &amp;amp;state)?;
    let refresh_token_plain = generate_refresh_token();
    let refresh_token_hash = hash_refresh_token(&amp;amp;refresh_token_plain);

    sqlx::query(
        &amp;quot;INSERT INTO refresh_tokens (user_id, token_hash, expires_at)
         VALUES ($1, $2, $3)&amp;quot;,
    )
    .bind(user.id)
    .bind(&amp;amp;refresh_token_hash)
    .bind(Utc::now() + chrono::Duration::days(REFRESH_TTL_DAYS))
    .execute(&amp;amp;state.db)
    .await?;

    let mut refresh_cookie = Cookie::new(&amp;quot;refresh_token&amp;quot;, refresh_token_plain);
    refresh_cookie.set_path(&amp;quot;/&amp;quot;);
    refresh_cookie.set_http_only(true);
    refresh_cookie.set_secure(!cfg!(debug_assertions));
    refresh_cookie.set_same_site(SameSite::Strict);
    refresh_cookie.set_max_age(Duration::days(REFRESH_TTL_DAYS as u64));

    jar.add(refresh_cookie);

    Ok((
        jar,
        Json(AuthTokens {
            access_token,
            token_type: &amp;quot;Bearer&amp;quot;,
            expires_in: ACCESS_TTL_SECONDS,
        }),
    ))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Le détail important : le refresh token en clair n’est jamais loggué ni renvoyé en JSON. La base ne stocke que son hash SHA-256.&lt;/p&gt;
&lt;h2 id=&quot;rotation-du-refresh-dans-une-transaction&quot;&gt;Rotation du refresh dans une transaction&lt;/h2&gt;
&lt;p&gt;Le refresh doit faire plus que décoder un token. Il hashe la valeur du cookie, verrouille la ligne correspondante, charge l’utilisateur, révoque l’ancien token et en insère un nouveau. Un refresh token volé devient à usage unique.&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;rust&quot;&gt;#[derive(sqlx::FromRow)]
struct RefreshTokenRow {
    user_id: Uuid,
    expires_at: chrono::DateTime&amp;lt;chrono::Utc&amp;gt;,
}

async fn refresh(
    State(state): State&amp;lt;AppState&amp;gt;,
    mut jar: CookieJar,
) -&amp;gt; Result&amp;lt;(CookieJar, Json&amp;lt;AuthTokens&amp;gt;), AuthError&amp;gt; {
    let refresh_token_plain = jar
        .get(&amp;quot;refresh_token&amp;quot;)
        .ok_or(AuthError::InvalidRefreshToken)?
        .value()
        .to_owned();

    let token_hash = hash_refresh_token(&amp;amp;refresh_token_plain);
    let mut tx = state.db.begin().await?;

    let row = sqlx::query_as::&amp;lt;_, RefreshTokenRow&amp;gt;(
        r#&amp;quot;
        SELECT user_id, expires_at
        FROM refresh_tokens
        WHERE token_hash = $1
          AND revoked = false
          AND expires_at &amp;gt; now()
        FOR UPDATE
        &amp;quot;#,
    )
    .bind(&amp;amp;token_hash)
    .fetch_optional(&amp;amp;mut *tx)
    .await?
    .ok_or(AuthError::InvalidRefreshToken)?;

    sqlx::query(&amp;quot;UPDATE refresh_tokens SET revoked = true WHERE token_hash = $1&amp;quot;)
        .bind(&amp;amp;token_hash)
        .execute(&amp;amp;mut *tx)
        .await?;

    let user = sqlx::query_as::&amp;lt;_, UserRow&amp;gt;(&amp;quot;SELECT id, email FROM users WHERE id = $1&amp;quot;)
        .bind(row.user_id)
        .fetch_one(&amp;amp;mut *tx)
        .await?;

    let access_token = sign_access_token(&amp;amp;user.id, &amp;amp;user.email, &amp;amp;state)?;
    let new_refresh_token_plain = generate_refresh_token();
    let new_refresh_token_hash = hash_refresh_token(&amp;amp;new_refresh_token_plain);

    sqlx::query(
        &amp;quot;INSERT INTO refresh_tokens (user_id, token_hash, expires_at)
         VALUES ($1, $2, $3)&amp;quot;,
    )
    .bind(user.id)
    .bind(&amp;amp;new_refresh_token_hash)
    .bind(Utc::now() + chrono::Duration::days(REFRESH_TTL_DAYS))
    .execute(&amp;amp;mut *tx)
    .await?;

    tx.commit().await?;

    let mut refresh_cookie = Cookie::new(&amp;quot;refresh_token&amp;quot;, new_refresh_token_plain);
    refresh_cookie.set_path(&amp;quot;/&amp;quot;);
    refresh_cookie.set_http_only(true);
    refresh_cookie.set_secure(!cfg!(debug_assertions));
    refresh_cookie.set_same_site(SameSite::Strict);
    refresh_cookie.set_max_age(Duration::days(REFRESH_TTL_DAYS as u64));

    jar.add(refresh_cookie);

    Ok((
        jar,
        Json(AuthTokens {
            access_token,
            token_type: &amp;quot;Bearer&amp;quot;,
            expires_in: ACCESS_TTL_SECONDS,
        }),
    ))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Le verrou &lt;code&gt;FOR UPDATE&lt;/code&gt; est volontaire. Si deux requêtes se battent pour le même refresh token, une seule verrouille et fait la rotation. La seconde attend, voit &lt;code&gt;revoked = true&lt;/code&gt;, et échoue.&lt;/p&gt;
&lt;h2 id=&quot;middleware-d-auth-et-extracteur-authuser&quot;&gt;Middleware d’auth et extracteur &lt;code&gt;AuthUser&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Dans Axum, un middleware au niveau des routes est le bon endroit pour valider le token Bearer et insérer l’utilisateur authentifié dans les extensions de la requête. Les handlers utilisent ensuite un extracteur typé.&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;rust&quot;&gt;#[derive(Clone, Debug)]
pub struct AuthUser {
    pub id: Uuid,
    pub email: String,
}

async fn auth_middleware(
    State(state): State&amp;lt;AppState&amp;gt;,
    mut req: Request,
    next: Next,
) -&amp;gt; Result&amp;lt;Response, AuthError&amp;gt; {
    let token = extract_bearer_token(req.headers()).ok_or(AuthError::MissingToken)?;
    let claims = decode_access_token(token, &amp;amp;state)?;

    let user_id = Uuid::parse_str(&amp;amp;claims.sub).map_err(|_| AuthError::InvalidToken)?;

    req.extensions_mut().insert(AuthUser {
        id: user_id,
        email: claims.email,
    });

    Ok(next.run(req).await)
}

impl&amp;lt;S&amp;gt; FromRequestParts&amp;lt;S&amp;gt; for AuthUser
where
    S: Send + Sync,
{
    type Rejection = AuthError;

    async fn from_request_parts(
        parts: &amp;amp;mut Parts,
        _state: &amp;amp;S,
    ) -&amp;gt; Result&amp;lt;Self, Self::Rejection&amp;gt; {
        parts
            .extensions
            .get::&amp;lt;AuthUser&amp;gt;()
            .cloned()
            .ok_or(AuthError::NotAuthenticated)
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Les handlers restent propres :&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;rust&quot;&gt;async fn me(AuthUser { id, email }: AuthUser) -&amp;gt; Json&amp;lt;serde_json::Value&amp;gt; {
    Json(json!({
        &amp;quot;id&amp;quot;: id.to_string(),
        &amp;quot;email&amp;quot;: email
    }))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;L’extracteur ne redécode pas le token. Il lit seulement la valeur insérée par le middleware, donc l’authentification a lieu une fois par requête.&lt;/p&gt;
&lt;h2 id=&quot;logout&quot;&gt;Logout&lt;/h2&gt;
&lt;p&gt;Le logout suit le même flow de hash et suppression. Il supprime la ligne du refresh token en Postgres et retire le cookie.&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;rust&quot;&gt;async fn logout(
    State(state): State&amp;lt;AppState&amp;gt;,
    mut jar: CookieJar,
) -&amp;gt; Result&amp;lt;(CookieJar, StatusCode), AuthError&amp;gt; {
    if let Some(cookie) = jar.get(&amp;quot;refresh_token&amp;quot;) {
        let token_hash = hash_refresh_token(cookie.value());

        sqlx::query(&amp;quot;DELETE FROM refresh_tokens WHERE token_hash = $1&amp;quot;)
            .bind(&amp;amp;token_hash)
            .execute(&amp;amp;state.db)
            .await?;
    }

    jar.remove(Cookie::from(&amp;quot;refresh_token&amp;quot;));

    Ok((jar, StatusCode::NO_CONTENT))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Le JWT d’accès reste valide jusqu’à ses 15 minutes d’expiration. Pour la plupart des API, c’est un tradeoff acceptable. Si tu as besoin d’une révocation immédiate des tokens d’accès, ajoute une petite denylist ou mets le token d’accès en base, mais ne construis ça que si un vrai besoin produit l’exige.&lt;/p&gt;
&lt;h2 id=&quot;cablage-du-router&quot;&gt;Câblage du router&lt;/h2&gt;
&lt;pre&gt;&lt;code data-lang=&quot;rust&quot;&gt;let protected_routes = Router::new()
    .route(&amp;quot;/me&amp;quot;, get(me))
    .route_layer(axum::middleware::from_fn_with_state(
        state.clone(),
        auth_middleware,
    ));

let app = Router::new()
    .route(&amp;quot;/login&amp;quot;, post(login))
    .route(&amp;quot;/refresh&amp;quot;, post(refresh))
    .route(&amp;quot;/logout&amp;quot;, post(logout))
    .merge(protected_routes)
    .with_state(state);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Si ce découpage en modules te semble à l’étroit, la même organisation scale quand tu déplaces handlers, middleware et code de tokens dans des modules séparés. Voir &lt;a href=&quot;/blog/structure-rust-web-project/&quot;&gt;comment structurer un projet web Rust&lt;/a&gt; pour les frontières de fichiers.&lt;/p&gt;
&lt;h2 id=&quot;checklist-prod&quot;&gt;Checklist prod&lt;/h2&gt;
&lt;p&gt;Avant de shipper :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;/&gt;
Les hashs de mots de passe utilisent argon2 avec un vrai coût mémoire.&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;/&gt;
Les tokens d’accès expirent en 15 minutes max.&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;/&gt;
Les refresh tokens sont aléatoires, hashés en Postgres, avec rotation au refresh.&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;/&gt;
Les refresh tokens utilisent des cookies &lt;code&gt;httpOnly&lt;/code&gt;, &lt;code&gt;Secure&lt;/code&gt; et &lt;code&gt;SameSite&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;/&gt;
La SPA n’écrit jamais de tokens dans &lt;code&gt;localStorage&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;/&gt;
Le logout révoque ou supprime le refresh token.&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;/&gt;
Rate limite &lt;code&gt;/login&lt;/code&gt; et &lt;code&gt;/refresh&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;/&gt;
Ajoute des index sur &lt;code&gt;refresh_tokens.token_hash&lt;/code&gt; et &lt;code&gt;users.email&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&quot;&quot; type=&quot;checkbox&quot;/&gt;
Utilise TLS pour PostgreSQL en production.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Une implémentation JWT avec Axum ne vaut que par sa gestion des refresh tokens. Le JWT d’accès est facile. La rotation du refresh, son stockage hashé et la frontière du cookie &lt;code&gt;httpOnly&lt;/code&gt; font qu’un flow de login survit à un vrai usage abusif.&lt;/p&gt;
&lt;div class=&quot;cta-box&quot;&gt;
&lt;p&gt;&lt;strong&gt;Tu veux le starter complet ?&lt;/strong&gt; Récupère mon template Axum gratuit (structure de projet + Dockerfile + CI + migrations) dans la &lt;a href=&quot;/newsletter/&quot;&gt;newsletter&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;
</content>
        
    </entry>
    <entry xml:lang="fr">
        <title>Héberger un microservice Rust sur Hetzner pour 6 €/mois (Docker + CI)</title>
        <published>2026-03-08T00:00:00+00:00</published>
        <updated>2026-03-08T00:00:00+00:00</updated>
        
        <author>
          <name>Rust Backend Lab</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://rust-backend-lab.pages.dev/blog/host-rust-hetzner/"/>
        <id>https://rust-backend-lab.pages.dev/blog/host-rust-hetzner/</id>
        
        <content type="html" xml:base="https://rust-backend-lab.pages.dev/blog/host-rust-hetzner/">&lt;p&gt;Pas besoin de Kubernetes à 80 €/mois pour ta première API Rust. Je fais tourner des petits services Axum sur un &lt;strong&gt;CX22 Hetzner à 6 €&lt;/strong&gt; avec Docker et Caddy : TLS, déploiements auto, ~5k rps de marge.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Transparence : les liens ci-dessous sont affiliés. Tu paies pareil, je touche une petite commission qui finance les articles gratuits. J’utilise exactement ce setup moi-même. Alternative : &lt;a rel=&quot;noopener external&quot; target=&quot;_blank&quot; href=&quot;https://m.do.co/c/YOURCODE&quot;&gt;DigitalOcean&lt;/a&gt; si tu préfères leur DX.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;pourquoi-hetzner&quot;&gt;Pourquoi Hetzner ?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;CX22 : 2 vCPU / 4 Go / 40 Go pour ~6 €/mois (Falkenstein/Nuremberg)&lt;/li&gt;
&lt;li&gt;NVMe rapides et pas chers, bon réseau vers l’UE et les US&lt;/li&gt;
&lt;li&gt;Tu échanges la magie managée contre du contrôle. Parfait pour des petits binaires Rust.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Besoin de US uniquement ou de Postgres managé ? Prends DigitalOcean ou Fly.io. Besoin de pas cher et rapide en Europe ? Hetzner gagne.&lt;/p&gt;
&lt;p&gt;👉 &lt;a rel=&quot;noopener external&quot; target=&quot;_blank&quot; href=&quot;https://www.hetzner.com/?aff=YOURCODE&quot;&gt;20 € de crédit Hetzner ici&lt;/a&gt; &lt;em&gt;(remplace YOURCODE dans &lt;code&gt;config.toml&lt;/code&gt;)&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&quot;1-dockerfile-multi-stage-minuscule&quot;&gt;1. Dockerfile (multi-stage, minuscule)&lt;/h2&gt;
&lt;pre&gt;&lt;code data-lang=&quot;dockerfile&quot;&gt;FROM rust:1.82-slim AS build
WORKDIR /app
COPY Cargo.toml Cargo.lock ./
COPY src ./src
COPY migrations ./migrations
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y pkg-config libssl-dev \
 &amp;amp;&amp;amp; cargo build --release

FROM debian:bookworm-slim
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y ca-certificates libssl3 \
 &amp;amp;&amp;amp; rm -rf /var/lib/apt/lists/*
COPY --from=build /app/target/release/rust-tasks-api /usr/local/bin/api
COPY --from=build /app/migrations ./migrations
ENV RUST_LOG=info ADDR=0.0.0.0:8080
EXPOSE 8080
CMD [&amp;quot;/usr/local/bin/api&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Image de ~30 à 50 Mo. Les binaires Rust quasi statiques adorent les images slim.&lt;/p&gt;
&lt;h2 id=&quot;2-compose-et-caddy-tls-en-5-min&quot;&gt;2. Compose et Caddy (TLS en 5 min)&lt;/h2&gt;
&lt;pre&gt;&lt;code data-lang=&quot;yaml&quot;&gt;# compose.yaml
services:
  api:
    image: ghcr.io/you/rust-tasks-api:latest
    env_file: .env
    restart: unless-stopped
  caddy:
    image: caddy:2-alpine
    ports: [&amp;quot;80:80&amp;quot;, &amp;quot;443:443&amp;quot;]
    volumes: [&amp;quot;./Caddyfile:/etc/caddy/Caddyfile&amp;quot;, &amp;quot;caddy_data:/data&amp;quot;]
    restart: unless-stopped
volumes: { caddy_data: {} }
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# Caddyfile
api.yourdomain.com {
    reverse_proxy api:8080
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Caddy gère Let’s Encrypt tout seul. Pas de danse certbot avec nginx.&lt;/p&gt;
&lt;h2 id=&quot;3-deploiement-ci-github-actions&quot;&gt;3. Déploiement CI (GitHub Actions)&lt;/h2&gt;
&lt;pre&gt;&lt;code data-lang=&quot;yaml&quot;&gt;name: deploy
on: { push: { branches: [main] } }
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: YOUR_GITHUB_USERNAME
          password: GHCR_TOKEN_AS_SECRET
      - uses: docker/build-push-action@v6
        with: { push: true, tags: ghcr.io/YOU/rust-tasks-api:latest }
      - name: pull and restart on VPS
        uses: appleboy/ssh-action@v1
        with:
          host: HETZNER_HOST_AS_SECRET
          username: root
          key: HETZNER_SSH_KEY_AS_SECRET
          script: |
            cd /opt/api &amp;amp;&amp;amp; docker compose pull &amp;amp;&amp;amp; docker compose up -d
            docker image prune -f
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Push sur main, en ligne en ~2 min. Assez zéro-downtime pour une v1 (tu ajouteras du blue/green plus tard).&lt;/p&gt;
&lt;h2 id=&quot;4-postgres-ou&quot;&gt;4. Postgres : où ?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Démarre sur le même VPS avec Docker (&lt;code&gt;postgres:16-alpine&lt;/code&gt;, volume monté). OK jusqu’à ~10k utilisateurs.&lt;/li&gt;
&lt;li&gt;Pour grandir, prends le Postgres managé Hetzner ou un CX22 séparé pour la BDD. Règle &lt;code&gt;max_connections&lt;/code&gt; selon ton pool (12 à 20 par instance d’API).&lt;/li&gt;
&lt;li&gt;Backups : &lt;code&gt;pg_dump&lt;/code&gt; quotidien vers une Storage Box Hetzner et &lt;code&gt;pgbackrest&lt;/code&gt; hebdo. Teste les restaurations !&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;detail-des-couts&quot;&gt;Détail des coûts&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;VPS CX22 : ~6 €/mois&lt;/li&gt;
&lt;li&gt;Domaine : ~12 €/an&lt;/li&gt;
&lt;li&gt;Backups (Storage Box 100 Go) : ~1 €/mois&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Total : ~8 €/mois&lt;/strong&gt; pour une vraie API TLS qui encaisse des milliers de rps&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Compare : K8s managé + LB + BDD managée, 60 à 150 €/mois pour le même trafic.&lt;/p&gt;
&lt;h2 id=&quot;checklist-de-durcissement&quot;&gt;Checklist de durcissement&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;UFW : autorise 22/80/443 uniquement, clés SSH, fail2ban&lt;/li&gt;
&lt;li&gt;Caddy vers l’API sur réseau Docker privé, pas d’exposition directe du 8080&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RUST_LOG=info&lt;/code&gt;, logs JSON, &lt;code&gt;/health&lt;/code&gt; et Uptime Kuma&lt;/li&gt;
&lt;li&gt;Surveille le disque (les logs Docker !) : rotation &lt;code&gt;max-size: &quot;10m&quot;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;Ensuite : ajoute &lt;code&gt;/metrics&lt;/code&gt; Prometheus avec des alertes, puis lis le &lt;a href=&quot;/blog/reduce-p99-latency-rust/&quot;&gt;guide latence p99&lt;/a&gt; avant de scaler.&lt;/p&gt;
&lt;/blockquote&gt;
</content>
        
    </entry>
    <entry xml:lang="fr">
        <title>Comment j&#39;ai réduit la latence p99 de 60 % sur un service Rust</title>
        <published>2026-03-01T00:00:00+00:00</published>
        <updated>2026-03-01T00:00:00+00:00</updated>
        
        <author>
          <name>Rust Backend Lab</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://rust-backend-lab.pages.dev/blog/reduce-p99-latency-rust/"/>
        <id>https://rust-backend-lab.pages.dev/blog/reduce-p99-latency-rust/</id>
        
        <content type="html" xml:base="https://rust-backend-lab.pages.dev/blog/reduce-p99-latency-rust/">&lt;p&gt;Un service Rust à &lt;strong&gt;220 ms de p99&lt;/strong&gt; qui aurait dû être rapide. Après une semaine : &lt;strong&gt;85 ms de p99 (moins 60 %)&lt;/strong&gt;, même hardware, même Postgres. Voici exactement ce qui a compté, dans l’ordre.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Stack : Axum 0.7, Tokio, SQLx, Postgres 16, un seul VPS Hetzner à 12 € + Postgres managé. Charge : ~2k rps mixtes lecture/écriture. Mesuré avec &lt;code&gt;oha&lt;/code&gt; et des histogrammes Prometheus (pas des moyennes !).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;0-mesure-la-p99-pas-la-moyenne&quot;&gt;0. Mesure la p99, pas la moyenne&lt;/h2&gt;
&lt;p&gt;La moyenne cache les queues de distribution. J’ai ajouté :&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;rust&quot;&gt;// histogram, not counter
metrics::histogram!(&amp;quot;http_request_duration_seconds&amp;quot;, latency);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Et j’ai dashboardé p50, p95 et p99 séparément. La p50 était déjà à 12 ms. Correct. La p99 était le problème. Des correctifs différents ciblent des percentiles différents.&lt;/p&gt;
&lt;h2 id=&quot;correctif-1-pool-et-index-manquant-moins-35-de-p99&quot;&gt;Correctif 1 : pool et index manquant (moins 35 % de p99)&lt;/h2&gt;
&lt;p&gt;Deux classiques empilés :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pool à &lt;code&gt;max_connections(50)&lt;/code&gt; face à un petit Postgres : contention et file d’attente. Descendu à &lt;strong&gt;12&lt;/strong&gt;, avec &lt;code&gt;acquire_timeout(3s)&lt;/code&gt;. Le temps de file a disparu.&lt;/li&gt;
&lt;li&gt;Un endpoint faisait &lt;code&gt;WHERE user_id = $1 ORDER BY created_at DESC&lt;/code&gt; sans index composite. &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; montrait un seq scan sous concurrence.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code data-lang=&quot;sql&quot;&gt;CREATE INDEX CONCURRENTLY idx_tasks_user_created
  ON tasks (user_id, created_at DESC);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Résultat : p99 de 220 à 145 ms. Ennuyeux et énorme. Vérifie toujours la BDD en premier. Rust est rarement le goulot.&lt;/p&gt;
&lt;h2 id=&quot;correctif-2-requetes-n-1-moins-15-de-p99&quot;&gt;Correctif 2 : requêtes N+1 (moins 15 % de p99)&lt;/h2&gt;
&lt;p&gt;Une route chargeait les tâches puis bouclait un &lt;code&gt;SELECT&lt;/code&gt; par assigné. 1 + 47 requêtes par appel à p99.&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;sql&quot;&gt;-- avant : N+1 dans une boucle Rust
-- après : un seul JOIN
SELECT t.id, t.title, u.name AS assignee
FROM tasks t LEFT JOIN users u ON u.id = t.assignee_id
WHERE t.project_id = $1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Avec &lt;code&gt;query_as!&lt;/code&gt; de SQLx, un seul aller-retour. p99 de 145 à 122 ms. &lt;code&gt;pg_stat_statements&lt;/code&gt; et les spans de tracing par requête ont rendu ça évident.&lt;/p&gt;
&lt;h2 id=&quot;correctif-3-allocs-json-sur-le-chemin-chaud-moins-10-de-p99&quot;&gt;Correctif 3 : allocs JSON sur le chemin chaud (moins 10 % de p99)&lt;/h2&gt;
&lt;p&gt;Le profiling avec &lt;code&gt;flamegraph&lt;/code&gt; montrait 18 % du temps dans le churn de &lt;code&gt;serde_json::Value&lt;/code&gt; : parse, &lt;code&gt;Value&lt;/code&gt;, resérialisation. Correctif : désérialiser direct dans des structs, utiliser &lt;code&gt;bytes::Bytes&lt;/code&gt; pour le passthrough, activer &lt;code&gt;CompressionLayer&lt;/code&gt; seulement pour les gros bodies.&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;rust&quot;&gt;// mal : Json&amp;lt;Value&amp;gt; puis bidouille
// bien : DTO typé, borrow quand possible
#[derive(Deserialize)]
pub struct Ingest&amp;lt;&amp;#39;a&amp;gt; { #[serde(borrow)] pub title: &amp;amp;&amp;#39;a str }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;p99 de 122 à 108 ms. Petit mais gratuit.&lt;/p&gt;
&lt;h2 id=&quot;correctif-4-cout-du-tracing-et-des-logs-moins-8-de-p99&quot;&gt;Correctif 4 : coût du tracing et des logs (moins 8 % de p99)&lt;/h2&gt;
&lt;p&gt;Niveau &lt;code&gt;TRACE&lt;/code&gt; avec des jolis logs en prod et des bodies complets loggués. Passé à :&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;rust&quot;&gt;tracing_subscriber::fmt().json()
    .with_max_level(tracing::Level::INFO)
    .with_env_filter(&amp;quot;myapp=info,tower_http=warn,sqlx=warn&amp;quot;)
    .init();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Spans de debug échantillonnés, &lt;code&gt;info!&lt;/code&gt; par ligne supprimés. p99 de 108 à 95 ms. L’observabilité ne devrait pas coûter 10 %.&lt;/p&gt;
&lt;h2 id=&quot;correctif-5-timeouts-et-degradation-gracieuse-le-reste&quot;&gt;Correctif 5 : timeouts et dégradation gracieuse (le reste)&lt;/h2&gt;
&lt;p&gt;Sans timeouts, un seul downstream lent empoisonne tous les workers. Ajouté :&lt;/p&gt;
&lt;pre&gt;&lt;code data-lang=&quot;rust&quot;&gt;use tower_http::timeout::TimeoutLayer;
use std::time::Duration;
// par route : appels externes 2s, acquisition BDD 3s
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Plus &lt;code&gt;tokio::time::timeout&lt;/code&gt; sur les appels &lt;code&gt;reqwest&lt;/code&gt; sortants avec fallback sur le cache. p99 de 95 à 85 ms, et la p99.9 a arrêté de monter à plusieurs secondes.&lt;/p&gt;
&lt;h2 id=&quot;checklist-a-voler&quot;&gt;Checklist (à voler)&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Dashboarde p50/p95/p99 séparément&lt;/li&gt;
&lt;li&gt;Pool à 10-20, &lt;code&gt;acquire_timeout&lt;/code&gt;, surveille la profondeur de file&lt;/li&gt;
&lt;li&gt;&lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; sur les 5 top requêtes, ajoute des index composites&lt;/li&gt;
&lt;li&gt;Tue les N+1 (join ou &lt;code&gt;WHERE id = ANY($1)&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;JSON typé, évite &lt;code&gt;Value&lt;/code&gt; sur les chemins chauds&lt;/li&gt;
&lt;li&gt;Logs JSON en INFO, fais taire le bruit &lt;code&gt;sqlx&lt;/code&gt; et &lt;code&gt;tower_http&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Timeouts partout et compression sélective&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--release&lt;/code&gt; + &lt;code&gt;lto=&quot;thin&quot;&lt;/code&gt; + &lt;code&gt;codegen-units=1&lt;/code&gt; pour les builds prod&lt;/li&gt;
&lt;li&gt;Teste la charge avec &lt;code&gt;oha -c 100 -z 60s&lt;/code&gt;, pas 5s&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Total : 220 à 85 ms de p99. Pas de réécriture, pas de nouvelle infra, juste de la mesure et des correctifs ennuyeux.&lt;/p&gt;
&lt;div class=&quot;cta-box&quot;&gt;
&lt;p&gt;&lt;strong&gt;Tu veux la checklist complète et le JSON du dashboard Grafana ?&lt;/strong&gt; Gratuit dans la &lt;a href=&quot;/newsletter/&quot;&gt;newsletter&lt;/a&gt;. Je fais aussi des &lt;a href=&quot;/hire-me/&quot;&gt;sprints perf d’une semaine&lt;/a&gt;, benchmarks avant/après inclus.&lt;/p&gt;
&lt;/div&gt;
</content>
        
    </entry>
    <entry xml:lang="fr">
        <title>Les meilleures crates Rust pour une API en 2026</title>
        <published>2026-02-22T00:00:00+00:00</published>
        <updated>2026-02-22T00:00:00+00:00</updated>
        
        <author>
          <name>Rust Backend Lab</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://rust-backend-lab.pages.dev/blog/best-rust-crates-api-2026/"/>
        <id>https://rust-backend-lab.pages.dev/blog/best-rust-crates-api-2026/</id>
        
        <content type="html" xml:base="https://rust-backend-lab.pages.dev/blog/best-rust-crates-api-2026/">&lt;p&gt;Ne perds pas une semaine à évaluer 40 crates. Voici &lt;strong&gt;la stack backend 2026 tranchée&lt;/strong&gt; que j’utilise pour chaque API Axum.&lt;/p&gt;
&lt;h2 id=&quot;coeur&quot;&gt;Cœur&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;axum 0.7&lt;/code&gt;&lt;/strong&gt; : router. Extracteurs et middlewares Tower. À associer avec &lt;code&gt;tokio 1.x&lt;/code&gt; full.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;tokio&lt;/code&gt;&lt;/strong&gt; : runtime. &lt;code&gt;rt-multi-thread&lt;/code&gt;, &lt;code&gt;macros&lt;/code&gt;, &lt;code&gt;signal&lt;/code&gt; (graceful shutdown !), &lt;code&gt;time&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;serde&lt;/code&gt; + &lt;code&gt;serde_json&lt;/code&gt;&lt;/strong&gt; : DTOs. &lt;code&gt;#[serde(rename_all=&quot;camelCase&quot;)]&lt;/code&gt; à la frontière API, &lt;code&gt;snake_case&lt;/code&gt; à l’intérieur.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;donnees&quot;&gt;Données&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;sqlx 0.8&lt;/code&gt;&lt;/strong&gt; (&lt;code&gt;runtime-tokio&lt;/code&gt;, &lt;code&gt;postgres&lt;/code&gt;, &lt;code&gt;macros&lt;/code&gt;, &lt;code&gt;migrate&lt;/code&gt;) : Postgres async avec requêtes vérifiées à la compilation. &lt;code&gt;query!&lt;/code&gt; attrape les fautes SQL au build.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;redis 0.26&lt;/code&gt; / &lt;code&gt;deadpool-redis&lt;/code&gt;&lt;/strong&gt; : cache, rate limit et sessions. Pas de cache dans un HashMap partagé entre instances.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;refinery&lt;/code&gt; ou &lt;code&gt;sqlx::migrate!&lt;/code&gt;&lt;/strong&gt; : migrations versionnées, jouées au boot. Jamais de &lt;code&gt;CREATE TABLE&lt;/code&gt; à la main en prod.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;plomberie-http&quot;&gt;Plomberie HTTP&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;tower-http&lt;/code&gt;&lt;/strong&gt; : &lt;code&gt;TraceLayer&lt;/code&gt;, &lt;code&gt;CorsLayer&lt;/code&gt;, &lt;code&gt;CompressionLayer&lt;/code&gt;, &lt;code&gt;RequestIdLayer&lt;/code&gt;, &lt;code&gt;TimeoutLayer&lt;/code&gt;. Il te faut les quatre en prod.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;utoipa&lt;/code&gt; + &lt;code&gt;utoipa-swagger-ui&lt;/code&gt;&lt;/strong&gt; : OpenAPI généré depuis le code. Tes équipes front vont t’adorer.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;validator&lt;/code&gt; (derive)&lt;/strong&gt; : &lt;code&gt;#[validate(length, email, range)]&lt;/code&gt; sur les DTOs d’entrée. Rejette les données pourries à la frontière.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;observabilite&quot;&gt;Observabilité&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;tracing&lt;/code&gt; + &lt;code&gt;tracing-subscriber&lt;/code&gt;&lt;/strong&gt; (&lt;code&gt;env-filter&lt;/code&gt;, &lt;code&gt;json&lt;/code&gt;) : logs structurés. JSON en prod, joli en dev.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;opentelemetry&lt;/code&gt; + &lt;code&gt;tracing-opentelemetry&lt;/code&gt;&lt;/strong&gt; : traces vers Jaeger ou Tempo quand les logs ne suffisent plus.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;metrics&lt;/code&gt; + &lt;code&gt;metrics-exporter-prometheus&lt;/code&gt;&lt;/strong&gt; : &lt;code&gt;/metrics&lt;/code&gt; pour le RPS, les histogrammes de latence et les stats du pool.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;robustesse&quot;&gt;Robustesse&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;thiserror&lt;/code&gt; + &lt;code&gt;anyhow&lt;/code&gt;&lt;/strong&gt; : &lt;code&gt;thiserror&lt;/code&gt; pour les erreurs d’API, &lt;code&gt;anyhow&lt;/code&gt; pour les binaires et le démarrage. Un seul &lt;code&gt;AppError&lt;/code&gt; mappé vers HTTP.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;backon&lt;/code&gt; ou &lt;code&gt;tokio-retry&lt;/code&gt;&lt;/strong&gt; : backoff exponentiel pour les appels aval. Pas de boucles de retry à la main.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;reqwest&lt;/code&gt;&lt;/strong&gt; (&lt;code&gt;rustls-tls&lt;/code&gt;, &lt;code&gt;json&lt;/code&gt;) : HTTP sortant. Mets des timeouts ! Par défaut c’est infini, et c’est un tueur en prod.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;mon-extrait-cargo-par-defaut&quot;&gt;Mon extrait Cargo par défaut&lt;/h2&gt;
&lt;pre&gt;&lt;code data-lang=&quot;toml&quot;&gt;axum = &amp;quot;0.7&amp;quot;
tokio = { version = &amp;quot;1&amp;quot;, features = [&amp;quot;full&amp;quot;] }
serde = { version = &amp;quot;1&amp;quot;, features = [&amp;quot;derive&amp;quot;] }
serde_json = &amp;quot;1&amp;quot;
sqlx = { version = &amp;quot;0.8&amp;quot;, features = [&amp;quot;runtime-tokio&amp;quot;, &amp;quot;postgres&amp;quot;, &amp;quot;macros&amp;quot;, &amp;quot;migrate&amp;quot;, &amp;quot;uuid&amp;quot;, &amp;quot;chrono&amp;quot;] }
tower-http = { version = &amp;quot;0.5&amp;quot;, features = [&amp;quot;full&amp;quot;] }
tracing = &amp;quot;0.1&amp;quot;
tracing-subscriber = { version = &amp;quot;0.3&amp;quot;, features = [&amp;quot;env-filter&amp;quot;, &amp;quot;json&amp;quot;] }
thiserror = &amp;quot;2&amp;quot;
anyhow = &amp;quot;1&amp;quot;
validator = { version = &amp;quot;0.18&amp;quot;, features = [&amp;quot;derive&amp;quot;] }
utoipa = { version = &amp;quot;5&amp;quot;, features = [&amp;quot;axum_extras&amp;quot;] }
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;ce-que-j-evite-pour-des-api&quot;&gt;Ce que j’évite (pour des API)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Les gros ORM magiques : les requêtes SQLx explicites scalent mieux pour du backend&lt;/li&gt;
&lt;li&gt;&lt;code&gt;async-std&lt;/code&gt; pour des serveurs en 2026 : Tokio a gagné, l’écosystème suit&lt;/li&gt;
&lt;li&gt;Rouler ta propre auth JWT : prends &lt;code&gt;jsonwebtoken&lt;/code&gt; + &lt;code&gt;axum-login&lt;/code&gt; ou une auth externalisée (Clerk/Auth0) pour démarrer&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;Tu veux tout ça déjà câblé ? Le &lt;a href=&quot;/newsletter/&quot;&gt;template gratuit de la newsletter&lt;/a&gt; l’a préconfiguré, avec Dockerfile et CI.&lt;/p&gt;
&lt;/blockquote&gt;
</content>
        
    </entry>
    <entry xml:lang="fr">
        <title>Rust vs Go pour les microservices en 2026 : les vrais tradeoffs</title>
        <published>2026-02-15T00:00:00+00:00</published>
        <updated>2026-02-15T00:00:00+00:00</updated>
        
        <author>
          <name>Rust Backend Lab</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://rust-backend-lab.pages.dev/blog/rust-vs-go-microservices/"/>
        <id>https://rust-backend-lab.pages.dev/blog/rust-vs-go-microservices/</id>
        
        <content type="html" xml:base="https://rust-backend-lab.pages.dev/blog/rust-vs-go-microservices/">&lt;p&gt;Rust vs Go, c’est une guerre de religion. Voici la version pragmatique de quelqu’un qui ship les deux : &lt;strong&gt;Go pour la vitesse de dev CRUD, Rust pour la latence de queue, le coût et la fiabilité à l’échelle.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&quot;en-deux-mots&quot;&gt;En deux mots&lt;/h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;Rust (Axum)&lt;/th&gt;&lt;th&gt;Go (Chi/Gin)&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Latence p50 (JSON+BDD simple)&lt;/td&gt;&lt;td&gt;~1 à 3 ms&lt;/td&gt;&lt;td&gt;~2 à 5 ms&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;p99 sous charge&lt;/td&gt;&lt;td&gt;Excellente, stable&lt;/td&gt;&lt;td&gt;Bonne, pauses GC visibles&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Mémoire par instance&lt;/td&gt;&lt;td&gt;10 à 30 Mo&lt;/td&gt;&lt;td&gt;30 à 80 Mo&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Binaire&lt;/td&gt;&lt;td&gt;Unique, ~5 à 15 Mo&lt;/td&gt;&lt;td&gt;Unique, ~10 à 20 Mo&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Vitesse de dev (CRUD)&lt;/td&gt;&lt;td&gt;Plus lente (taxe borrow checker)&lt;/td&gt;&lt;td&gt;Plus rapide&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Confiance au refactor&lt;/td&gt;&lt;td&gt;Très haute&lt;/td&gt;&lt;td&gt;Moyenne (les tests te portent)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Recrutement&lt;/td&gt;&lt;td&gt;Plus dur, plutôt senior&lt;/td&gt;&lt;td&gt;Plus facile&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Écosystème (HTTP/ORM)&lt;/td&gt;&lt;td&gt;Mûrit vite (Axum, SQLx)&lt;/td&gt;&lt;td&gt;Très mature&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Ces chiffres sont des ordres de grandeur sur un VPS Hetzner à 6 €, Postgres à côté, &lt;code&gt;--release&lt;/code&gt;, connexions poolées. Ta charge sera différente. Benchmarke.&lt;/p&gt;
&lt;h2 id=&quot;ou-rust-gagne&quot;&gt;Où Rust gagne&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;1. Latence de queue.&lt;/strong&gt; Pas de pauses GC. À 5k à 20k rps avec JSON + BDD, la p99 Rust reste plate pendant que la p99 Go tremble (surtout avec de gros heaps). Si ton SLO c’est p99 &amp;lt; 50ms, Rust te donne de la marge.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Coût.&lt;/strong&gt; 2x moins de mémoire + plus de rps par cœur = moins d’instances. Sur Hetzner ou Fly, passer de 4 à 2 instances paie vite le temps de dev en plus.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Fiabilité.&lt;/strong&gt; &lt;code&gt;Result&lt;/code&gt;, &lt;code&gt;match&lt;/code&gt; exhaustifs, vérification SQL à la compilation avec &lt;code&gt;sqlx::query!&lt;/code&gt;. Des classes entières de bugs null-pointer et d’erreurs non gérées disparaissent. Refactorer 30k lignes fait se sentir en sécurité.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. Un seul langage pour les chemins chauds.&lt;/strong&gt; Besoin de JSON SIMD, de parsing custom ou de logique embarquée plus tard ? Tu es déjà en Rust.&lt;/p&gt;
&lt;h2 id=&quot;ou-go-gagne&quot;&gt;Où Go gagne&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;1. Vitesse.&lt;/strong&gt; &lt;code&gt;net/http&lt;/code&gt; + &lt;code&gt;sqlc&lt;/code&gt;/&lt;code&gt;pgx&lt;/code&gt; + &lt;code&gt;golang-migrate&lt;/code&gt; : du CRUD shippé en quelques heures. Rust prend 1,5 à 2x plus longtemps pour le même CRUD ennuyeux, surtout avec des juniors.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Recrutement.&lt;/strong&gt; 10x plus de devs backend Go dispos. Le recrutement Rust est lent et senior.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Écosystème ennuyeux (en bien).&lt;/strong&gt; Chaque middleware, chaque SDK cloud, chaque réponse StackOverflow existe. Avec Rust, il faut parfois coller des bouts.&lt;/p&gt;
&lt;h2 id=&quot;ma-regle&quot;&gt;Ma règle&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CRUD interne, 3 devs, à livrer le mois prochain ?&lt;/strong&gt; Go.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;API publique, SLO p99, gros rps par cœur, sensible au coût ?&lt;/strong&gt; Rust + Axum.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Migration ?&lt;/strong&gt; Ne réécris pas tout. Extrais d’abord le service chaud (passerelle d’auth, bidding, ingestion). Pattern strangler. Voir mon &lt;a href=&quot;/hire-me/&quot;&gt;freelance&lt;/a&gt; si tu veux un plan de migration.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;une-tactique-de-migration-qui-marche&quot;&gt;Une tactique de migration qui marche&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Mets le service Rust derrière le même Postgres / NATS / Kafka&lt;/li&gt;
&lt;li&gt;Bascule 10 % du trafic en shadow, compare p50/p99 et taux d’erreur&lt;/li&gt;
&lt;li&gt;Bascule route par route, garde Go en fallback pendant 2 semaines&lt;/li&gt;
&lt;li&gt;Mesure le coût : instances, mémoire, violations p99 avant et après&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Les équipes que j’accompagne voient typiquement &lt;strong&gt;30 à 60 % de p99 en moins et 30 à 50 % de mémoire en moins&lt;/strong&gt; en passant les chemins chauds de Go à Axum. Sur du CRUD ennuyeux, le gain est nul. Ne t’embête pas.&lt;/p&gt;
&lt;h2 id=&quot;verdict&quot;&gt;Verdict&lt;/h2&gt;
&lt;p&gt;Go optimise le &lt;strong&gt;débit de l’équipe&lt;/strong&gt;. Rust optimise &lt;strong&gt;l’efficacité machine et la fiabilité&lt;/strong&gt;. Choisis selon ta contrainte : time-to-market ou coût et latence à l’échelle.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Tu construis le côté Rust ? Commence par le &lt;a href=&quot;/blog/axum-postgres-rest-api/&quot;&gt;guide Axum + Postgres&lt;/a&gt; et la &lt;a href=&quot;/blog/structure-rust-web-project/&quot;&gt;structure de projet&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
</content>
        
    </entry>
    <entry xml:lang="fr">
        <title>Structurer un projet web Rust (une archi Axum propre qui scale)</title>
        <published>2026-02-08T00:00:00+00:00</published>
        <updated>2026-02-08T00:00:00+00:00</updated>
        
        <author>
          <name>Rust Backend Lab</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://rust-backend-lab.pages.dev/blog/structure-rust-web-project/"/>
        <id>https://rust-backend-lab.pages.dev/blog/structure-rust-web-project/</id>
        
        <content type="html" xml:base="https://rust-backend-lab.pages.dev/blog/structure-rust-web-project/">&lt;p&gt;La plupart des tutos Axum s’arrêtent à 3 routes dans &lt;code&gt;main.rs&lt;/code&gt;. Un vrai service a plus de 30 routes, des jobs en tâche de fond et 3 devs. Voici la structure que j’utilise en production.&lt;/p&gt;
&lt;h2 id=&quot;le-probleme-du-main-rs-fourre-tout&quot;&gt;Le problème du main.rs fourre-tout&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Conflits de merge à chaque PR&lt;/li&gt;
&lt;li&gt;Aucune place définie pour la logique métier face à la glue HTTP&lt;/li&gt;
&lt;li&gt;Impossible de tester sans lancer HTTP&lt;/li&gt;
&lt;li&gt;Erreurs gérées de 5 façons différentes&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;la-structure&quot;&gt;La structure&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;src/
  main.rs        # 60 lignes max : config, tracing, pool, router, serve
  config.rs      # env typée
  state.rs       # AppState { db, redis, http_client, config }
  error.rs       # AppError
  extractors.rs  # AuthUser, RequestId (extracteurs custom)
  routes/        # HTTP uniquement : parse l&amp;#39;entrée, appelle le service, mappe la sortie
    mod.rs
    tasks.rs
    users.rs
  services/      # logique métier : quasi pure, testable
    tasks.rs
  repos/         # SQL uniquement (optionnel tant que tu n&amp;#39;en as pas besoin)
    tasks.rs
  models/
  middleware/
  observability.rs
tests/
  api_tasks.rs   # black-box via axum-test ou reqwest
migrations/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Règle générale : &lt;code&gt;routes/&lt;/code&gt; connaît HTTP, &lt;code&gt;services/&lt;/code&gt; non. &lt;code&gt;repos/&lt;/code&gt; connaît SQL, &lt;code&gt;services/&lt;/code&gt; n’écrit pas de SQL brut si tu les sépares.&lt;/p&gt;
&lt;h2 id=&quot;main-rs-reste-bete&quot;&gt;main.rs reste bête&lt;/h2&gt;
&lt;pre&gt;&lt;code data-lang=&quot;rust&quot;&gt;#[tokio::main]
async fn main() -&amp;gt; anyhow::Result&amp;lt;()&amp;gt; {
    let cfg = config::load()?;
    observability::init(&amp;amp;cfg)?;
    let db = db::connect(&amp;amp;cfg.database_url).await?;
    let state = AppState::new(db, cfg.clone());
    let app = routes::router(state);
    let listener = tokio::net::TcpListener::bind(cfg.addr).await?;
    tracing::info!(&amp;quot;listening on {}&amp;quot;, cfg.addr);
    axum::serve(listener, app).await?;
    Ok(())
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Si ton &lt;code&gt;main.rs&lt;/code&gt; dépasse 100 lignes, tu mélanges les responsabilités.&lt;/p&gt;
&lt;h2 id=&quot;les-services-se-testent&quot;&gt;Les services se testent&lt;/h2&gt;
&lt;pre&gt;&lt;code data-lang=&quot;rust&quot;&gt;// services/tasks.rs
pub async fn create_task(db: &amp;amp;PgPool, title: &amp;amp;str) -&amp;gt; Result&amp;lt;Task, AppError&amp;gt; {
    if title.trim().is_empty() {
        return Err(AppError::BadRequest(&amp;quot;title is empty&amp;quot;.into()));
    }
    repos::tasks::insert(db, title).await.map_err(Into::into)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code data-lang=&quot;rust&quot;&gt;// routes/tasks.rs: glue fine
pub async fn create_task(
    State(s): State&amp;lt;AppState&amp;gt;,
    Json(input): Json&amp;lt;CreateTask&amp;gt;,
) -&amp;gt; Result&amp;lt;impl IntoResponse, AppError&amp;gt; {
    let task = services::tasks::create_task(&amp;amp;s.db, &amp;amp;input.title).await?;
    Ok((StatusCode::CREATED, Json(task)))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Maintenant tu peux tester &lt;code&gt;services::tasks::create_task&lt;/code&gt; avec juste une BDD, sans HTTP.&lt;/p&gt;
&lt;h2 id=&quot;erreurs-un-seul-enum&quot;&gt;Erreurs : un seul enum&lt;/h2&gt;
&lt;p&gt;Un seul &lt;code&gt;AppError&lt;/code&gt; pour toute l’API (voir le &lt;a href=&quot;/blog/axum-postgres-rest-api/&quot;&gt;guide Axum + Postgres&lt;/a&gt;). Mapping : &lt;code&gt;NotFound → 404&lt;/code&gt;, &lt;code&gt;BadRequest → 400&lt;/code&gt;, &lt;code&gt;Unauthorized → 401&lt;/code&gt;, &lt;code&gt;Db → 500&lt;/code&gt; (loggue tout, renvoie du générique).&lt;/p&gt;
&lt;p&gt;Ne crée pas &lt;code&gt;TaskError&lt;/code&gt;, &lt;code&gt;UserError&lt;/code&gt;, &lt;code&gt;AuthError&lt;/code&gt; sans vraie raison. Tu écrirais juste 3x des impls &lt;code&gt;IntoResponse&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&quot;config-typee-echec-au-demarrage&quot;&gt;Config : typée, échec au démarrage&lt;/h2&gt;
&lt;pre&gt;&lt;code data-lang=&quot;rust&quot;&gt;#[derive(Clone)]
pub struct Config {
    pub addr: SocketAddr,
    pub database_url: String,
}

pub fn load() -&amp;gt; anyhow::Result&amp;lt;Config&amp;gt; {
    dotenvy::dotenv().ok();
    Ok(Config {
        addr: std::env::var(&amp;quot;ADDR&amp;quot;).unwrap_or(&amp;quot;0.0.0.0:8080&amp;quot;.into()).parse()?,
        database_url: std::env::var(&amp;quot;DATABASE_URL&amp;quot;).context(&amp;quot;DATABASE_URL missing&amp;quot;)?,
    })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Échoue au boot, pas sur la première requête à 3h du matin.&lt;/p&gt;
&lt;h2 id=&quot;strategie-de-tests&quot;&gt;Stratégie de tests&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cargo test services::&lt;/code&gt;: tests unitaires rapides, avec &lt;code&gt;sqlx::test&lt;/code&gt; et un vrai Postgres en Docker&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tests/api_*.rs&lt;/code&gt;: aller-retour HTTP complet, une BDD de test par run, truncate entre les tests&lt;/li&gt;
&lt;li&gt;Ne mocke pas &lt;code&gt;PgPool&lt;/code&gt;. Utilise un vrai conteneur Postgres. Les mocks mentent sur le SQL.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;quand-passer-aux-workspaces&quot;&gt;Quand passer aux workspaces ?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;1 service, moins de 50 routes : une seule crate (ce guide)&lt;/li&gt;
&lt;li&gt;2 à 3 services qui partagent des modèles : workspace Cargo + &lt;code&gt;crates/common&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Microservices : un repo par déployable, partagés via des crates versionnées, pas des hacks de chemins&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Commence en monolithe modulaire. Extrais quand les cycles de déploiement divergent, pas avant.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Le template complet avec cette structure, le Dockerfile et la CI GitHub est dans ma &lt;a href=&quot;/newsletter/&quot;&gt;newsletter&lt;/a&gt; gratuite.&lt;/p&gt;
&lt;/blockquote&gt;
</content>
        
    </entry>
    <entry xml:lang="fr">
        <title>Construire une API REST performante en Rust avec Axum et Postgres</title>
        <published>2026-02-01T00:00:00+00:00</published>
        <updated>2026-02-01T00:00:00+00:00</updated>
        
        <author>
          <name>Rust Backend Lab</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://rust-backend-lab.pages.dev/blog/axum-postgres-rest-api/"/>
        <id>https://rust-backend-lab.pages.dev/blog/axum-postgres-rest-api/</id>
        
        <content type="html" xml:base="https://rust-backend-lab.pages.dev/blog/axum-postgres-rest-api/">&lt;p&gt;Oublie les tutos jouets de type “hello world”. Voici comment construire une &lt;strong&gt;API REST prête pour la production&lt;/strong&gt; en Rust avec &lt;strong&gt;Axum + SQLx + Postgres&lt;/strong&gt;, la stack que je recommande à chaque équipe backend en 2026.&lt;/p&gt;
&lt;p&gt;Au programme : routing propre, state partagé, JSON validé, erreurs correctes, logs structurés, pool de connexions et migrations.&lt;/p&gt;
&lt;h2 id=&quot;pourquoi-axum&quot;&gt;Pourquoi Axum ?&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Construit sur Tokio + Tower + Hyper : des middlewares matures et composables&lt;/li&gt;
&lt;li&gt;Système d’extracteurs (&lt;code&gt;State&lt;/code&gt;, &lt;code&gt;Json&lt;/code&gt;, &lt;code&gt;Path&lt;/code&gt;, &lt;code&gt;Query&lt;/code&gt;) : moins de boilerplate qu’Actix&lt;/li&gt;
&lt;li&gt;Tests faciles, OpenAPI facile via &lt;code&gt;utoipa&lt;/code&gt; ou &lt;code&gt;aide&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Si tu viens d’Express, FastAPI ou du chi de Go, Axum te semblera familier, avec la sécurité de Rust en plus.&lt;/p&gt;
&lt;h2 id=&quot;1-setup-du-projet&quot;&gt;1. Setup du projet&lt;/h2&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;cargo new rust-tasks-api &amp;amp;&amp;amp; 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
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;L’essentiel du &lt;code&gt;Cargo.toml&lt;/code&gt; : &lt;code&gt;axum 0.7&lt;/code&gt;, &lt;code&gt;tokio 1.x&lt;/code&gt;, &lt;code&gt;sqlx 0.8&lt;/code&gt;, &lt;code&gt;tower-http&lt;/code&gt; pour CORS, trace et compression.&lt;/p&gt;
&lt;h2 id=&quot;2-une-architecture-qui-tient-la-charge&quot;&gt;2. Une architecture qui tient la charge&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;src/
  main.rs        # câblage : config, pool, router, serve
  config.rs      # config d&amp;#39;env
  db.rs          # PgPool
  error.rs       # AppError -&amp;gt; HTTP
  state.rs       # AppState
  routes/
    mod.rs
    tasks.rs     # handlers
    health.rs
  models/
    task.rs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Règle : &lt;strong&gt;des handlers fins&lt;/strong&gt;. La logique métier vit dans des fonctions qui prennent &lt;code&gt;&amp;amp;PgPool&lt;/code&gt; et renvoient &lt;code&gt;Result&amp;lt;T, AppError&amp;gt;&lt;/code&gt;. Tu me remercieras à 50 routes.&lt;/p&gt;
&lt;h2 id=&quot;3-state-et-pool&quot;&gt;3. State et pool&lt;/h2&gt;
&lt;pre&gt;&lt;code data-lang=&quot;rust&quot;&gt;// state.rs
use sqlx::PgPool;

#[derive(Clone)]
pub struct AppState {
    pub db: PgPool,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code data-lang=&quot;rust&quot;&gt;// db.rs
use sqlx::postgres::{PgPoolOptions, PgPool};

pub async fn connect(url: &amp;amp;str) -&amp;gt; anyhow::Result&amp;lt;PgPool&amp;gt; {
    let pool = PgPoolOptions::new()
        .max_connections(20)
        .min_connections(2)
        .acquire_timeout(std::time::Duration::from_secs(3))
        .connect(url).await?;
    sqlx::migrate!(&amp;quot;./migrations&amp;quot;).run(&amp;amp;pool).await?;
    Ok(pool)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pourquoi &lt;code&gt;max_connections(20)&lt;/code&gt; ? Commence petit. Postgres déteste 200 connexions inactives. Scale avec PgBouncer plus tard, pas avec un pool géant.&lt;/p&gt;
&lt;h2 id=&quot;4-des-erreurs-bien-gerees&quot;&gt;4. Des erreurs bien gérées&lt;/h2&gt;
&lt;pre&gt;&lt;code data-lang=&quot;rust&quot;&gt;// error.rs
use axum::{http::StatusCode, response::{IntoResponse, Response}, Json};
use serde_json::json;

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

impl IntoResponse for AppError {
    fn into_response(self) -&amp;gt; Response {
        let (status, msg) = match &amp;amp;self {
            AppError::NotFound =&amp;gt; (StatusCode::NOT_FOUND, self.to_string()),
            AppError::BadRequest(_) =&amp;gt; (StatusCode::BAD_REQUEST, self.to_string()),
            AppError::Db(_) =&amp;gt; (StatusCode::INTERNAL_SERVER_ERROR, &amp;quot;internal error&amp;quot;.into()),
        };
        tracing::error!(error = ?self, &amp;quot;request failed&amp;quot;);
        (status, Json(json!({ &amp;quot;error&amp;quot;: msg }))).into_response()
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ne expose jamais les détails de &lt;code&gt;sqlx::Error&lt;/code&gt; aux clients. Loggue l’erreur complète, renvoie une 500 générique.&lt;/p&gt;
&lt;h2 id=&quot;5-handlers-du-crud-avec-validation&quot;&gt;5. Handlers : du CRUD avec validation&lt;/h2&gt;
&lt;pre&gt;&lt;code data-lang=&quot;rust&quot;&gt;// 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,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code data-lang=&quot;rust&quot;&gt;// 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&amp;lt;AppState&amp;gt;,
    Json(input): Json&amp;lt;CreateTask&amp;gt;,
) -&amp;gt; Result&amp;lt;(axum::http::StatusCode, Json&amp;lt;Task&amp;gt;), AppError&amp;gt; {
    use validator::Validate;
    input.validate().map_err(|e| AppError::BadRequest(e.to_string()))?;
    let task = sqlx::query_as!(
        Task,
        &amp;quot;INSERT INTO tasks (id, title, done) VALUES ($1, $2, false) RETURNING id, title, done&amp;quot;,
        Uuid::new_v4(), input.title
    ).fetch_one(&amp;amp;s.db).await?;
    Ok((axum::http::StatusCode::CREATED, Json(task)))
}

pub async fn get_task(
    State(s): State&amp;lt;AppState&amp;gt;,
    Path(id): Path&amp;lt;Uuid&amp;gt;,
) -&amp;gt; Result&amp;lt;Json&amp;lt;Task&amp;gt;, AppError&amp;gt; {
    let task = sqlx::query_as!(Task, &amp;quot;SELECT id, title, done FROM tasks WHERE id=$1&amp;quot;, id)
        .fetch_optional(&amp;amp;s.db).await?
        .ok_or(AppError::NotFound)?;
    Ok(Json(task))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Avec les macros &lt;code&gt;query_as!&lt;/code&gt; et &lt;code&gt;query!&lt;/code&gt;, ton SQL est vérifié à la compilation. Les fautes de frappe sont caught avant la CI.&lt;/p&gt;
&lt;h2 id=&quot;6-router-et-middlewares&quot;&gt;6. Router et middlewares&lt;/h2&gt;
&lt;pre&gt;&lt;code data-lang=&quot;rust&quot;&gt;// 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(&amp;quot;/health&amp;quot;, get(health))
    .route(&amp;quot;/tasks&amp;quot;, post(create_task).get(list_tasks))
    .route(&amp;quot;/tasks/:id&amp;quot;, 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?;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ajoute &lt;code&gt;TraceLayer&lt;/code&gt; dès le premier jour. Débugger la prod sans &lt;code&gt;trace_id&lt;/code&gt; ni logs structurés, c’est l’enfer.&lt;/p&gt;
&lt;h2 id=&quot;7-benchmark-rapide&quot;&gt;7. Benchmark rapide&lt;/h2&gt;
&lt;pre&gt;&lt;code data-lang=&quot;bash&quot;&gt;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
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Si tu es sous 2k rps sur des lectures triviales, vérifie : taille du pool, index manquant, allocs JSON, build debug (utilise &lt;code&gt;--release&lt;/code&gt; !).&lt;/p&gt;
&lt;h2 id=&quot;prochaines-etapes&quot;&gt;Prochaines étapes&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Ajoute l’auth : JWT via &lt;code&gt;jsonwebtoken&lt;/code&gt; + &lt;code&gt;axum-login&lt;/code&gt;, ou clés API en Postgres&lt;/li&gt;
&lt;li&gt;Ajoute OpenAPI : &lt;code&gt;utoipa&lt;/code&gt; + Swagger UI en 30 lignes&lt;/li&gt;
&lt;li&gt;Déploie pas cher : voir &lt;a href=&quot;/blog/host-rust-hetzner/&quot;&gt;Héberger une API Rust sur Hetzner pour 6 €/mois&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Réduis la latence de queue : voir &lt;a href=&quot;/blog/reduce-p99-latency-rust/&quot;&gt;Comment j’ai réduit la p99 de 60 %&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;cta-box&quot;&gt;
&lt;p&gt;&lt;strong&gt;🚀 Tu veux le starter complet ?&lt;/strong&gt; Récupère mon template Axum gratuit (cette structure + Dockerfile + CI + migrations) dans la &lt;a href=&quot;/newsletter/&quot;&gt;newsletter&lt;/a&gt;. La &lt;a href=&quot;/products/rust-backend-roadmap/&quot;&gt;Roadmap&lt;/a&gt; payante va 10x plus loin : tests, auth, observabilité, déploiement.&lt;/p&gt;
&lt;/div&gt;
</content>
        
    </entry>
    <entry xml:lang="fr">
        <title>Cours vidéo Axum en production (liste d&#39;attente)</title>
        <published>2026-01-11T00:00:00+00:00</published>
        <updated>2026-01-11T00:00:00+00:00</updated>
        
        <author>
          <name>Rust Backend Lab</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://rust-backend-lab.pages.dev/products/axum-course/"/>
        <id>https://rust-backend-lab.pages.dev/products/axum-course/</id>
        
        <content type="html" xml:base="https://rust-backend-lab.pages.dev/products/axum-course/">&lt;h1 id=&quot;cours-video-axum-en-production-bientot-149-eur&quot;&gt;Cours vidéo Axum en production (bientôt, 149 €)&lt;/h1&gt;
&lt;p&gt;Un cours vidéo &lt;strong&gt;en français&lt;/strong&gt;, axé code : &lt;strong&gt;construire une vraie API de &lt;code&gt;cargo new&lt;/code&gt; jusqu’à la prod en HTTPS&lt;/strong&gt;, avec tests, observabilité et tuning p99.&lt;/p&gt;
&lt;h2 id=&quot;programme-brouillon&quot;&gt;Programme (brouillon)&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Bases Axum : routing, extracteurs, state&lt;/li&gt;
&lt;li&gt;Postgres + SQLx : migrations, transactions, pooling&lt;/li&gt;
&lt;li&gt;Auth : JWT et clés API, middleware&lt;/li&gt;
&lt;li&gt;Erreurs, validation et OpenAPI&lt;/li&gt;
&lt;li&gt;Tests : unitaires et intégration avec une vraie BDD&lt;/li&gt;
&lt;li&gt;Observabilité : tracing, métriques, dashboards&lt;/li&gt;
&lt;li&gt;Déploiement : Docker, Caddy, Hetzner, CI&lt;/li&gt;
&lt;li&gt;Performance : playbook p99 et tests de charge&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;prix&quot;&gt;Prix&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Early-bird (liste d’attente) : &lt;strong&gt;99 €&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Lancement : &lt;strong&gt;149 €&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Équipe (5 places) : &lt;strong&gt;499 €&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;cta-box&quot;&gt;
&lt;p&gt;&lt;strong&gt;👉 Rejoindre la liste d’attente :&lt;/strong&gt; abonne-toi à la &lt;a href=&quot;/newsletter/&quot;&gt;newsletter&lt;/a&gt; avec l’objet « AXUM ». Je lance le cours quand 500 devs sont inscrits, pas de cours fantôme avec 12 élèves.&lt;/p&gt;
&lt;/div&gt;
</content>
        
    </entry>
    <entry xml:lang="fr">
        <title>Roadmap Rust Backend 2026 : de junior à embauché</title>
        <published>2026-01-10T00:00:00+00:00</published>
        <updated>2026-01-10T00:00:00+00:00</updated>
        
        <author>
          <name>Rust Backend Lab</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://rust-backend-lab.pages.dev/products/rust-backend-roadmap/"/>
        <id>https://rust-backend-lab.pages.dev/products/rust-backend-roadmap/</id>
        
        <content type="html" xml:base="https://rust-backend-lab.pages.dev/products/rust-backend-roadmap/">&lt;h1 id=&quot;roadmap-rust-backend-2026-49-eur&quot;&gt;Roadmap Rust Backend 2026 (49 €)&lt;/h1&gt;
&lt;p&gt;Arrête de sauter de tuto en tuto. Un &lt;strong&gt;chemin étape par étape, des bases Rust au poste de dev backend&lt;/strong&gt;, avec projets, checklists et prépa entretiens.&lt;/p&gt;
&lt;h2 id=&quot;pour-qui&quot;&gt;Pour qui&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Devs backend (Go/Python/Node) qui passent à Rust&lt;/li&gt;
&lt;li&gt;Débutants Rust bloqués après le Book&lt;/li&gt;
&lt;li&gt;Candidats qui visent des postes backend et système&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;contenu&quot;&gt;Contenu&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Un parcours en 6 étapes : Rust de base, Axum, Postgres/SQLx, Tokio, tests, déploiement et entretiens&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;4 projets portfolio&lt;/strong&gt; (API de tâches, service d’auth, worker d’ingestion, tableau temps réel)&lt;/li&gt;
&lt;li&gt;Checklists prod, p99, Docker et CI&lt;/li&gt;
&lt;li&gt;Un pack d’entretien avec 60 questions et réponses (ownership en entretien, Tokio, SQL, system design en Rust)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Notion + PDF + mises à jour à vie&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;prix&quot;&gt;Prix&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;49 € en une fois.&lt;/strong&gt; Mises à jour gratuites pour l’édition 2026. Remboursé sous 14 jours, sans question.&lt;/p&gt;
&lt;div class=&quot;cta-box&quot;&gt;
&lt;p&gt;&lt;strong&gt;👉 Acheter :&lt;/strong&gt; TODO, connecter un lien Gumroad ou Lemon Squeezy ici. Commence par une liste d’attente via la &lt;a href=&quot;/newsletter/&quot;&gt;newsletter&lt;/a&gt; pour valider la demande avant de construire le PDF complet.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Astuce : prévente à 29 € pour les 100 premiers abonnés, puis 49 €. Rareté et boucle de retours.&lt;/em&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;h2 id=&quot;faq&quot;&gt;FAQ&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;C’est un cours vidéo ?&lt;/strong&gt; Non : du texte, du code et des checklists. Plus rapide à mettre à jour, plus facile à consulter. Le cours vidéo est à part (&lt;a href=&quot;/products/axum-course/&quot;&gt;cours Axum&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Combien de temps ?&lt;/strong&gt; 8 à 12 semaines à 5h/semaine si tu fais les projets.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Remboursement ?&lt;/strong&gt; 14 jours, un email, c’est réglé.&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="fr">
        <title>Freelance : expert Rust backend</title>
        <published>2026-01-04T00:00:00+00:00</published>
        <updated>2026-01-04T00:00:00+00:00</updated>
        
        <author>
          <name>Rust Backend Lab</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://rust-backend-lab.pages.dev/hire-me/"/>
        <id>https://rust-backend-lab.pages.dev/hire-me/</id>
        
        <content type="html" xml:base="https://rust-backend-lab.pages.dev/hire-me/">&lt;h1 id=&quot;freelance-rust-backend-crab&quot;&gt;Freelance Rust backend 🦀&lt;/h1&gt;
&lt;p&gt;Missions freelance uniquement, pas de mentoring d’équipe. Le moyen le plus rapide pour moi de monétiser ce blog, et pour toi de sécuriser Rust en prod.&lt;/p&gt;
&lt;h2 id=&quot;je-t-aide-sur&quot;&gt;Je t’aide sur :&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Audits d’architecture Axum : structure projet, erreurs, state, auth&lt;/li&gt;
&lt;li&gt;Audits performance : latence p99, tuning Tokio, pooling, requêtes N+1, allocs JSON&lt;/li&gt;
&lt;li&gt;Revues Postgres : schéma, index, migrations (sqlx / refinery), pooling&lt;/li&gt;
&lt;li&gt;Migrations : roadmap Go / Node / Python vers Rust plus un service pilote&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;formules&quot;&gt;Formules&lt;/h2&gt;
&lt;h3 id=&quot;revue-de-code-express-490-eur&quot;&gt;Revue de code express (490 €)&lt;/h3&gt;
&lt;p&gt;Revue async de ton repo Axum, rapport de 10 à 15 pages plus vidéo Loom, sous 5 jours.&lt;/p&gt;
&lt;h3 id=&quot;sprint-performance-1-900-eur&quot;&gt;Sprint performance (1 900 €)&lt;/h3&gt;
&lt;p&gt;Une semaine, profiling plus correctifs p99, benchmarks avant/après inclus.&lt;/p&gt;
&lt;h3 id=&quot;build-sur-mesure-prix-fixe&quot;&gt;Build sur mesure (prix fixe)&lt;/h3&gt;
&lt;p&gt;Un microservice pilote, cadré et au forfait. Tu repars avec un repo à toi plus un appel de passation.&lt;/p&gt;
&lt;h2 id=&quot;pourquoi-moi&quot;&gt;Pourquoi moi ?&lt;/h2&gt;
&lt;p&gt;J’écris des &lt;a href=&quot;/blog/&quot;&gt;guides prod pratiques&lt;/a&gt; avec du code reproductible, pas de la théorie. Tu as la même rigueur sur ton code.&lt;/p&gt;
&lt;h2 id=&quot;contact&quot;&gt;Contact&lt;/h2&gt;
&lt;p&gt;Email : &lt;strong&gt;hello@rustbackendlab.com&lt;/strong&gt; (TODO : remplacer) avec : accès repo ou doc d’archi, contexte p99 et infra, objectif et deadline.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;En ce moment : 2 créneaux par mois. Les abonnés de la newsletter sont prioritaires.&lt;/p&gt;
&lt;/blockquote&gt;
</content>
        
    </entry>
    <entry xml:lang="fr">
        <title>Sponsoriser Rust Backend Lab</title>
        <published>2026-01-03T00:00:00+00:00</published>
        <updated>2026-01-03T00:00:00+00:00</updated>
        
        <author>
          <name>Rust Backend Lab</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://rust-backend-lab.pages.dev/sponsors/"/>
        <id>https://rust-backend-lab.pages.dev/sponsors/</id>
        
        <content type="html" xml:base="https://rust-backend-lab.pages.dev/sponsors/">&lt;h1 id=&quot;sponsoriser-rust-backend-lab-crab&quot;&gt;Sponsoriser Rust Backend Lab 🦀&lt;/h1&gt;
&lt;p&gt;Touchez une audience ultra ciblée : &lt;strong&gt;des devs Rust backend francophones, seniors, CTO de startups&lt;/strong&gt;. Des gens qui achètent de l’hébergement, des outils et des formations.&lt;/p&gt;
&lt;h2 id=&quot;audience&quot;&gt;Audience&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Devs backend et système&lt;/li&gt;
&lt;li&gt;Utilisateurs Axum / Tokio / Postgres&lt;/li&gt;
&lt;li&gt;CTO de startups qui évaluent Rust&lt;/li&gt;
&lt;li&gt;Seniors avec du pouvoir d’achat&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Sponsors idéaux : hébergeurs (Hetzner, Fly.io, Shuttle), observabilité, outils dev, cloud/SaaS, plateformes de cours, job boards.&lt;/p&gt;
&lt;h2 id=&quot;formats&quot;&gt;Formats&lt;/h2&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Format&lt;/th&gt;&lt;th&gt;Contenu&lt;/th&gt;&lt;th&gt;Idéal pour&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Sponsor d’article&lt;/td&gt;&lt;td&gt;Intro de 80 mots + logo&lt;/td&gt;&lt;td&gt;Lancement / notoriété&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Mise en avant newsletter&lt;/td&gt;&lt;td&gt;Section dédiée, 150 mots&lt;/td&gt;&lt;td&gt;Conversions&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Intégration tuto&lt;/td&gt;&lt;td&gt;Vrai usage de ton produit dans du code&lt;/td&gt;&lt;td&gt;Confiance + activation&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Offre d’emploi&lt;/td&gt;&lt;td&gt;Postes Rust backend&lt;/td&gt;&lt;td&gt;Recrutement&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;regles&quot;&gt;Règles&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Uniquement des produits que j’utiliserais moi-même&lt;/li&gt;
&lt;li&gt;Contenu sponsorisé toujours signalé&lt;/li&gt;
&lt;li&gt;Pas de casinos crypto, SEO douteux ou pubs génériques&lt;/li&gt;
&lt;li&gt;Affiliation + sponsor cumulables si pertinent&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;contact&quot;&gt;Contact&lt;/h2&gt;
&lt;p&gt;Email : &lt;strong&gt;sponsor@rustbackendlab.com&lt;/strong&gt; (TODO : remplacer)&lt;/p&gt;
&lt;p&gt;Précise : produit, URL cible, budget, planning. Je réponds sous 48h avec le kit média et des captures de trafic.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Peu de trafic mais hyper ciblé ? Oui, c’est le principe. 1 000 devs Rust backend valent mieux que 50 000 lecteurs généralistes pour un outil dev B2B.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;Stats actuelles (à mettre à jour chaque mois) :&lt;/strong&gt; visiteurs mensuels : &lt;em&gt;TODO&lt;/em&gt;, abonnés newsletter : &lt;em&gt;TODO&lt;/em&gt;, top articles : guide Axum+Postgres, latence p99, Rust vs Go.&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="fr">
        <title>Newsletter : la checklist Rust Backend Prod gratuite</title>
        <published>2026-01-02T00:00:00+00:00</published>
        <updated>2026-01-02T00:00:00+00:00</updated>
        
        <author>
          <name>Rust Backend Lab</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://rust-backend-lab.pages.dev/newsletter/"/>
        <id>https://rust-backend-lab.pages.dev/newsletter/</id>
        
        <content type="html" xml:base="https://rust-backend-lab.pages.dev/newsletter/">&lt;h1 id=&quot;recois-la-checklist-rust-backend-prod-gratuite&quot;&gt;Reçois la checklist Rust Backend Prod gratuite&lt;/h1&gt;
&lt;p&gt;Un email pratique par semaine pour les devs backend. Zéro blabla.&lt;/p&gt;
&lt;h2 id=&quot;ce-que-tu-recois-tout-de-suite&quot;&gt;Ce que tu reçois tout de suite :&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Template de projet Axum&lt;/strong&gt; (routes, state, erreurs, tracing)&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Checklist Postgres prod&lt;/strong&gt; (pooling, migrations, index)&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Checklist latence p99&lt;/strong&gt; (les 12 correctifs qui comptent vraiment)&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Questions d’entretien Rust backend&lt;/strong&gt; (Axum + Tokio + SQL)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;chaque-semaine&quot;&gt;Chaque semaine :&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Un vrai pattern backend en Rust (avec du code)&lt;/li&gt;
&lt;li&gt;Un benchmark ou un retour de prod&lt;/li&gt;
&lt;li&gt;Un outil ou une crate à connaître&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;cta-box&quot;&gt;
&lt;p&gt;&lt;strong&gt;👉 Inscription :&lt;/strong&gt; remplace l’URL du formulaire dans &lt;code&gt;config.toml&lt;/code&gt; (&lt;code&gt;extra.newsletter_provider_url&lt;/code&gt;) par ton lien Buttondown / ConvertKit / Beehiiv, puis redéploie.&lt;/p&gt;
&lt;p&gt;Recommandé : &lt;strong&gt;Buttondown&lt;/strong&gt; (simple, pas cher) ou &lt;strong&gt;ConvertKit&lt;/strong&gt; (mieux pour automatiser la vente de cours).&lt;/p&gt;
&lt;/div&gt;
&lt;h2 id=&quot;pourquoi-s-inscrire&quot;&gt;Pourquoi s’inscrire ?&lt;/h2&gt;
&lt;p&gt;Le trafic Google est volatile. L’email ne l’est pas. Je mets mes meilleurs conseils prod dans la newsletter en premier, le blog récupère la version publique ensuite.&lt;/p&gt;
&lt;p&gt;&lt;small&gt;Zéro spam. Désinscription en un clic. Je ne vends jamais ton email.&lt;/small&gt;&lt;/p&gt;
</content>
        
    </entry>
    <entry xml:lang="fr">
        <title>À propos</title>
        <published>2026-01-01T00:00:00+00:00</published>
        <updated>2026-01-01T00:00:00+00:00</updated>
        
        <author>
          <name>Rust Backend Lab</name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://rust-backend-lab.pages.dev/about/"/>
        <id>https://rust-backend-lab.pages.dev/about/</id>
        
        <content type="html" xml:base="https://rust-backend-lab.pages.dev/about/">&lt;h1 id=&quot;a-propos-de-rust-backend-lab&quot;&gt;À propos de Rust Backend Lab&lt;/h1&gt;
&lt;p&gt;Rust Backend Lab est une publication de niche pour les &lt;strong&gt;devs backend qui mettent Rust en production&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;On se concentre sur un seul sujet : &lt;strong&gt;les backends web Rust avec Axum + Postgres + Tokio&lt;/strong&gt;. Architecture, performance, déploiement. Pas d’articles génériques sur “c’est quoi l’ownership”.&lt;/p&gt;
&lt;h2 id=&quot;pour-qui&quot;&gt;Pour qui&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Devs backend (Rust, Go, Python, Node) qui passent à Rust&lt;/li&gt;
&lt;li&gt;Devs système qui ont besoin d’API HTTP qui tiennent la charge&lt;/li&gt;
&lt;li&gt;Devs seniors et CTO de startups qui évaluent Rust&lt;/li&gt;
&lt;li&gt;Entreprises qui recrutent des devs Rust backend&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;ce-que-tu-y-trouves&quot;&gt;Ce que tu y trouves&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;De vraies structures de projet (pas des snippets de 50 lignes)&lt;/li&gt;
&lt;li&gt;Des benchmarks avec méthodologie, pas du marketing&lt;/li&gt;
&lt;li&gt;Des patterns Postgres : pooling, migrations, transactions&lt;/li&gt;
&lt;li&gt;Des playbooks latence p99&lt;/li&gt;
&lt;li&gt;De l’hébergement prod pas cher (6 à 12 €/mois)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;qui-ecrit-ici&quot;&gt;Qui écrit ici ?&lt;/h2&gt;
&lt;p&gt;Je suis dev backend, je travaille tous les jours avec Rust, Postgres et le cloud. J’écris les guides que j’aurais voulu lire quand j’ai migré des services Go/Node vers Rust.&lt;/p&gt;
&lt;h2 id=&quot;modele-economique-transparent&quot;&gt;Modèle économique (transparent)&lt;/h2&gt;
&lt;p&gt;Ce blog est une activité :&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Articles gratuits → liste email&lt;/li&gt;
&lt;li&gt;Produits payants (roadmap, cours vidéo, templates)&lt;/li&gt;
&lt;li&gt;Sponsors (outils dev, hébergeurs)&lt;/li&gt;
&lt;li&gt;Liens affiliés (Hetzner, DigitalOcean), toujours signalés&lt;/li&gt;
&lt;li&gt;Freelance : audits et revues de code&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Travailler ensemble ? Voir &lt;a href=&quot;/sponsors/&quot;&gt;Sponsors&lt;/a&gt; ou &lt;a href=&quot;/hire-me/&quot;&gt;Freelance&lt;/a&gt;.&lt;/p&gt;
</content>
        
    </entry>
</feed>
