Aller au contenu

Projet : API web avec Axum en Rust

Dans ce projet, vous allez construire un vrai serveur web : une petite API de tâches (to-do). D’autres programmes (un site web, une application mobile, ou simplement curl dans votre terminal) lui envoient des requêtes HTTP, et il répond en JSON. C’est le même genre de chose qui fait tourner le « back end » (la partie serveur) de la plupart des applications que vous utilisez.

Nous allons utiliser axum, un framework web populaire et agréable, créé par l’équipe de Tokio. Il s’appuie sur Rust asynchrone et sur serde, donc ce projet rassemble beaucoup de ce que vous avez appris : les structs, Result, les smart pointers (pointeurs intelligents) et l’état partagé entre threads. Si vous avez fait le projet liste de tâches, vous reconnaîtrez l’idée. Cette fois, la liste de tâches vit derrière une adresse web.

À la fin, votre serveur répondra à ces requêtes :

Méthode et chemin Ce qu’elle fait Statut en cas de succès
GET /todos Lister toutes les tâches 200 OK
GET /todos/{id} Obtenir une tâche 200 OK, ou 404 Not Found
POST /todos Créer une tâche à partir de JSON 201 Created
DELETE /todos/{id} Supprimer une tâche 204 No Content, ou 404 Not Found

En JSON, une tâche ressemble à ceci :

{"id":1,"title":"Acheter du lait","done":false}

Une remarque importante : un serveur web attend des requêtes indéfiniment et a besoin d’un port réseau, il ne peut donc pas tourner dans le Rust Playground. Le code serveur de cette page est fait pour votre propre ordinateur, avec cargo run. Chaque étape ci-dessous a été compilée et exécutée, et les réponses affichées sont réelles. Quand une partie du programme peut tourner dans le Playground, vous trouverez un bouton « Essayez par vous-même ».

Créez un nouveau projet et ajoutez quatre crates :

Terminal window
cargo new todo-api
cd todo-api
cargo add [email protected] tokio --features tokio/full
cargo add serde --features derive
cargo add serde_json

Votre Cargo.toml contient maintenant quelque chose comme ceci (les numéros de version exacts peuvent être plus récents) :

[dependencies]
axum = "0.8"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }

Le rôle de chacune :

  • axum gère le HTTP : il lit les requêtes et envoie les réponses.
  • tokio est le runtime asynchrone qui fait tout tourner. Voir Async/Await.
  • serde et serde_json transforment vos structs en JSON et inversement. Voir JSON avec serde.

Il vous faudra aussi curl pour envoyer des requêtes. Il est fourni avec macOS, Linux, et Windows 10 et suivants. (Sous Windows, tapez curl.exe dans PowerShell, car curl tout court y désigne autre chose.)

Remplacez src/main.rs par ceci :

use axum::{routing::get, Router};
#[tokio::main]
async fn main() {
let app = Router::new().route("/", get(hello));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
println!("Serveur à l'écoute sur http://127.0.0.1:3000");
axum::serve(listener, app).await.unwrap();
}
async fn hello() -> &'static str {
"Bonjour, le monde !"
}

Trois éléments travaillent ensemble ici :

  • Un handler (gestionnaire) est une async fn qui répond à une requête. hello renvoie du texte, et axum le renvoie comme réponse.
  • Le router (routeur) relie des chemins à des handlers. .route("/", get(hello)) veut dire « quand quelqu’un envoie une requête GET sur /, appelle hello ».
  • Le listener ouvre le port 3000 sur votre propre ordinateur (127.0.0.1), et axum::serve commence à répondre aux requêtes. Il tourne jusqu’à ce que vous l’arrêtiez.

Lancez-le avec cargo run. Le programme affiche une ligne, puis attend :

Serveur à l'écoute sur http://127.0.0.1:3000

Laissez-le tourner, ouvrez un deuxième terminal et envoyez-lui une requête :

Terminal window
curl http://127.0.0.1:3000/
Bonjour, le monde !

Le serveur n’ajoute pas de retour à la ligne après le texte, donc l’invite de votre terminal peut apparaître juste après Bonjour, le monde !. C’est normal. Sur cette page, chaque réponse est affichée sur sa propre ligne. Vous pouvez aussi ouvrir http://127.0.0.1:3000/ dans votre navigateur. Appuyez sur Ctrl + C dans le premier terminal pour arrêter le serveur. Il faut l’arrêter et le relancer chaque fois que vous modifiez le code.

Étape 2 : renvoyer du JSON à partir d’une struct

