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¶
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 :
É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 :
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