Ajouter une route web (handler axum + template Askama)

Ce guide explique comment ajouter un endpoint HTTP SSR complet dans gitrust : route, handler, template Askama, et test E2E Playwright.

Pré-requis

  • Familiarité avec le tutoriel 01-getting-started.md.
  • Notions de base d'Axum (extracteurs, State, IntoResponse).

Vue d'ensemble du flux

routes.rs → handler (handlers/*.rs) → service (gitrust-core) → template Askama

Gitrust suit le patron vertical slice : chaque route traverse toutes les couches. Ne créez jamais un handler sans template, ni un service sans route.

Étape 1 : Déclarer la route dans routes.rs

Ouvrez crates/gitrust-web/src/routes.rs. Ajoutez votre route dans la section qui correspond à son contexte (routes authentifiées, routes repo, routes admin…) :

// Exemple : GET /settings/notifications
.route(
    "/settings/notifications",
    get(handlers::notifications::preferences_form)
        .post(handlers::notifications::preferences_submit),
)

La signature de routes.rs utilise Router<DatabaseConnection> — l'état partagé est toujours la connexion DB injectée via State.

Étape 2 : Créer le handler

Créez ou complétez crates/gitrust-web/src/handlers/mon_handler.rs :

use axum::{extract::State, response::IntoResponse};
use sea_orm::DatabaseConnection;

use crate::{
    auth::AuthUser,
    error::AppError,
    templates::MonPageTemplate,
};
use gitrust_core::services::mon_service::MonService;

/// GET /settings/mon-endpoint
pub async fn mon_form(
    State(db): State<DatabaseConnection>,
    user: AuthUser,
) -> Result<impl IntoResponse, AppError> {
    let data = MonService::get_data(&db, user.user_id).await?;

    Ok(MonPageTemplate {
        username: user.username.clone(),
        is_admin: user.has_role("admin"),
        current_path: "/settings/mon-endpoint".to_owned(),
        data,
    })
}

/// POST /settings/mon-endpoint
pub async fn mon_submit(
    State(db): State<DatabaseConnection>,
    user: AuthUser,
    axum::Form(input): axum::Form<MonInput>,
) -> Result<impl IntoResponse, AppError> {
    MonService::update_data(&db, user.user_id, input).await?;
    Ok(axum::response::Redirect::to("/settings/mon-endpoint"))
}

Règles obligatoires :

  • Pas de .unwrap() ni .expect() — toutes les erreurs remontent via ? vers AppError.
  • AppError implémente IntoResponse et mappe les variantes vers les codes HTTP appropriés.
  • AuthUser (extracteur rustwarden-core) rejette automatiquement les requêtes non authentifiées avec 401.

Étape 3 : Déclarer le module handler

Dans crates/gitrust-web/src/handlers/mod.rs, ajoutez :

pub mod mon_handler;

Étape 4 : Créer la struct de template

Dans crates/gitrust-web/src/templates.rs, ajoutez la struct Askama :

#[derive(Template)]
#[template(path = "settings/mon_endpoint.html")]
pub struct MonPageTemplate {
    pub username: String,
    pub is_admin: bool,
    pub current_path: String,
    pub data: MonData, // votre DTO métier
}

Les champs username, is_admin, et current_path sont présents sur toutes les structs de template — ils alimentent la sidebar contextuelle.

Étape 5 : Créer le template Askama

Créez crates/gitrust-web/templates/settings/mon_endpoint.html :

{% extends "base.html" %}

{% block title %}Mon endpoint — gitrust{% endblock %}

{% block content %}
<div class="container mx-auto px-4 py-8">
  <h1 class="text-2xl font-bold mb-6">Mon endpoint</h1>

  <form method="POST" action="/settings/mon-endpoint">
    <input type="hidden" name="_csrf" value="{{ csrf_token }}">

    <div class="form-control mb-4">
      <label class="label">
        <span class="label-text">Valeur</span>
      </label>
      <input
        type="text"
        name="valeur"
        value="{{ data.valeur }}"
        class="input input-bordered"
        required
      >
    </div>

    <button type="submit" class="btn btn-primary">Enregistrer</button>
  </form>
</div>
{% endblock %}

Règle CSRF : tout formulaire POST doit inclure <input type="hidden" name="_csrf" value="{{ csrf_token }}">. Le middleware rustwarden-core rejette les requêtes sans token CSRF valide avec 403.

Étape 6 : Gestion des erreurs dans les templates

Pour afficher les erreurs de validation à l'utilisateur, utilisez le pattern flash :

// Dans le handler POST, en cas d'erreur de validation :
return Ok(axum::response::Redirect::to(
    "/settings/mon-endpoint?error=valeur+invalide"
));

Dans le template :

{% if let Some(err) = query_params.error %}
  <div class="alert alert-error">{{ err }}</div>
{% endif %}

Étape 7 : Écrire les tests E2E Playwright

Créez tests/e2e/mon_endpoint.spec.ts :

import { test, expect } from "@playwright/test";

test("la page mon-endpoint est accessible après login", async ({ page }) => {
  // Authentification (helper existant)
  await page.goto("/login");
  await page.fill("[name=username]", "alice");
  await page.fill("[name=password]", "SecurePass123!");
  await page.click("[type=submit]");

  await page.goto("/settings/mon-endpoint");
  await expect(page).toHaveTitle(/Mon endpoint/);
});

test("soumettre le formulaire redirige vers la même page", async ({ page }) => {
  // ... login ...
  await page.goto("/settings/mon-endpoint");
  await page.fill("[name=valeur]", "nouvelle-valeur");
  await page.click("[type=submit]");
  await expect(page).toHaveURL("/settings/mon-endpoint");
});

Lancez les tests E2E :

npm run test:e2e

Exemple complet : la page de gestion des labels

Pour un exemple réel de ce patron appliqué à une feature complète, consultez :

  • Route : crates/gitrust-web/src/routes.rs — section Issues / Labels
  • Handler : crates/gitrust-web/src/handlers/labels.rs
  • Template : crates/gitrust-web/templates/repository/labels.html
  • Service : crates/gitrust-core/src/services/label_service.rs
  • Tests E2E : tests/e2e/labels.spec.ts

Voir aussi