Erstellung modularer Web-APIs mit Axum in Rust
Daniel Hayes
Full-Stack Engineer · Leapcell

Einleitung
In der sich ständig weiterentwickelnden Landschaft der Backend-Entwicklung ist der Aufbau robuster, skalierbarer und wartbarer Web-APIs von größter Bedeutung. Rust hat sich mit seinem Fokus auf Leistung, Sicherheit und Nebenläufigkeit als überzeugende Wahl für diesen Bereich herauskristallisiert. Die rohe Leistung von Rust geht jedoch oft mit einer steileren Lernkurve einher, insbesondere bei der Orchestrierung komplexer Anwendungslogik. Hier glänzen moderne Web-Frameworks wie Axum. Axum, das auf dem leistungsstarken Tokio-Runtime und dem vielseitigen Tower-Ökosystem aufbaut, bietet eine einfache und ergonomische Möglichkeit, Webdienste in Rust zu erstellen. Es verfolgt das Konzept der Modularität, das es Entwicklern ermöglicht, ihre API-Endpunkte zu organisieren, gemeinsame Zustände effizient zu verwalten und leistungsstarke Middleware kohärent zu integrieren. Dieser Artikel führt Sie durch den Prozess des Aufbaus einer modularen Web-API mit Axum und zeigt, wie Sie Routing effektiv handhaben, Anwendungszustände gemeinsam nutzen und die durch Tower-Services gebotene Erweiterbarkeit nutzen.
Kernkonzepte verstehen
Bevor wir uns mit der Implementierung befassen, wollen wir einige grundlegende Konzepte klären, die für den Aufbau von APIs mit Axum zentral sind:
- Axum: Ein Webanwendungs-Framework für Rust. Es basiert auf
tokio
(asynchrone Laufzeit) undhyper
(HTTP-Bibliothek) und nutzt dastower
-Ökosystem für Middleware und Service-Komposition. - Routing: Der Mechanismus, mit dem eingehende HTTP-Anfragen basierend auf ihrem URL-Pfad und ihrer HTTP-Methode an die entsprechenden Handler-Funktionen weitergeleitet werden. Axum bietet eine deklarative und typsichere Möglichkeit, Routen zu definieren.
- Zustandsverwaltung: In Webanwendungen ist es oft notwendig, Daten (z. B. Datenbankverbindungen, Konfigurationen, Caches) zwischen verschiedenen Anforderungshandlern auszutauschen. Axum bietet robuste Mechanismen zur Verwaltung von anwendungsweiten und anforderungsspezifischen Zuständen.
- Tower-Services: Tower ist eine Bibliothek modularer, wiederverwendbarer Komponenten zum Aufbau robuster Netzwerkanwendungen. In Axum sind Handler im Wesentlichen
Tower.Service
-Implementierungen und Middleware sindTower.Layer
, die Services umschließen. Diese Architektur fördert Komposition und Wiederverwendbarkeit. - Middleware: Funktionen oder Dienste, die sich zwischen dem Server und dem Handler befinden und es Ihnen ermöglichen, Anfragen vorab zu verarbeiten oder Antworten nachzubereiten. Häufige Verwendungszwecke sind Authentifizierung, Protokollierung, Fehlerbehandlung und Ratenbegrenzung.
Aufbau einer modularen Web-API
Lassen Sie uns eine einfache API zur Verwaltung einer Benutzerliste erstellen, die modulares Routing, Zustandsfreigabe und die Anwendung von Tower-Services demonstriert.
Projekteinrichtung
Erstellen Sie zunächst ein neues Rust-Projekt:
car go new axum_modular_api cd axum_modular_api
Fügen Sie die erforderlichen Abhängigkeiten zu Ihrer Cargo.toml
hinzu:
[dependencies] axum = { version = "0.7", features = ["macros"] } tokio = { version = "1", features = ["full"] } serde = { version = "1", features = ["derive"] } serde_json = "1" tracing = "0.1" tracing-subscriber = "0.3"
Definieren unseres Anwendungszustands
Für unsere Benutzerverwaltungs-API benötigen wir eine Möglichkeit, Benutzer zu speichern. Ein einfaches Vec<User>
, das in einem Arc<Mutex<...>>
verpackt ist, ist ein guter Ausgangspunkt für den In-Memory-Zustand.
Erstellen Sie eine Datei src/models.rs
:
// src/models.rs use serde::{Deserialize, Serialize}; use std::sync::{Arc, Mutex}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct User { pub id: u32, pub name: String, pub email: String, } pub type AppState = Arc<Mutex<Vec<User>>>; pub fn initialize_state() -> AppState { Arc::new(Mutex::new(vec![ User { id: 1, name: "Alice".to_string(), email: "alice@example.com".to_string() }, User { id: 2, name: "Bob".to_string(), email: "bob@example.com".to_string() }, ])) }
Modulares Routing
Wir werden unsere Routen zur besseren Wartbarkeit in separate Module aufteilen. Erstellen Sie eine Datei src/routes/mod.rs
und src/routes/users.rs
.
src/routes/users.rs
: Dieses Modul enthält alle benutzerbezogenen Endpunkte.
// src/routes/users.rs use axum::{ extract::{Path, State}, http::StatusCode, response::IntoResponse, routing::{get, post}, Json, Router, }; use serde_json::json; use crate::models::{AppState, User}; pub fn users_router() -> Router<AppState> { Router::new() .route("/", get(list_users).post(create_user)) .route("/:id", get(get_user).put(update_user).delete(delete_user)) } async fn list_users(State(state): State<AppState>) -> Json<Vec<User>> { let users = state.lock().unwrap(); Json(users.clone()) } async fn get_user(State(state): State<AppState>, Path(id): Path<u32>) -> Result<Json<User>, StatusCode> { let users = state.lock().unwrap(); if let Some(user) = users.iter().find(|u| u.id == id) { Ok(Json(user.clone())) } else { Err(StatusCode::NOT_FOUND) } } async fn create_user(State(state): State<AppState>, Json(mut new_user): Json<User>) -> impl IntoResponse { let mut users = state.lock().unwrap(); let next_id = users.iter().map(|u| u.id).max().unwrap_or(0) + 1; new_user.id = next_id; users.push(new_user.clone()); (StatusCode::CREATED, Json(new_user)) } async fn update_user( State(state): State<AppState>, Path(id): Path<u32>, Json(updated_user): Json<User>, ) -> impl IntoResponse { let mut users = state.lock().unwrap(); if let Some(user) = users.iter_mut().find(|u| u.id == id) { user.name = updated_user.name; user.email = updated_user.email; (StatusCode::OK, Json(user.clone())) } else { (StatusCode::NOT_FOUND, Json(json!({ "message": "User not found" }))) } } async fn delete_user(State(state): State<AppState>, Path(id): Path<u32>) -> StatusCode { let mut users = state.lock().unwrap(); let initial_len = users.len(); users.retain(|u| u.id != id); if users.len() < initial_len { StatusCode::NO_CONTENT } else { StatusCode::NOT_FOUND } }
src/routes/mod.rs
: Dieses Modul exportiert unsere Sub-Router neu und kann potenziell gemeinsame Routen enthalten.
// src/routes/mod.rs pub mod users;
Zusammensetzen der Hauptanwendung
Nun bringen wir alles in src/main.rs
zusammen. Wir initialisieren unseren Anwendungszustand, erstellen den Router und fügen auf einfache Weise Protokollierung mit tracing
hinzu.
// src/main.rs mod models; mod routes; use axum::{ routing::get, Router, }; use tower_http::trace::{self, TraceLayer}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; use std::time::Duration; #[tokio::main] async fn main() { tracing_subscriber::registry() .with( tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| "axum_modular_api=debug,tower_http=debug".into()) знаходжу ) .with(tracing_subscriber::fmt::layer()) .init(); let app_state = models::initialize_state(); // Bauen Sie unsere Anwendung mit einer Route let app = Router::new() .route("/", get(|| async { "Hello, Modular Axum API!" })) // Montieren Sie den Benutzer-Router unter dem Pfad /users .nest("/users", routes::users::users_router()) .with_state(app_state) // Fügen Sie Tower-Services (Middleware) hinzu .layer( TraceLayer::new_for_http() .make_span_with(trace::DefaultMakeSpan::new().include_headers(true)) .on_request(trace::DefaultOnRequest::new().level(tracing::Level::INFO)) .on_response(trace::DefaultOnResponse::new().level(tracing::Level::INFO).latency_300_ms(Duration::from_millis(300))) ); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); tracing::info!("listening on {}", listener.local_addr().unwrap()); axum::serve(listener, app).await.unwrap(); }
Ausführen der API
Sie können diese API mit cargo run
ausführen.
car go run
Dann können Sie mit Werkzeugen wie curl
damit interagieren:
- Alle Benutzer abrufen:
curl http://localhost:3000/users
- Benutzer nach ID abrufen:
curl http://localhost:3000/users/1
- Neuen Benutzer POSTen:
curl -X POST -H "Content-Type: application/json" -d '{"name": "Charlie", "email": "charlie@example.com"}' http://localhost:3000/users
- Benutzer aktualisieren per PUT:
curl -X PUT -H "Content-Type: application/json" -d '{"name": "Alice Smith", "email": "alice.smith@example.com"}' http://localhost:3000/users/1
- Benutzer löschen:
curl -X DELETE http://localhost:3000/users/2
Erläuterung wichtiger Funktionen
-
Modulares Routing:
- Die Funktion
users_router()
insrc/routes/users.rs
gibt einenaxum::Router
zurück. Dieser Router kapselt die gesamte Benutzeroberflächenlogik. - In
main.rs
verwenden wir.nest("/users", routes::users::users_router())
, um diesen Unter-Router unter dem Pfad/users
zu montieren. Dies schafft eine klare Hierarchie und hält Ihre Hauptdateimain.rs
übersichtlicher. - Der
Router<AppState>
-Typ stellt sicher, dassAppState
konsistent an alle Routen innerhalb dieses Routers weitergegeben wird.
- Die Funktion
-
Zustandsverwaltung:
- Wir definieren
AppState
alsArc<Mutex<Vec<User>>>
.Arc
ermöglicht mehrere Besitzer des Zustands, undMutex
kümmert sich um den sicheren gleichzeitigen Zugriff. - Die Methode
with_state(app_state)
des Hauptrouters injiziert unserAppState
in die Anwendung. - In Handler-Funktionen wird
State(state): State<AppState>
als Extraktor verwendet, um den gemeinsamen Zustand abzurufen. Dies ist typsicher und idiomatisch für Axum.
- Wir definieren
-
Tower-Services und Middleware:
- Wir haben
tower_http::trace::TraceLayer
verwendet, um Protokollierung von Anfragen/Antworten hinzuzufügen. Dies ist ein leistungsstarkes Beispiel für einen Tower-Layer
. - Die Methode
.layer(...)
des Routers wendet diese Middleware auf alle Routen an, die nachwith_state
und vor.layer
definiert sind. Wenn sie vorwith_state
angewendet wird, hätte die Middleware keinen Zugriff auf den Zustand. - Tower-Services können verkettet werden, sodass Sie komplexe Middleware-Pipelines für Authentifizierung, Ratenbegrenzung, CORS, Komprimierung usw. erstellen können, ohne Ihre Kernlogik zu überladen.
- Wir haben
Anwendungsszenarien
Dieser modulare Ansatz ist vorteilhaft für:
- Große APIs: Wenn Ihre API wächst, verhindert die Trennung von Belangen in separate Routing-Module, dass Ihre
main.rs
zu einer monolithischen Datei wird. - Teamzusammenarbeit: Verschiedene Teams oder Entwickler können an separaten API-Modulen arbeiten, ohne nennenswerte Merge-Konflikte.
- Wartbarkeit: Änderungen in einem API-Bereich wirken sich weniger wahrscheinlich auf nicht verwandte Teile aus.
- Testbarkeit: Einzelne Router und ihre Handler können isoliert getestet werden.
Fazit
Der Aufbau modularer Web-APIs mit Axum in Rust bietet einen leistungsstarken und organisierten Weg, Komplexität zu bewältigen und Skalierbarkeit und Wartbarkeit zu gewährleisten. Durch die effektive Nutzung des deklarativen Routings von Axum, der robusten Zustandsverwaltung und der komponierbaren Dienste des tower
-Ökosystems können Entwickler Hochleistungs-, typsichere und leicht erweiterbare Backend-Systeme erstellen. Dieser Ansatz rationalisiert nicht nur die Entwicklung, sondern fördert auch eine saubere Architektur, die den Aufbau und die Wartung Ihrer Rust-Webanwendungen zu einer Freude macht.