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?versAppError. AppErrorimplémenteIntoResponseet mappe les variantes vers les codes HTTP appropriés.AuthUser(extracteur rustwarden-core) rejette automatiquement les requêtes non authentifiées avec401.
É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
GitRust