Projekt: Web-API mit Axum in Rust
In diesem Projekt baust du einen echten Webserver: eine kleine To-do-API. Andere Programme (eine Website, eine Handy-App oder einfach curl in deinem Terminal) schicken ihr HTTP-Anfragen, und sie antwortet mit JSON. Auf genau so etwas läuft das „Backend“ der meisten Apps, die du benutzt.
Wir nehmen axum, ein beliebtes, freundliches Web-Framework vom Tokio-Team. Es baut auf asynchronem Rust und serde auf. Dieses Projekt verbindet also vieles, was du schon gelernt hast: Structs, Result, Smart Pointer und gemeinsam genutzte Daten zwischen Threads. Wenn du die To-do-Liste fürs Terminal gebaut hast, kennst du die Idee schon. Diesmal steckt die To-do-Liste hinter einer Webadresse.
Was du baust
Abschnitt betitelt „Was du baust“Wenn du fertig bist, beantwortet dein Server diese Anfragen:
| Methode und Pfad | Was sie macht | Status bei Erfolg |
|---|---|---|
GET /todos |
Alle To-dos auflisten | 200 OK |
GET /todos/{id} |
Ein To-do holen | 200 OK oder 404 Not Found |
POST /todos |
Ein To-do aus JSON anlegen | 201 Created |
DELETE /todos/{id} |
Ein To-do löschen | 204 No Content oder 404 Not Found |
Ein To-do sieht in JSON so aus:
{"id":1,"title":"Milch kaufen","done":false}Ein wichtiger Hinweis: Ein Webserver wartet für immer auf Anfragen und braucht einen Netzwerk-Port. Deshalb kann er nicht im Rust Playground laufen. Der Servercode auf dieser Seite ist für deinen eigenen Computer gedacht, mit cargo run. Jeder Schritt unten wurde kompiliert und ausgeführt, und die gezeigten Antworten sind echt. Wo ein Teil des Programms doch im Playground laufen kann, findest du einen „Selbst ausprobieren“-Button.
Das Projekt einrichten
Abschnitt betitelt „Das Projekt einrichten“Leg ein neues Projekt an und füge vier Crates hinzu:
cargo new todo-apicd todo-apicargo add serde --features derivecargo add serde_jsonIn deiner Cargo.toml steht jetzt ungefähr das hier (die genauen Versionsnummern können neuer sein):
[dependencies]axum = "0.8"serde = { version = "1", features = ["derive"] }serde_json = "1"tokio = { version = "1", features = ["full"] }Wofür die Crates da sind:
- axum kümmert sich um HTTP: Es liest Anfragen und schickt Antworten.
- tokio ist die asynchrone Laufzeitumgebung (Runtime), die alles ausführt. Siehe Async/Await.
- serde und serde_json machen aus deinen Structs JSON und wieder zurück. Siehe JSON mit Serde.
Außerdem brauchst du curl, um Anfragen zu schicken. Es ist bei macOS, Linux und Windows 10 und neuer dabei. (Unter Windows tippst du in der PowerShell curl.exe, denn ein einfaches curl bedeutet dort etwas anderes.)
Schritt 1: Hallo, Welt!
Abschnitt betitelt „Schritt 1: Hallo, Welt!“Ersetze src/main.rs durch Folgendes:
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!("Server läuft auf http://127.0.0.1:3000"); axum::serve(listener, app).await.unwrap();}
async fn hello() -> &'static str { "Hallo, Welt!"}Hier arbeiten drei Teile zusammen:
- Ein Handler ist eine
async fn, die eine Anfrage beantwortet.hellogibt etwas Text zurück, und axum schickt ihn als Antwort zurück. - Der Router verbindet Pfade mit Handlern.
.route("/", get(hello))bedeutet: „Wenn jemand eineGET-Anfrage an/schickt, rufhelloauf.“ - Der Listener öffnet Port 3000 auf deinem eigenen Computer (
127.0.0.1), undaxum::servebeginnt, Anfragen zu beantworten. Das läuft, bis du es stoppst.
Starte das Programm mit cargo run. Es gibt eine Zeile aus und wartet dann:
Server läuft auf http://127.0.0.1:3000Lass es laufen, öffne ein zweites Terminal und schick ihm eine Anfrage:
curl http://127.0.0.1:3000/Hallo, Welt!Der Server hängt keinen Zeilenumbruch an den Text an, deshalb erscheint dein Terminal-Prompt vielleicht direkt hinter Hallo, Welt!. Das ist normal. Auf dieser Seite steht jede Antwort in einer eigenen Zeile. Du kannst http://127.0.0.1:3000/ auch in deinem Browser öffnen. Drück im ersten Terminal Strg + C, um den Server zu stoppen. Nach jeder Änderung am Code musst du ihn stoppen und neu starten.
Schritt 2: JSON aus einem Struct zurückgeben
Abschnitt betitelt „Schritt 2: JSON aus einem Struct zurückgeben“APIs antworten normalerweise mit JSON. In axum packst du jeden Wert, der serdes Serialize implementiert, in Json(...). axum macht daraus JSON und setzt den richtigen content-type-Header:
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!("Server läuft auf http://127.0.0.1:3000"); axum::serve(listener, app).await.unwrap();}
async fn hello() -> &'static str { "Hallo, Welt!"}
async fn example_todo() -> Json<Todo> { let todo = Todo { id: 1, title: String::from("axum lernen"), done: false, }; Json(todo)}Die Option -i sagt curl, dass es auch die Statuszeile und die Header zeigen soll:
curl -i http://127.0.0.1:3000/exampleHTTP/1.1 200 OKcontent-type: application/jsoncontent-length: 43
{"id":1,"title":"axum lernen","done":false}(curl zeigt außerdem einen date-Header, den wir auf dieser Seite weglassen.) Die JSON-Keys sind genau die Feldnamen des Structs. Diese JSON-Form kannst du im Playground prüfen, ganz ohne Server, denn das ist reines serde. Hier siehst du auch das Struct NewTodo, das wir in Schritt 4 für eingehende Daten benutzen:
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("axum lernen"), done: false }; println!("{}", serde_json::to_string(&todo).unwrap());
let list = vec![todo.clone(), Todo { id: 2, title: String::from("Milch kaufen"), done: true }]; println!("{}", serde_json::to_string(&list).unwrap());
let input: NewTodo = serde_json::from_str(r#"{"title":"Gassi gehen"}"#).unwrap(); println!("{:?}", input);
let bad = serde_json::from_str::<NewTodo>(r#"{"name":"Gassi gehen"}"#); println!("{}", bad.unwrap_err());}Ausgabe:
{"id":1,"title":"axum lernen","done":false}[{"id":1,"title":"axum lernen","done":false},{"id":2,"title":"Milch kaufen","done":true}]NewTodo { title: "Gassi gehen" }missing field `title` at line 1 column 22Ein Vec<Todo> wird zu einem JSON-Array. Genau das wird GET /todos zurückgeben. Die letzte Zeile ist die Fehlermeldung von serde_json (auf Englisch): Das Feld title fehlt, entdeckt in Zeile 1, Spalte 22.
Schritt 3: Pfadparameter
Abschnitt betitelt „Schritt 3: Pfadparameter“In einem Pfad wie /todos/2 ändert sich die 2 von Anfrage zu Anfrage. In axum 0.8 markierst du diesen Teil der Route mit geschweiften Klammern, {id}, und liest ihn im Handler mit dem Extractor Path aus:
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!("Server läuft auf http://127.0.0.1:3000"); axum::serve(listener, app).await.unwrap();}
async fn hello_name(Path(name): Path<String>) -> String { format!("Hallo, {name}!")}
async fn add(Path((a, b)): Path<(i32, i32)>) -> String { format!("{a} + {b} = {}", a + b)}Ein Extractor ist ein Handler-Parameter, der ein Stück aus der Anfrage herausholt. Path(name): Path<String> ist ein Pattern: Es packt den Path aus und gibt dir den String darin. Bei zwei Parametern bekommst du ein Tupel.
curl http://127.0.0.1:3000/hello/Anacurl http://127.0.0.1:3000/add/2/3Hallo, Ana!2 + 3 = 5Jetzt kommt eine schöne Überraschung. Du hast gesagt, dass a und b vom Typ i32 sind, also prüft axum das für dich. Schick etwas, das keine Zahl ist:
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`Die Meldung kommt von axum: Der Wert x an Position 1 lässt sich nicht in ein i32 umwandeln. Dein Handler ist nicht einmal gelaufen. Die Typen in deiner Funktionssignatur sind die Validierung.
Schritt 4: JSON mit POST annehmen
Abschnitt betitelt „Schritt 4: JSON mit POST annehmen“Um ein To-do anzulegen, schickt der Client JSON im Body (Rumpf) der Anfrage. Der Extractor Json liest den Body und deserialisiert ihn in dein Struct. Der Client schickt nur einen Titel, deshalb benutzen wir für die Eingabe ein eigenes Struct 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!("Server läuft auf 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)}In der Route benutzt du post(...) statt get(...). Bei curl setzt -X POST die Methode, -H fügt einen Header hinzu, und -d schickt den Body:
curl -X POST http://127.0.0.1:3000/echo -H "Content-Type: application/json" -d '{"title":"Milch kaufen"}'{"id":1,"title":"Milch kaufen","done":false}Auch hier prüft axum die Eingabe, bevor dein Code läuft. Fehlt im JSON das Feld title, bekommst du 422 Unprocessable Entity:
curl -i -X POST http://127.0.0.1:3000/echo -H "Content-Type: application/json" -d '{"name":"Milch kaufen"}'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 23Die Meldung heißt: Der JSON-Body ließ sich nicht in den Zieltyp umwandeln, weil das Feld title fehlt. Und wenn du den Header Content-Type vergisst, bekommst du 415 Unsupported Media Type mit der Meldung Expected request with `Content-Type: application/json` . Diesen Header vergisst man beim Testen mit curl am häufigsten.
Schritt 5: Shared State für eine echte To-do-API
Abschnitt betitelt „Schritt 5: Shared State für eine echte To-do-API“Bis jetzt hat sich jeder Handler seine Daten selbst ausgedacht. Eine echte API muss sich To-dos zwischen Anfragen merken. Anfragen können gleichzeitig ankommen und werden auf verschiedenen Threads bearbeitet. Die Liste muss also sicher teilbar sein. Das ist die Aufgabe des Paars, das du auf der Seite Nebenläufigkeit kennengelernt hast:
Mutex<Vec<Todo>>sorgt dafür, dass immer nur eine Anfrage die Liste ändert.Arc<...>erlaubt, dass viele Handler einen Zeiger auf dieselbe Liste besitzen (siehe Smart Pointer).
Wir geben dem Typ mit type Db = Arc<Mutex<Vec<Todo>>>; einen kurzen Namen, übergeben ihn mit .with_state(db) an den Router und lesen ihn in Handlern mit dem Extractor State. axum klont den Arc für jede Anfrage (das ist billig: Es kopiert nur einen Zeiger).
Hier ist das vollständige Programm. Ersetze src/main.rs damit:
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,}
// Die „Datenbank“: eine Liste von To-dos, die sich alle Anfragen teilen.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!("Server läuft auf 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 }}Gehen wir die neuen Ideen durch:
- Ein Pfad, mehrere Methoden.
get(list_todos).post(create_todo)schicktGET /todosundPOST /todosan verschiedene Handler. - Statuscodes sind Rückgabewerte.
StatusCodestammt aus dem Modulhttpvon axum. Ein Handler kann einResultzurückgeben: AusOk(Json(todo))wird 200 mit JSON, ausErr(StatusCode::NOT_FOUND)ein leeres 404. Ein Tupel(StatusCode::CREATED, Json(todo))setzt den Status und den Body. Gibst du nurStatusCode::NO_CONTENT(204) zurück, heißt das „erledigt, nichts zurückzuschicken“. lock()und der Guard.db.lock().unwrap()wartet, bis keine andere Anfrage die Liste benutzt. Dann bekommst du einen Guard, den du wie einen normalenVeclesen und ändern kannst. Die Sperre wird freigegeben, wenn der Guard am Ende der Funktion seinen Gültigkeitsbereich (Scope) verlässt.- Neue IDs. Die nächste ID ist um eins größer als die größte ID in der Liste, bei einer leeren Liste ist sie 1.
- Löschen.
retainbehält jedes To-do, dessen ID nicht passt. Hat sich die Länge nicht geändert, wurde nichts gelöscht, und wir geben 404 zurück.
Warum std::sync::Mutex und kein asynchroner Mutex? Der ist in Ordnung (und schneller), solange du die Sperre nicht über ein .await hinweg hältst. Das macht keiner unserer Handler: Sie sperren, arbeiten mit der Liste und kehren zurück.
Die Logik mit Liste und Sperre braucht keinen Webserver, also kannst du sie im Playground ausprobieren. Diese Funktionen machen dieselbe Arbeit wie die Handler, nur ohne 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); // das macht axum für jede Anfrage
println!("{:?}", create(&db, "Milch kaufen")); println!("{:?}", create(&second_handle, "axum lernen")); println!("{:?}", get(&db, 2)); println!("{:?}", get(&db, 99)); println!("1 gelöscht? {}", delete(&db, 1)); println!("1 noch einmal gelöscht? {}", delete(&db, 1)); println!("übrig: {}", db.lock().unwrap().len());}Ausgabe:
Todo { id: 1, title: "Milch kaufen", done: false }Todo { id: 2, title: "axum lernen", done: false }Some(Todo { id: 2, title: "axum lernen", done: false })None1 gelöscht? true1 noch einmal gelöscht? falseübrig: 1Das To-do, das über second_handle angelegt wurde, taucht auf, wenn du über db liest, denn beide Arcs zeigen auf dieselbe Liste.
Schritt 6: Mit curl testen
Abschnitt betitelt „Schritt 6: Mit curl testen“Starte den Server mit cargo run und probier in einem zweiten Terminal jeden Endpunkt aus. Am Anfang ist die Liste leer:
curl http://127.0.0.1:3000/todos[]Leg ein To-do an. Achte auf den Status 201 Created:
curl -i -X POST http://127.0.0.1:3000/todos -H "Content-Type: application/json" -d '{"title":"Milch kaufen"}'HTTP/1.1 201 Createdcontent-type: application/jsoncontent-length: 44
{"id":1,"title":"Milch kaufen","done":false}Leg ein zweites an und lass dir alle auflisten:
curl -X POST http://127.0.0.1:3000/todos -H "Content-Type: application/json" -d '{"title":"axum lernen"}'curl http://127.0.0.1:3000/todos{"id":2,"title":"axum lernen","done":false}[{"id":1,"title":"Milch kaufen","done":false},{"id":2,"title":"axum lernen","done":false}]Hol eins über seine ID und frag nach einem, das es nicht gibt:
curl http://127.0.0.1:3000/todos/2curl -i http://127.0.0.1:3000/todos/99{"id":2,"title":"axum lernen","done":false}HTTP/1.1 404 Not Foundcontent-length: 0Lösch To-do 1 und versuch dann, es noch einmal zu löschen:
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":"axum lernen","done":false}]Ein paar Fehler fängt axum auch selbst ab. Ein falscher Typ im JSON ergibt ein 422, das das Problem erklärt. Und eine Methode, die du für einen Pfad nicht eingerichtet hast, ergibt 405 Method Not Allowed, mit einem Header, der die funktionierenden Methoden auflistet:
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: 0Die erste Meldung sagt: Bei title steht eine Zahl (42), erwartet wurde aber ein String. Stopp den Server mit Strg + C und starte ihn neu: Die Liste ist leer. Die Daten leben nur im Arbeitsspeicher. Sie zu speichern ist die erste Idee weiter unten.
Häufige Fehler
Abschnitt betitelt „Häufige Fehler“Die alte Routen-Syntax :id benutzen. Viele Tutorials im Netz wurden für axum 0.7 geschrieben, das /todos/:id benutzt hat. In axum 0.8 kompiliert das zwar, aber das Programm löst gleich beim Start eine Panic aus:
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.Die Meldung sagt: Pfadsegmente dürfen nicht mit : beginnen, für Platzhalter nimmst du {capture}. Die Lösung: Schreib /todos/{id}.
Json vor andere Extractors stellen. Der Body einer Anfrage lässt sich nur einmal lesen. Deshalb muss der Extractor, der ihn liest (Json), der letzte Parameter sein. Wenn du create_todo(Json(input): Json<NewTodo>, State(db): State<Db>) schreibst, sagt dir der Fehler das nicht direkt. Die wichtigen Zeilen sind:
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 messageWenn du „Handler is not satisfied“ siehst, prüf die Reihenfolge deiner Parameter. Und prüf, ob axum jeden Rückgabetyp in eine Antwort umwandeln kann. Der Hinweis am Ende schlägt #[axum::debug_handler] vor: Damit bekommst du eine verständlichere Fehlermeldung.
-H "Content-Type: application/json" bei curl vergessen. Der Server antwortet mit 415. Der Extractor Json braucht diesen Header.
Den Neustart vergessen. Der laufende Server ist noch das alte Programm. Stopp ihn nach jeder Änderung mit Strg + C und führ wieder cargo run aus.
Ideen zum Erweitern
Abschnitt betitelt „Ideen zum Erweitern“Sobald die einfache API funktioniert, sind das gute nächste Schritte. Probier sie einzeln aus.
- Ein To-do als erledigt markieren. Füge
PATCH /todos/{id}mit.patch(...)an der zweiten Route hinzu. Such das To-do mititer_mut(), setzdone = trueund gib es zurück. - Validierung. Ein leerer Titel sollte nicht erlaubt sein. Ein Handler kann
Result<(StatusCode, Json<Todo>), (StatusCode, String)>zurückgeben undErr((StatusCode::BAD_REQUEST, String::from("Titel darf nicht leer sein")))schicken. Prüf das mittitle.trim().is_empty(). - IDs, die sich nie wiederholen. Mit „größte ID plus eins“ kann die ID des neuesten To-dos nach dem Löschen erneut vergeben werden. Halte stattdessen einen eigenen Zähler in deinem State, zum Beispiel ein Struct mit
todos: Vec<Todo>undnext_id: u32imMutex. - Mit einer JSON-Datei speichern. Leite auch
DeserializefürTodoab. Lies beim Starttodos.jsonmitstd::fs::read_to_stringundserde_json::from_strein (oder beginne leer, wenn die Datei nicht existiert). Schreib die Liste nach jeder Änderung mitserde_json::to_string_prettyundstd::fs::writezurück. Auf den Seiten Dateien lesen und schreiben und JSON mit Serde findest du alles, was du brauchst. Später kannst du die Datei gegen eine echte Datenbank wie SQLite austauschen. - Logging mit tower-http. Führ
cargo add tower-http --features traceundcargo add tracing-subscriberaus und füge dann eine Logging-Schicht (Layer) hinzu. Der Server gibt jetzt für jede Anfrage eine Zeile aus, mit Methode, Pfad, Status und Dauer:
use tower_http::trace::TraceLayer;
// am Anfang von main:tracing_subscriber::fmt() .with_max_level(tracing_subscriber::filter::LevelFilter::DEBUG) .init();
// beim Bauen des Routers: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);- Hinweise zum Deployment.
127.0.0.1nimmt nur Anfragen von deinem eigenen Computer an. Wenn auch andere Rechner zugreifen sollen (zum Beispiel in Docker oder auf einem Server), bindest du stattdessen an0.0.0.0. Viele Hosting-Dienste teilen deinem Programm über die UmgebungsvariablePORTmit, welchen Port es benutzen soll. Die liest du mitstd::env::var(siehe Umgebungsvariablen und Prozesse). Bau mitcargo build --releasefür einen viel schnelleren Server. Das Ergebnis ist eine einzige Datei intarget/release/, die du auf einen Linux-Server kopieren oder in ein kleines Docker-Image packen kannst. Für eine öffentliche Website stellst du einen Reverse Proxy (wie Caddy oder nginx) davor, der sich um HTTPS kümmert.
Zusammenfassung
Abschnitt betitelt „Zusammenfassung“- Ein Handler ist eine
async fn. Ein Router ordnet Methode und Pfad einem Handler zu.axum::serveführt ihn auf einemTcpListenervontokioaus. - Extractors holen Daten aus der Anfrage:
Pathfür Teile der URL (/todos/{id}),Jsonfür den Body (immer als Letztes) undStatefür gemeinsam genutzte Daten. - Gib
Json(value)für JSON zurück,StatusCodefür einen Status, ein Tupel, um beides zu setzen, oder einResult, um zwischen Erfolg und Fehlerstatus zu wählen. Arc<Mutex<Vec<Todo>>>teilt eine Liste sicher zwischen allen Anfragen. Sperren, benutzen, Guard fallen lassen. Halte die Sperre nicht über ein.awaithinweg.- Teste deine API mit
curl:-izeigt den Status,-Xsetzt die Methode,-Hfügt einen Header hinzu, und-dschickt einen Body.
Kurzer Check
Abschnitt betitelt „Kurzer Check“Kurzer Check
1. Wie schreibst du in axum 0.8 eine Route mit einer ID im Pfad?
2. Ein Handler gibt (StatusCode::CREATED, Json(todo)) zurück. Was bekommt der Client?
3. Warum steckt die gemeinsame Liste in einem Arc<Mutex<...>>?
4. Welcher Extractor muss als letzter Parameter eines Handlers kommen?
5. Du schickst mit curl -X POST etwas JSON, vergisst aber den Header Content-Type: application/json. Was antwortet axum?
Punkte: 0 / 5
Weiterlesen
Abschnitt betitelt „Weiterlesen“- Dokumentation von axum – docs.rs (auf Englisch): die offizielle Anleitung, mit Abschnitten zu Routing, Handlern und Antworten.
- Extractors in axum – docs.rs (auf Englisch): jeder eingebaute Extractor und die Regeln für ihre Reihenfolge.
- Tokio-Tutorial (auf Englisch): Lerne die asynchrone Runtime kennen, auf der axum läuft, inklusive Shared State.
- Dokumentation von tower-http – docs.rs (auf Englisch): fertige Layer für Logging, CORS, Kompression und Timeouts.
- HTTP-Statuscodes – MDN: was 200, 201, 204, 404, 422 und die anderen bedeuten (auf Deutsch).