Section intitulée « Étape 2 : renvoyer du JSON à partir d’une struct »

Les API répondent généralement en JSON. Avec axum, vous enveloppez dans Json(...) n’importe quelle valeur qui implémente le trait Serialize de serde, et axum la transforme en JSON et pose le bon en-tête content-type :

use axum::{routing::get, Json, Router};
use serde::Serialize;
#[derive(Serialize)]
struct Todo {
id: u32,
title: String,
done: bool,
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(hello))
.route("/example", get(example_todo));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
println!("Serveur à l'écoute sur http://127.0.0.1:3000");
axum::serve(listener, app).await.unwrap();
}
async fn hello() -> &'static str {
"Bonjour, le monde !"
}
async fn example_todo() -> Json<Todo> {
let todo = Todo {
id: 1,
title: String::from("Apprendre axum"),
done: false,
};
Json(todo)
}

L’option -i demande à curl d’afficher aussi la ligne de statut et les en-têtes :

Terminal window
curl -i http://127.0.0.1:3000/example
HTTP/1.1 200 OK
content-type: application/json
content-length: 46
{"id":1,"title":"Apprendre axum","done":false}

(curl affiche aussi un en-tête date, que nous omettons sur cette page.) Les clés JSON sont exactement les noms des champs de la struct. Vous pouvez vérifier la forme de ce JSON dans le Playground, sans serveur, car c’est du serde tout simple. Voici aussi la struct NewTodo que nous utiliserons à l’étape 4 pour les données reçues :

