Zum Inhalt springen

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.

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.

Leg ein neues Projekt an und füge vier Crates hinzu:

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

In 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.)

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. hello gibt etwas Text zurück, und axum schickt ihn als Antwort zurück.
  • Der Router verbindet Pfade mit Handlern. .route("/", get(hello)) bedeutet: „Wenn jemand eine GET-Anfrage an / schickt, ruf hello auf.“
  • Der Listener öffnet Port 3000 auf deinem eigenen Computer (127.0.0.1), und axum::serve beginnt, 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:3000

Lass es laufen, öffne ein zweites Terminal und schick ihm eine Anfrage:

Terminal window
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.

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:

Terminal window
curl -i http://127.0.0.1:3000/example
HTTP/1.1 200 OK
content-type: application/json
content-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 22
▶ Selbst ausprobieren

Ein 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.

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.

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

Jetzt 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:

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`

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.

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:

Terminal window
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:

Terminal window
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 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 23

Die 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.

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 /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
}
}

Gehen wir die neuen Ideen durch:

  • Ein Pfad, mehrere Methoden. get(list_todos).post(create_todo) schickt GET /todos und POST /todos an verschiedene Handler.
  • Statuscodes sind Rückgabewerte. StatusCode stammt aus dem Modul http von axum. Ein Handler kann ein Result zurückgeben: Aus Ok(Json(todo)) wird 200 mit JSON, aus Err(StatusCode::NOT_FOUND) ein leeres 404. Ein Tupel (StatusCode::CREATED, Json(todo)) setzt den Status und den Body. Gibst du nur StatusCode::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 normalen Vec lesen 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. retain behä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 })
None
1 gelöscht? true
1 noch einmal gelöscht? false
übrig: 1
▶ Selbst ausprobieren

Das To-do, das über second_handle angelegt wurde, taucht auf, wenn du über db liest, denn beide Arcs zeigen auf dieselbe Liste.

Starte den Server mit cargo run und probier in einem zweiten Terminal jeden Endpunkt aus. Am Anfang ist die Liste leer:

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

Leg ein To-do an. Achte auf den Status 201 Created:

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

Leg ein zweites an und lass dir alle auflisten:

Terminal window
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:

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

Lösch To-do 1 und versuch dann, es noch einmal zu löschen:

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":"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:

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

Die 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.

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 message

Wenn 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.

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 mit iter_mut(), setz done = true und gib es zurück.
  • Validierung. Ein leerer Titel sollte nicht erlaubt sein. Ein Handler kann Result<(StatusCode, Json<Todo>), (StatusCode, String)> zurückgeben und Err((StatusCode::BAD_REQUEST, String::from("Titel darf nicht leer sein"))) schicken. Prüf das mit title.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> und next_id: u32 im Mutex.
  • Mit einer JSON-Datei speichern. Leite auch Deserialize für Todo ab. Lies beim Start todos.json mit std::fs::read_to_string und serde_json::from_str ein (oder beginne leer, wenn die Datei nicht existiert). Schreib die Liste nach jeder Änderung mit serde_json::to_string_pretty und std::fs::write zurü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 trace und cargo add tracing-subscriber aus 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.1 nimmt nur Anfragen von deinem eigenen Computer an. Wenn auch andere Rechner zugreifen sollen (zum Beispiel in Docker oder auf einem Server), bindest du stattdessen an 0.0.0.0. Viele Hosting-Dienste teilen deinem Programm über die Umgebungsvariable PORT mit, welchen Port es benutzen soll. Die liest du mit std::env::var (siehe Umgebungsvariablen und Prozesse). Bau mit cargo build --release für einen viel schnelleren Server. Das Ergebnis ist eine einzige Datei in target/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.
  • Ein Handler ist eine async fn. Ein Router ordnet Methode und Pfad einem Handler zu. axum::serve führt ihn auf einem TcpListener von tokio aus.
  • Extractors holen Daten aus der Anfrage: Path für Teile der URL (/todos/{id}), Json für den Body (immer als Letztes) und State für gemeinsam genutzte Daten.
  • Gib Json(value) für JSON zurück, StatusCode für einen Status, ein Tupel, um beides zu setzen, oder ein Result, 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 .await hinweg.
  • Teste deine API mit curl: -i zeigt den Status, -X setzt die Methode, -H fügt einen Header hinzu, und -d schickt einen Body.

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