Progetto: API web con Axum in Rust
In questo progetto costruisci un vero server web: una piccola API per le cose da fare (to-do). Altri programmi (un sito, un’app per il telefono, o semplicemente curl nel tuo terminale) le mandano richieste HTTP, e lei risponde con del JSON. È lo stesso genere di cosa che fa girare il «back end» della maggior parte delle app che usi.
Useremo axum, un framework web popolare e amichevole creato dal team di Tokio. Si appoggia su Rust asincrono e su serde, quindi questo progetto mette insieme tante cose che hai imparato: struct, Result, smart pointer e stato condiviso tra thread. Se hai fatto la lista di cose da fare (CLI), riconoscerai l’idea. Questa volta la lista vive dietro un indirizzo web.
Cosa costruirai
Sezione intitolata “Cosa costruirai”Alla fine, il tuo server risponderà a queste richieste:
| Metodo e percorso | Cosa fa | Stato in caso di successo |
|---|---|---|
GET /todos |
Elenca tutte le cose da fare | 200 OK |
GET /todos/{id} |
Restituisce una cosa da fare | 200 OK, oppure 404 Not Found |
POST /todos |
Crea una cosa da fare a partire da JSON | 201 Created |
DELETE /todos/{id} |
Elimina una cosa da fare | 204 No Content, oppure 404 Not Found |
In JSON, una cosa da fare ha questo aspetto:
{"id":1,"title":"Buy milk","done":false}Una nota importante: un server web aspetta richieste all’infinito e ha bisogno di una porta di rete, quindi non può girare nel Rust Playground. Il codice del server in questa pagina è pensato per il tuo computer, con cargo run. Ogni passo qui sotto è stato compilato ed eseguito, e le risposte mostrate sono reali (per questo i dati di esempio restano in inglese, esattamente come li ha restituiti il server). Dove una parte del programma può girare nel Playground, trovi un pulsante «Provalo tu».
Preparare il progetto
Sezione intitolata “Preparare il progetto”Crea un nuovo progetto e aggiungi quattro crate:
cargo new todo-apicd todo-apicargo add serde --features derivecargo add serde_jsonIl tuo Cargo.toml ora contiene qualcosa del genere (i numeri di versione esatti potrebbero essere più recenti):
[dependencies]axum = "0.8"serde = { version = "1", features = ["derive"] }serde_json = "1"tokio = { version = "1", features = ["full"] }A cosa serve ognuno:
- axum gestisce l’HTTP: legge le richieste e invia le risposte.
- tokio è il runtime asincrono che fa girare tutto. Vedi Async/await.
- serde e serde_json trasformano le tue struct in JSON e viceversa. Vedi JSON con Serde.
Ti serve anche curl per mandare le richieste. È incluso in macOS, Linux e Windows 10 e successivi. (Su Windows scrivi curl.exe in PowerShell, perché lì curl da solo significa un’altra cosa.)
Passo 1: Hello, world!
Sezione intitolata “Passo 1: Hello, world!”Sostituisci src/main.rs con questo:
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!("Listening on http://127.0.0.1:3000"); axum::serve(listener, app).await.unwrap();}
async fn hello() -> &'static str { "Hello, world!"}Qui lavorano insieme tre pezzi:
- Un handler è una
async fnche risponde a una richiesta.hellorestituisce un testo, e axum lo rimanda indietro come risposta. - Il router collega i percorsi agli handler.
.route("/", get(hello))significa «quando qualcuno manda una richiestaGETa/, chiamahello». - Il listener apre la porta 3000 sul tuo computer (
127.0.0.1), eaxum::serveinizia a rispondere alle richieste. Gira finché non lo fermi.
Eseguilo con cargo run. Il programma stampa una riga e poi aspetta:
Listening on http://127.0.0.1:3000Lascialo girare, apri un secondo terminale e mandagli una richiesta:
curl http://127.0.0.1:3000/Hello, world!Il server non aggiunge un a capo dopo il testo, quindi il prompt del terminale potrebbe comparire subito dopo Hello, world!. È normale. In questa pagina ogni risposta è mostrata su una riga a sé. Puoi anche aprire http://127.0.0.1:3000/ nel browser. Premi Ctrl + C nel primo terminale per fermare il server. Devi fermarlo e riavviarlo ogni volta che cambi il codice.
Passo 2: restituire JSON da una struct
Sezione intitolata “Passo 2: restituire JSON da una struct”Di solito le API rispondono in JSON. In axum avvolgi in Json(...) qualsiasi valore che implementa Serialize di serde, e axum lo trasforma in JSON e imposta l’header content-type giusto:
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!("Listening on http://127.0.0.1:3000"); axum::serve(listener, app).await.unwrap();}
async fn hello() -> &'static str { "Hello, world!"}
async fn example_todo() -> Json<Todo> { let todo = Todo { id: 1, title: String::from("Learn axum"), done: false, }; Json(todo)}Il flag -i dice a curl di mostrare anche la riga di stato e gli header:
curl -i http://127.0.0.1:3000/exampleHTTP/1.1 200 OKcontent-type: application/jsoncontent-length: 42
{"id":1,"title":"Learn axum","done":false}(curl stampa anche un header date, che in questa pagina omettiamo.) Le chiavi JSON sono esattamente i nomi dei campi della struct. Puoi controllare la forma di quel JSON nel Playground, senza server, perché è serde puro. Questa è anche la struct NewTodo che useremo al passo 4 per i dati in arrivo:
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("Learn axum"), done: false }; println!("{}", serde_json::to_string(&todo).unwrap());
let list = vec![todo.clone(), Todo { id: 2, title: String::from("Buy milk"), done: true }]; println!("{}", serde_json::to_string(&list).unwrap());
let input: NewTodo = serde_json::from_str(r#"{"title":"Walk the dog"}"#).unwrap(); println!("{:?}", input);
let bad = serde_json::from_str::<NewTodo>(r#"{"name":"Walk the dog"}"#); println!("{}", bad.unwrap_err());}Output:
{"id":1,"title":"Learn axum","done":false}[{"id":1,"title":"Learn axum","done":false},{"id":2,"title":"Buy milk","done":true}]NewTodo { title: "Walk the dog" }missing field `title` at line 1 column 23L’ultima riga è l’errore di serde (in inglese): manca il campo title alla riga 1, colonna 23. Un Vec<Todo> diventa un array JSON. È esattamente quello che restituirà GET /todos.
Passo 3: parametri nel percorso
Sezione intitolata “Passo 3: parametri nel percorso”In un percorso come /todos/2, il 2 cambia da una richiesta all’altra. In axum 0.8 segni quella parte della route con le parentesi graffe, {id}, e la leggi nell’handler con l’extractor 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!("Listening on http://127.0.0.1:3000"); axum::serve(listener, app).await.unwrap();}
async fn hello_name(Path(name): Path<String>) -> String { format!("Hello, {name}!")}
async fn add(Path((a, b)): Path<(i32, i32)>) -> String { format!("{a} + {b} = {}", a + b)}Un extractor è un parametro dell’handler che tira fuori un pezzo della richiesta. Path(name): Path<String> è un pattern: scarta il Path e ti dà la String che c’è dentro. Con due parametri ottieni una tupla.
curl http://127.0.0.1:3000/hello/Anacurl http://127.0.0.1:3000/add/2/3Hello, Ana!2 + 3 = 5Ecco una bella sorpresa. Hai detto che a e b sono i32, quindi axum lo controlla per te. Manda qualcosa che non è un numero:
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`Il messaggio dice che il valore x in posizione 1 non si può convertire in i32. Il tuo handler non è nemmeno partito. I tipi nella firma della funzione sono la validazione.
Passo 4: accettare JSON con POST
Sezione intitolata “Passo 4: accettare JSON con POST”Per creare una cosa da fare, il client manda del JSON nel corpo (body) della richiesta. L’extractor Json legge il corpo e lo deserializza nella tua struct. Il client manda solo un titolo, quindi per l’input usiamo una struct separata, NewTodo:
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!("Listening on 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)}Nella route usa post(...) invece di get(...). Con curl, -X POST imposta il metodo, -H aggiunge un header e -d manda il corpo:
curl -X POST http://127.0.0.1:3000/echo -H "Content-Type: application/json" -d '{"title":"Buy milk"}'{"id":1,"title":"Buy milk","done":false}Di nuovo, axum controlla l’input prima che parta il tuo codice. Se al JSON manca title, ottieni 422 Unprocessable Entity:
curl -i -X POST http://127.0.0.1:3000/echo -H "Content-Type: application/json" -d '{"name":"Buy milk"}'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 19E se ti dimentichi l’header Content-Type, ottieni 415 Unsupported Media Type con il messaggio Expected request with `Content-Type: application/json` . Quell’header è la cosa che si dimentica più spesso quando si fanno prove con curl.
Passo 5: stato condiviso per una vera API
Sezione intitolata “Passo 5: stato condiviso per una vera API”Finora ogni handler si inventava i suoi dati. Una vera API deve ricordarsi le cose da fare tra una richiesta e l’altra. Le richieste possono arrivare contemporaneamente e vengono gestite su thread diversi, quindi la lista deve essere sicura da condividere. È il lavoro della coppia che hai conosciuto nella pagina Concorrenza:
Mutex<Vec<Todo>>fa in modo che una sola richiesta alla volta modifichi la lista.Arc<...>permette a tanti handler di possedere un puntatore a quella stessa lista (vedi Smart pointer).
Diamo al tipo un nome breve con type Db = Arc<Mutex<Vec<Todo>>>;, lo passiamo al router con .with_state(db) e lo leggiamo negli handler con l’extractor State. axum clona l’Arc per ogni richiesta (costa poco: copia solo un puntatore).
Ecco il programma completo. Sostituisci src/main.rs con questo:
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,}
// Il «database»: una lista di cose da fare che ogni richiesta può condividere.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!("Listening on 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 }}Vediamo le idee nuove:
- Un percorso, più metodi.
get(list_todos).post(create_todo)mandaGET /todosePOST /todosa handler diversi. - I codici di stato sono valori restituiti.
StatusCodeviene dal modulohttpdi axum. Un handler può restituire unResult:Ok(Json(todo))diventa un 200 con JSON, edErr(StatusCode::NOT_FOUND)diventa un 404 vuoto. Una tupla(StatusCode::CREATED, Json(todo))imposta lo stato e il corpo. Restituire soloStatusCode::NO_CONTENT(204) significa «fatto, niente da rimandare». lock()e la guardia.db.lock().unwrap()aspetta che nessun’altra richiesta stia usando la lista, poi ti dà una guardia (guard) che puoi leggere e modificare come un normaleVec. Il lock viene rilasciato quando la guardia esce dallo scope alla fine della funzione.- Nuovi id. Il prossimo id è uno in più dell’id più grande nella lista, oppure 1 se la lista è vuota.
- Eliminare.
retaintiene tutte le cose da fare il cui id non corrisponde. Se la lunghezza non è cambiata, non è stato eliminato niente, quindi restituiamo 404.
Perché std::sync::Mutex e non uno asincrono? Va bene (ed è più veloce) finché non tieni il lock attraverso un .await. Nessuno dei nostri handler lo fa: prendono il lock, lavorano con la lista e restituiscono il risultato.
La logica di lista e lock non ha bisogno di un server web, quindi puoi provarla nel Playground. Queste funzioni fanno lo stesso lavoro degli handler, senza 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); // quello che fa axum per ogni richiesta
println!("{:?}", create(&db, "Buy milk")); println!("{:?}", create(&second_handle, "Learn axum")); println!("{:?}", get(&db, 2)); println!("{:?}", get(&db, 99)); println!("eliminato 1? {}", delete(&db, 1)); println!("eliminato di nuovo 1? {}", delete(&db, 1)); println!("rimasti: {}", db.lock().unwrap().len());}Output:
Todo { id: 1, title: "Buy milk", done: false }Todo { id: 2, title: "Learn axum", done: false }Some(Todo { id: 2, title: "Learn axum", done: false })Noneeliminato 1? trueeliminato di nuovo 1? falserimasti: 1La cosa da fare creata tramite second_handle compare quando leggi tramite db, perché entrambi gli Arc puntano alla stessa lista.
Passo 6: provalo con curl
Sezione intitolata “Passo 6: provalo con curl”Avvia il server con cargo run e, in un secondo terminale, prova ogni endpoint. All’inizio la lista è vuota:
curl http://127.0.0.1:3000/todos[]Crea una cosa da fare. Nota lo stato 201 Created:
curl -i -X POST http://127.0.0.1:3000/todos -H "Content-Type: application/json" -d '{"title":"Buy milk"}'HTTP/1.1 201 Createdcontent-type: application/jsoncontent-length: 40
{"id":1,"title":"Buy milk","done":false}Creane una seconda ed elencale tutte:
curl -X POST http://127.0.0.1:3000/todos -H "Content-Type: application/json" -d '{"title":"Learn axum"}'curl http://127.0.0.1:3000/todos{"id":2,"title":"Learn axum","done":false}[{"id":1,"title":"Buy milk","done":false},{"id":2,"title":"Learn axum","done":false}]Chiedine una per id, e chiedine una che non esiste:
curl http://127.0.0.1:3000/todos/2curl -i http://127.0.0.1:3000/todos/99{"id":2,"title":"Learn axum","done":false}HTTP/1.1 404 Not Foundcontent-length: 0Elimina la cosa da fare 1, poi prova a eliminarla di nuovo:
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":"Learn axum","done":false}]axum gestisce anche alcuni errori al posto tuo. Un tipo sbagliato nel JSON dà un 422 che spiega il problema, e un metodo che non hai previsto per un percorso dà 405 Method Not Allowed, con un header che elenca i metodi che funzionano:
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: 0Il primo messaggio dice che title doveva essere una stringa e invece è arrivato il numero intero 42. Ferma il server con Ctrl + C e riavvialo: la lista è vuota. I dati vivono solo in memoria. Salvarli è la prima idea qui sotto.
Errori comuni
Sezione intitolata “Errori comuni”Usare la vecchia sintassi :id per le route. Molti tutorial online sono stati scritti per axum 0.7, che usava /todos/:id. In axum 0.8 questo compila, ma il programma va in panic appena parte:
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.Il messaggio dice che i segmenti del percorso non devono iniziare con : e che per catturare un valore devi usare {capture}. La soluzione: scrivi /todos/{id}.
Mettere Json prima degli altri extractor. Il corpo della richiesta si può leggere una volta sola, quindi l’extractor che lo legge (Json) deve essere l’ultimo parametro. Se scrivi create_todo(Json(input): Json<NewTodo>, State(db): State<Db>), l’errore non lo dice in modo diretto. Le righe importanti sono:
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 messageQuando vedi «Handler is not satisfied» (il vincolo di trait Handler non è soddisfatto), controlla l’ordine dei parametri, e controlla che ogni tipo restituito sia qualcosa che axum sa trasformare in una risposta.
Dimenticare -H "Content-Type: application/json" in curl. Il server risponde 415. L’header è obbligatorio per l’extractor Json.
Dimenticare di riavviare. Il server in esecuzione è il vecchio programma. Fermalo con Ctrl + C e lancia di nuovo cargo run dopo ogni modifica.
Idee per ampliarlo
Sezione intitolata “Idee per ampliarlo”Quando l’API di base funziona, ecco dei buoni passi successivi. Provali uno alla volta.
- Segnare una cosa da fare come completata. Aggiungi
PATCH /todos/{id}con.patch(...)sulla seconda route. Trova la cosa da fare coniter_mut(), impostadone = truee restituiscila. - Validazione. Un titolo vuoto non dovrebbe essere ammesso. Un handler può restituire
Result<(StatusCode, Json<Todo>), (StatusCode, String)>, e rimandareErr((StatusCode::BAD_REQUEST, String::from("il titolo non può essere vuoto"))). Per controllare, usatitle.trim().is_empty(). - Id che non si ripetono mai. Con «id più grande più uno», eliminare la cosa da fare più recente permette di riusare il suo id. Tieni invece un contatore separato nel tuo stato, per esempio una struct con
todos: Vec<Todo>enext_id: u32dentro ilMutex. - Persistenza con un file JSON. Fai derivare a
TodoancheDeserialize. All’avvio, leggitodos.jsonconstd::fs::read_to_stringeserde_json::from_str(oppure parti vuoto se il file non esiste). Dopo ogni modifica, riscrivi la lista conserde_json::to_string_prettyestd::fs::write. Le pagine I/O su file e JSON con Serde hanno tutto quello che ti serve. Più avanti puoi sostituire il file con un vero database come SQLite (vedi Database). - Log con tower-http. Esegui
cargo add tower-http --features traceecargo add tracing-subscriber, poi aggiungi uno strato (layer) di logging. Ora il server stampa una riga per ogni richiesta, con metodo, percorso, stato e quanto tempo ha impiegato:
use tower_http::trace::TraceLayer;
// all'inizio di main:tracing_subscriber::fmt() .with_max_level(tracing_subscriber::filter::LevelFilter::DEBUG) .init();
// quando costruisci il router: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);- Note sul deploy.
127.0.0.1accetta richieste solo dal tuo computer. Per accettare richieste da altre macchine (per esempio dentro Docker o su un server), fai il bind su0.0.0.0. Molti servizi di hosting dicono al tuo programma quale porta usare tramite una variabile d’ambientePORT, che puoi leggere constd::env::var(vedi Ambiente e processi). Compila concargo build --releaseper un server molto più veloce. Il risultato è un singolo file intarget/release/che puoi copiare su un server Linux, o mettere in una piccola immagine Docker. Per un sito pubblico, mettilo dietro un reverse proxy (come Caddy o nginx) che gestisca l’HTTPS.
Riepilogo
Sezione intitolata “Riepilogo”- Un handler è una
async fn. Un router associa un metodo e un percorso a un handler.axum::servelo fa girare su unTcpListenerditokio. - Gli extractor prendono dati dalla richiesta:
Pathper parti dell’URL (/todos/{id}),Jsonper il corpo (sempre per ultimo) eStateper i dati condivisi. - Restituisci
Json(valore)per il JSON,StatusCodeper uno stato, una tupla per impostarli entrambi, oppure unResultper scegliere tra successo e uno stato di errore. Arc<Mutex<Vec<Todo>>>condivide in sicurezza una lista tra tutte le richieste. Prendi il lock, usalo e lascia che la guardia venga rilasciata. Non tenerlo attraverso un.await.- Prova la tua API con
curl:-imostra lo stato,-Ximposta il metodo,-Haggiunge un header e-dmanda un corpo.
Verifica veloce
Sezione intitolata “Verifica veloce”Verifica veloce
1. In axum 0.8, come scrivi una route con un id nel percorso?
2. Un handler restituisce (StatusCode::CREATED, Json(todo)). Cosa riceve il client?
3. Perché la lista condivisa è avvolta in Arc<Mutex<...>>?
4. Quale extractor deve venire per ultimo tra i parametri di un handler?
5. Fai curl -X POST con del JSON ma dimentichi l'header Content-Type: application/json. Cosa risponde axum?
Punteggio: 0 / 5
Per approfondire
Sezione intitolata “Per approfondire”- Documentazione di axum — docs.rs (in inglese): la guida ufficiale, con sezioni su routing, handler e risposte.
- Gli extractor di axum — docs.rs (in inglese): tutti gli extractor integrati e le regole sul loro ordine.
- Tutorial di Tokio (in inglese): impara il runtime asincrono che fa girare axum, compreso lo stato condiviso.
- Documentazione di tower-http — docs.rs (in inglese): strati pronti per logging, CORS, compressione e timeout.
- HTTP response status codes — MDN (in inglese): cosa significano 200, 201, 204, 404, 422 e gli altri.