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.
Ce que vous allez construire
Section intitulée « Ce que vous allez construire »À 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 ».
Préparer le projet
Section intitulée « Préparer le projet »Créez un nouveau projet et ajoutez quatre crates :
cargo new todo-apicd todo-apicargo add serde --features derivecargo add serde_jsonVotre 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.)
Étape 1 : Bonjour, le monde !
Section intitulée « Étape 1 : Bonjour, le monde ! »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 fnqui répond à une requête.hellorenvoie 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êteGETsur/, appellehello». - Le listener ouvre le port 3000 sur votre propre ordinateur (
127.0.0.1), etaxum::servecommence à 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:3000Laissez-le tourner, ouvrez un deuxième terminal et envoyez-lui une requête :
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 :
curl -i http://127.0.0.1:3000/exampleHTTP/1.1 200 OKcontent-type: application/jsoncontent-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 28La 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.
Étape 3 : les paramètres de chemin
Section intitulée « Étape 3 : les paramètres de chemin »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.
curl http://127.0.0.1:3000/hello/Anacurl http://127.0.0.1:3000/add/2/3Bonjour, Ana !2 + 3 = 5Voici 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 :
curl -i http://127.0.0.1:3000/add/2/xHTTP/1.1 400 Bad Requestcontent-type: text/plain; charset=utf-8content-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.
Étape 4 : accepter du JSON avec POST
Section intitulée « Étape 4 : accepter du JSON avec POST »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 :
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 :
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 Entitycontent-type: text/plain; charset=utf-8content-length: 99
Failed to deserialize the JSON body into the target type: missing field `title` at line 1 column 26Le 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 /todosasync 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 /todosasync 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)envoieGET /todosetPOST /todosvers des handlers différents. - Les codes de statut sont des valeurs de retour.
StatusCodevient du modulehttpd’axum. Un handler peut renvoyer unResult:Ok(Json(todo))devient un 200 avec du JSON, etErr(StatusCode::NOT_FOUND)devient un 404 vide. Un tuple(StatusCode::CREATED, Json(todo))fixe le statut et le corps. Renvoyer seulementStatusCode::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 unVecnormal. 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.
retaingarde 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 })None1 supprimée ? true1 supprimée à nouveau ? falserestantes : 1La 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.
Étape 6 : tester avec curl
Section intitulée « Étape 6 : tester avec curl »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 :
curl http://127.0.0.1:3000/todos[]Créez une tâche. Remarquez le statut 201 Created :
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 Createdcontent-type: application/jsoncontent-length: 47
{"id":1,"title":"Acheter du lait","done":false}Créez-en une deuxième et listez-les toutes :
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 :
curl http://127.0.0.1:3000/todos/2curl -i http://127.0.0.1:3000/todos/99{"id":2,"title":"Apprendre axum","done":false}HTTP/1.1 404 Not Foundcontent-length: 0Supprimez la tâche 1, puis essayez de la supprimer à nouveau :
curl -i -X DELETE http://127.0.0.1:3000/todos/1curl -i -X DELETE http://127.0.0.1:3000/todos/1HTTP/1.1 204 No Content
HTTP/1.1 404 Not Foundcontent-length: 0curl 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 :
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/2HTTP/1.1 422 Unprocessable Entitycontent-type: text/plain; charset=utf-8content-length: 130
Failed to deserialize the JSON body into the target type: title: invalid type: integer `42`, expected a string at line 1 column 12HTTP/1.1 405 Method Not Allowedallow: GET,HEAD,DELETEcontent-length: 0Ici, 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.
Erreurs courantes
Section intitulée « Erreurs courantes »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 messageQuand 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.
Idées pour aller plus loin
Section intitulée « Idées pour aller plus loin »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 aveciter_mut(), mettezdone = trueet renvoyez-la. - Validation. Un titre vide ne devrait pas être accepté. Un handler peut renvoyer
Result<(StatusCode, Json<Todo>), (StatusCode, String)>et répondreErr((StatusCode::BAD_REQUEST, String::from("le titre ne doit pas être vide"))). Utiliseztitle.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>etnext_id: u32à l’intérieur duMutex. - Persistance dans un fichier JSON. Dérivez aussi
DeserializesurTodo. Au démarrage, liseztodos.jsonavecstd::fs::read_to_stringetserde_json::from_str(ou partez d’une liste vide si le fichier n’existe pas). Après chaque modification, réécrivez la liste avecserde_json::to_string_prettyetstd::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 traceetcargo 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.1n’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 sur0.0.0.0. Beaucoup d’hébergeurs indiquent à votre programme quel port utiliser via une variable d’environnementPORT, que vous pouvez lire avecstd::env::var(voir Variables d’environnement et processus). Compilez aveccargo build --releasepour obtenir un serveur bien plus rapide. Le résultat est un seul fichier danstarget/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.
Récapitulatif
Section intitulée « Récapitulatif »- Un handler est une
async fn. Un router associe une méthode et un chemin à un handler.axum::servele fait tourner sur unTcpListenerdetokio. - Les extracteurs tirent des données de la requête :
Pathpour des parties de l’URL (/todos/{id}),Jsonpour le corps (toujours en dernier), etStatepour les données partagées. - Renvoyez
Json(value)pour du JSON,StatusCodepour un statut, un tuple pour fixer les deux, ou unResultpour 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:-iaffiche le statut,-Xchoisit la méthode,-Hajoute un en-tête et-denvoie un corps.
Petit quiz
Section intitulée « Petit quiz »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
Pour aller plus loin
Section intitulée « Pour aller plus loin »- Documentation d’axum — docs.rs (en anglais) : le guide officiel, avec des sections sur le routage, les handlers et les réponses.
- Les extracteurs d’axum — docs.rs (en anglais) : tous les extracteurs intégrés et les règles sur leur ordre.
- Tutoriel Tokio (en anglais) : découvrez le runtime asynchrone qui fait tourner axum, y compris l’état partagé.
- Documentation de tower-http — docs.rs (en anglais) : des couches prêtes à l’emploi pour les logs, le CORS, la compression et les délais d’expiration.
- Codes de réponse HTTP — MDN : ce que signifient 200, 201, 204, 404, 422 et les autres.