use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize)]
struct Todo {
id: u32,
title: String,
done: bool,
}
#[derive(Debug, Deserialize)]
struct NewTodo {
title: String,
}
fn main() {
let todo = Todo { id: 1, title: String::from("Apprendre axum"), done: false };
println!("{}", serde_json::to_string(&todo).unwrap());
let list = vec![todo.clone(), Todo { id: 2, title: String::from("Acheter du lait"), done: true }];
println!("{}", serde_json::to_string(&list).unwrap());
let input: NewTodo = serde_json::from_str(r#"{"title":"Promener le chien"}"#).unwrap();
println!("{:?}", input);
let bad = serde_json::from_str::<NewTodo>(r#"{"name":"Promener le chien"}"#);
println!("{}", bad.unwrap_err());
}

Sortie :

{"id":1,"title":"Apprendre axum","done":false}
[{"id":1,"title":"Apprendre axum","done":false},{"id":2,"title":"Acheter du lait","done":true}]
NewTodo { title: "Promener le chien" }
missing field `title` at line 1 column 28
▶ Essayez par vous-même

La dernière ligne est le message d’erreur de serde_json, en anglais : il manque le champ title (ligne 1, colonne 28). Un Vec<Todo> devient un tableau JSON. C’est exactement ce que renverra GET /todos.

Dans un chemin comme /todos/2, le 2 change d’une requête à l’autre. Avec axum 0.8, vous marquez cette partie de la route avec des accolades, {id}, et vous la lisez dans le handler avec l’extracteur Path :

use axum::{extract::Path, routing::get, Router};
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/hello/{name}", get(hello_name))
.route("/add/{a}/{b}", get(add));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
println!("Serveur à l'écoute sur http://127.0.0.1:3000");
axum::serve(listener, app).await.unwrap();
}
async fn hello_name(Path(name): Path<String>) -> String {
format!("Bonjour, {name} !")
}
async fn add(Path((a, b)): Path<(i32, i32)>) -> String {
format!("{a} + {b} = {}", a + b)
}

Un extracteur est un paramètre de handler qui extrait un morceau de la requête. Path(name): Path<String> est un pattern (motif) : il déballe le Path et vous donne le String qu’il contient. Avec deux paramètres, vous obtenez un tuple.

Terminal window
curl http://127.0.0.1:3000/hello/Ana
curl http://127.0.0.1:3000/add/2/3
Bonjour, Ana !
2 + 3 = 5

Voici une bonne surprise. Vous avez dit que a et b sont des i32, donc axum le vérifie pour vous. Envoyez quelque chose qui n’est pas un nombre :

Terminal window
curl -i http://127.0.0.1:3000/add/2/x
HTTP/1.1 400 Bad Request
content-type: text/plain; charset=utf-8
content-length: 68
Invalid URL: Cannot parse value at index 1 with value `x` to a `i32`

Ce message vient d’axum (en anglais) : l’URL est invalide, car la valeur x à l’indice 1 ne peut pas être convertie en i32. Votre handler n’a même pas été exécuté. Les types dans la signature de votre fonction sont la validation.

Pour créer une tâche, le client envoie du JSON dans le corps (body) de la requête. L’extracteur Json lit le corps et le désérialise dans votre struct. Le client n’envoie qu’un titre, donc nous utilisons une struct NewTodo à part pour les données reçues :

use axum::{routing::post, Json, Router};
use serde::{Deserialize, Serialize};
#[derive(Serialize)]
struct Todo {
id: u32,
title: String,
done: bool,
}
#[derive(Deserialize)]
struct NewTodo {
title: String,
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/echo", post(echo_todo));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
println!("Serveur à l'écoute sur http://127.0.0.1:3000");
axum::serve(listener, app).await.unwrap();
}
async fn echo_todo(Json(input): Json<NewTodo>) -> Json<Todo> {
let todo = Todo {
id: 1,
title: input.title,
done: false,
};
Json(todo)
}

Dans la route, utilisez post(...) au lieu de get(...). Avec curl, -X POST choisit la méthode, -H ajoute un en-tête et -d envoie le corps :

Terminal window
curl -X POST http://127.0.0.1:3000/echo -H "Content-Type: application/json" -d '{"title":"Acheter du lait"}'
{"id":1,"title":"Acheter du lait","done":false}

Là encore, axum vérifie les données avant que votre code ne s’exécute. S’il manque title dans le JSON, vous obtenez 422 Unprocessable Entity :

Terminal window
curl -i -X POST http://127.0.0.1:3000/echo -H "Content-Type: application/json" -d '{"name":"Acheter du lait"}'
HTTP/1.1 422 Unprocessable Entity
content-type: text/plain; charset=utf-8
content-length: 99
Failed to deserialize the JSON body into the target type: missing field `title` at line 1 column 26

Le message (en anglais) dit qu’axum n’a pas pu désérialiser le corps JSON dans le type attendu, parce que le champ title manque. Et si vous oubliez l’en-tête Content-Type, vous obtenez 415 Unsupported Media Type avec le message Expected request with `Content-Type: application/json` . Cet en-tête est l’oubli le plus fréquent quand on teste avec curl.

Étape 5 : un état partagé pour une vraie API de tâches

Section intitulée « Étape 5 : un état partagé pour une vraie API de tâches »

Jusqu’ici, chaque handler inventait ses données. Une vraie API doit se souvenir des tâches d’une requête à l’autre. Les requêtes peuvent arriver en même temps et sont traitées sur des threads différents, donc la liste doit pouvoir être partagée sans risque. C’est le rôle du duo que vous avez rencontré sur la page Concurrence :

  • Mutex<Vec<Todo>> garantit qu’une seule requête modifie la liste à la fois.
  • Arc<...> permet à plusieurs handlers de posséder un pointeur vers cette même liste (voir Smart pointers).

On donne un nom court au type avec type Db = Arc<Mutex<Vec<Todo>>>;, on le passe au routeur avec .with_state(db), et on le lit dans les handlers avec l’extracteur State. axum clone l’Arc pour chaque requête (c’est peu coûteux : seul un pointeur est copié).

Voici le programme complet. Remplacez src/main.rs par celui-ci :

use std::sync::{Arc, Mutex};
use axum::{
extract::{Path, State},
http::StatusCode,
routing::get,
Json, Router,
};
use serde::{Deserialize, Serialize};
#[derive(Clone, Serialize)]
struct Todo {
id: u32,
title: String,
done: bool,
}
#[derive(Deserialize)]
struct NewTodo {
title: String,
}
// La « base de données » : une liste de tâches que toutes les requêtes partagent.
type Db = Arc<Mutex<Vec<Todo>>>;
#[tokio::main]
async fn main() {
let db: Db = Arc::new(Mutex::new(Vec::new()));
let app = Router::new()
.route("/todos", get(list_todos).post(create_todo))
.route("/todos/{id}", get(get_todo).delete(delete_todo))
.with_state(db);
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
println!("Serveur à l'écoute sur http://127.0.0.1:3000");
axum::serve(listener, app).await.unwrap();
}
// GET /todos
async fn list_todos(State(db): State<Db>) -> Json<Vec<Todo>> {
let todos = db.lock().unwrap();
Json(todos.clone())
}
// GET /todos/{id}
async fn get_todo(State(db): State<Db>, Path(id): Path<u32>) -> Result<Json<Todo>, StatusCode> {
let todos = db.lock().unwrap();
match todos.iter().find(|todo| todo.id == id) {
Some(todo) => Ok(Json(todo.clone())),
None => Err(StatusCode::NOT_FOUND),
}
}
// POST /todos
async fn create_todo(State(db): State<Db>, Json(input): Json<NewTodo>) -> (StatusCode, Json<Todo>) {
let mut todos = db.lock().unwrap();
let id = todos.iter().map(|todo| todo.id).max().unwrap_or(0) + 1;
let todo = Todo {
id,
title: input.title,
done: false,
};
todos.push(todo.clone());
(StatusCode::CREATED, Json(todo))
}
// DELETE /todos/{id}
async fn delete_todo(State(db): State<Db>, Path(id): Path<u32>) -> StatusCode {
let mut todos = db.lock().unwrap();
let before = todos.len();
todos.retain(|todo| todo.id != id);
if todos.len() < before {
StatusCode::NO_CONTENT
} else {
StatusCode::NOT_FOUND
}
}

Passons en revue les nouvelles idées :

  • Un chemin, plusieurs méthodes. get(list_todos).post(create_todo) envoie GET /todos et POST /todos vers des handlers différents.
  • Les codes de statut sont des valeurs de retour. StatusCode vient du module http d’axum. Un handler peut renvoyer un Result : Ok(Json(todo)) devient un 200 avec du JSON, et Err(StatusCode::NOT_FOUND) devient un 404 vide. Un tuple (StatusCode::CREATED, Json(todo)) fixe le statut et le corps. Renvoyer seulement StatusCode::NO_CONTENT (204) veut dire « c’est fait, rien à renvoyer ».
  • lock() et le guard. db.lock().unwrap() attend qu’aucune autre requête n’utilise la liste, puis vous donne un guard (verrou actif) que vous pouvez lire et modifier comme un Vec normal. Le verrou est libéré quand le guard sort de la portée, à la fin de la fonction.
  • Nouveaux ids. Le prochain id vaut un de plus que le plus grand id de la liste, ou 1 si la liste est vide.
  • Supprimer. retain garde toutes les tâches dont l’id ne correspond pas. Si la longueur n’a pas changé, rien n’a été supprimé, donc on renvoie 404.

Pourquoi std::sync::Mutex et pas un mutex asynchrone ? Il convient très bien (et il est plus rapide) tant que vous ne gardez pas le verrou pendant un .await. Aucun de nos handlers ne le fait : ils verrouillent, travaillent sur la liste, puis rendent la main.

La logique de liste et de verrou n’a pas besoin d’un serveur web, vous pouvez donc l’essayer dans le Playground. Ces fonctions font le même travail que les handlers, sans axum :

use std::sync::{Arc, Mutex};
#[derive(Clone, Debug)]
struct Todo {
id: u32,
title: String,
done: bool,
}
type Db = Arc<Mutex<Vec<Todo>>>;
fn create(db: &Db, title: &str) -> Todo {
let mut todos = db.lock().unwrap();
let id = todos.iter().map(|todo| todo.id).max().unwrap_or(0) + 1;
let todo = Todo { id, title: title.to_string(), done: false };
todos.push(todo.clone());
todo
}
fn get(db: &Db, id: u32) -> Option<Todo> {
let todos = db.lock().unwrap();
todos.iter().find(|todo| todo.id == id).cloned()
}
fn delete(db: &Db, id: u32) -> bool {
let mut todos = db.lock().unwrap();
let before = todos.len();
todos.retain(|todo| todo.id != id);
todos.len() < before
}
fn main() {
let db: Db = Arc::new(Mutex::new(Vec::new()));
let second_handle = Arc::clone(&db); // ce que fait axum pour chaque requête
println!("{:?}", create(&db, "Acheter du lait"));
println!("{:?}", create(&second_handle, "Apprendre axum"));
println!("{:?}", get(&db, 2));
println!("{:?}", get(&db, 99));
println!("1 supprimée ? {}", delete(&db, 1));
println!("1 supprimée à nouveau ? {}", delete(&db, 1));
println!("restantes : {}", db.lock().unwrap().len());
}

Sortie :

Todo { id: 1, title: "Acheter du lait", done: false }
Todo { id: 2, title: "Apprendre axum", done: false }
Some(Todo { id: 2, title: "Apprendre axum", done: false })
None
1 supprimée ? true
1 supprimée à nouveau ? false
restantes : 1
▶ Essayez par vous-même

La tâche créée via second_handle apparaît quand on lit via db, parce que les deux Arc pointent vers la même liste.

Lancez le serveur avec cargo run et, dans un deuxième terminal, essayez chaque endpoint (point d’accès). Au début, la liste est vide :

Terminal window
curl http://127.0.0.1:3000/todos
[]

Créez une tâche. Remarquez le statut 201 Created :

Terminal window
curl -i -X POST http://127.0.0.1:3000/todos -H "Content-Type: application/json" -d '{"title":"Acheter du lait"}'
HTTP/1.1 201 Created
content-type: application/json
content-length: 47
{"id":1,"title":"Acheter du lait","done":false}

Créez-en une deuxième et listez-les toutes :

Terminal window
curl -X POST http://127.0.0.1:3000/todos -H "Content-Type: application/json" -d '{"title":"Apprendre axum"}'
curl http://127.0.0.1:3000/todos
{"id":2,"title":"Apprendre axum","done":false}
[{"id":1,"title":"Acheter du lait","done":false},{"id":2,"title":"Apprendre axum","done":false}]

Récupérez-en une par son id, puis demandez-en une qui n’existe pas :

Terminal window
curl http://127.0.0.1:3000/todos/2
curl -i http://127.0.0.1:3000/todos/99
{"id":2,"title":"Apprendre axum","done":false}
HTTP/1.1 404 Not Found
content-length: 0

Supprimez la tâche 1, puis essayez de la supprimer à nouveau :

Terminal window
curl -i -X DELETE http://127.0.0.1:3000/todos/1
curl -i -X DELETE http://127.0.0.1:3000/todos/1
HTTP/1.1 204 No Content
HTTP/1.1 404 Not Found
content-length: 0
Terminal window
curl http://127.0.0.1:3000/todos
[{"id":2,"title":"Apprendre axum","done":false}]

axum gère aussi certaines erreurs pour vous. Un mauvais type dans le JSON donne un 422 qui explique le problème, et une méthode que vous n’avez pas prévue pour un chemin donne 405 Method Not Allowed, avec un en-tête qui liste les méthodes qui fonctionnent :

Terminal window
curl -i -X POST http://127.0.0.1:3000/todos -H "Content-Type: application/json" -d '{"title": 42}'
curl -i -X PUT http://127.0.0.1:3000/todos/2
HTTP/1.1 422 Unprocessable Entity
content-type: text/plain; charset=utf-8
content-length: 130
Failed to deserialize the JSON body into the target type: title: invalid type: integer `42`, expected a string at line 1 column 12
HTTP/1.1 405 Method Not Allowed
allow: GET,HEAD,DELETE
content-length: 0

Ici, le message indique que title a reçu un entier (42) alors qu’une chaîne de caractères était attendue. Arrêtez le serveur avec Ctrl + C et relancez-le : la liste est vide. Les données ne vivent qu’en mémoire. Les enregistrer est la première idée ci-dessous.

Utiliser l’ancienne syntaxe de route :id. Beaucoup de tutoriels en ligne ont été écrits pour axum 0.7, qui utilisait /todos/:id. Avec axum 0.8, cela compile, mais le programme panique dès son démarrage :

thread 'main' (12345) panicked at src/main.rs:32:10:
Path segments must not start with `:`. For capture groups, use `{capture}`. If you meant to literally match a segment starting with a colon, call `without_v07_checks` on the router.

Le message dit que les segments de chemin ne doivent pas commencer par :, et qu’il faut écrire {capture} pour les parties variables. La correction : écrivez /todos/{id}.

Mettre Json avant les autres extracteurs. Le corps de la requête ne peut être lu qu’une seule fois, donc l’extracteur qui le lit (Json) doit être le dernier paramètre. Si vous écrivez create_todo(Json(input): Json<NewTodo>, State(db): State<Db>), l’erreur ne le dit pas directement. Voici les lignes importantes :

error[E0277]: the trait bound `fn(Json<NewTodo>, State<...>) -> ... {create_todo}: Handler<_, ...>` is not satisfied
|
31 | .route("/todos", get(list_todos).post(create_todo))
| ---- ^^^^^^^^^^^ unsatisfied trait bound
|
= note: Consider using `#[axum::debug_handler]` to improve the error message

Quand vous voyez « Handler is not satisfied » (le trait Handler n’est pas satisfait), vérifiez l’ordre de vos paramètres, et vérifiez que chaque type de retour est quelque chose qu’axum sait transformer en réponse.

Oublier -H "Content-Type: application/json" dans curl. Le serveur répond 415. Cet en-tête est obligatoire pour l’extracteur Json.

Oublier de relancer. Le serveur qui tourne, c’est l’ancien programme. Arrêtez-le avec Ctrl + C et refaites cargo run après chaque modification.

Une fois l’API de base fonctionnelle, voici de bonnes étapes suivantes. Essayez-les une par une.

  • Marquer une tâche comme terminée. Ajoutez PATCH /todos/{id} avec .patch(...) sur la deuxième route. Trouvez la tâche avec iter_mut(), mettez done = true et renvoyez-la.
  • Validation. Un titre vide ne devrait pas être accepté. Un handler peut renvoyer Result<(StatusCode, Json<Todo>), (StatusCode, String)> et répondre Err((StatusCode::BAD_REQUEST, String::from("le titre ne doit pas être vide"))). Utilisez title.trim().is_empty() pour vérifier.
  • Des ids qui ne se répètent jamais. Avec « le plus grand id plus un », supprimer la tâche la plus récente permet de réutiliser son id. Gardez plutôt un compteur séparé dans votre état, par exemple une struct avec todos: Vec<Todo> et next_id: u32 à l’intérieur du Mutex.
  • Persistance dans un fichier JSON. Dérivez aussi Deserialize sur Todo. Au démarrage, lisez todos.json avec std::fs::read_to_string et serde_json::from_str (ou partez d’une liste vide si le fichier n’existe pas). Après chaque modification, réécrivez la liste avec serde_json::to_string_pretty et std::fs::write. Les pages Lire et écrire des fichiers et JSON avec serde contiennent tout ce qu’il vous faut. Plus tard, vous pourrez remplacer le fichier par une vraie base de données comme SQLite.
  • Des logs avec tower-http. Lancez cargo add tower-http --features trace et cargo add tracing-subscriber, puis ajoutez une couche (layer) de journalisation. Le serveur affiche alors une ligne pour chaque requête, avec la méthode, le chemin, le statut et la durée :
use tower_http::trace::TraceLayer;
// au début de main :
tracing_subscriber::fmt()
.with_max_level(tracing_subscriber::filter::LevelFilter::DEBUG)
.init();
// lors de la construction du routeur :
let app = Router::new()
.route("/todos", get(list_todos).post(create_todo))
.route("/todos/{id}", get(get_todo).delete(delete_todo))
.layer(TraceLayer::new_for_http())
.with_state(db);
  • Notes sur le déploiement. 127.0.0.1 n’accepte que les requêtes venant de votre propre ordinateur. Pour accepter des requêtes d’autres machines (par exemple dans Docker ou sur un serveur), écoutez plutôt sur 0.0.0.0. Beaucoup d’hébergeurs indiquent à votre programme quel port utiliser via une variable d’environnement PORT, que vous pouvez lire avec std::env::var (voir Variables d’environnement et processus). Compilez avec cargo build --release pour obtenir un serveur bien plus rapide. Le résultat est un seul fichier dans target/release/, que vous pouvez copier sur un serveur Linux ou mettre dans une petite image Docker. Pour un site public, placez-le derrière un reverse proxy (comme Caddy ou nginx) qui gère le HTTPS.
  • Un handler est une async fn. Un router associe une méthode et un chemin à un handler. axum::serve le fait tourner sur un TcpListener de tokio.
  • Les extracteurs tirent des données de la requête : Path pour des parties de l’URL (/todos/{id}), Json pour le corps (toujours en dernier), et State pour les données partagées.
  • Renvoyez Json(value) pour du JSON, StatusCode pour un statut, un tuple pour fixer les deux, ou un Result pour choisir entre un succès et un statut d’erreur.
  • Arc<Mutex<Vec<Todo>>> partage une seule liste sans risque entre toutes les requêtes. Verrouillez-la, utilisez-la, et laissez le guard être libéré. Ne la gardez pas verrouillée pendant un .await.
  • Testez votre API avec curl : -i affiche le statut, -X choisit la méthode, -H ajoute un en-tête et -d envoie un corps.

Petit quiz

1. Avec axum 0.8, comment écrit-on une route avec un id dans le chemin ?

2. Un handler renvoie (StatusCode::CREATED, Json(todo)). Que reçoit le client ?

3. Pourquoi la liste partagée est-elle enveloppée dans Arc<Mutex<...>> ?

4. Quel extracteur doit venir en dernier dans les paramètres d'un handler ?

5. Vous envoyez du JSON avec curl -X POST, mais vous oubliez l'en-tête Content-Type: application/json. Que répond axum ?

Score : 0 / 5