API-Versioning in Rust mit Axum und Actix Web navigieren
Lukas Schneider
DevOps Engineer · Leapcell

Einleitung
Wenn Webanwendungen weiterentwickelt werden, ändern sich auch ihre APIs unweigerlich. Neue Funktionen werden hinzugefügt, alte werden ausgemustert und Datenstrukturen verfeinert. Um sicherzustellen, dass diese Änderungen bestehende Client-Anwendungen nicht beeinträchtigen, wird eine robuste API-Versionierung zu einer wichtigen Praxis. Ohne eine klare Strategie können API-Updates zu erheblichen Kompatibilitätsproblemen, frustrierten Benutzern und einer hohen Wartungslast führen. Im Rust-Web-Ökosystem bieten Frameworks wie Axum und Actix Web leistungsstarke Werkzeuge zum Erstellen performanter und zuverlässiger APIs. Dieser Artikel befasst sich mit zwei primären API-Versioning-Strategien – der Verwendung von URL-Pfaden und der Nutzung des Accept
-Headers – und untersucht deren Implementierungsdetails, Vorteile und Nachteile im Kontext dieser beliebten Rust-Frameworks.
Verständnis von API-Versioning-Strategien
Bevor wir uns mit den Details befassen, definieren wir die Kernkonzepte im Zusammenhang mit API-Versioning. API-Versioning ist die Praxis, mehrere Versionen einer API gleichzeitig zu pflegen, sodass Clients wählen können, mit welcher Version sie interagieren.
Dies verhindert, dass sich Änderungen, die die Kompatibilität brechen, auf ältere Clients auswirken, während neue Clients von den neuesten Funktionen profitieren können.
Die beiden Hauptstrategien, die wir untersuchen werden, sind:
- URL-Pfad-Versioning: Dabei wird die API-Version direkt in den URL-Pfad eingebettet. Zum Beispiel
/api/v1/users
und/api/v2/users
. - Accept-Header-Versioning: Hier geben Clients ihre gewünschte API-Version an, indem sie einen benutzerdefinierten Medientyp innerhalb des
Accept
-HTTP-Headers senden. Zum BeispielAccept: application/vnd.myapi.v1+json
.
URL-Pfad-Versioning
Prinzip und Implementierung
Das URL-Pfad-Versioning ist wohl die einfachste und am weitesten verbreitete Methode. Die Versionsnummer ist ein klarer, sichtbarer Teil des Pfades des Endpunkts. Dies macht es für Entwickler intuitiv zu verstehen, mit welcher Version sie interagieren, und ermöglicht einfache Routing-Regeln.
In Rust machen sowohl Axum als auch Actix Web die Implementierung des URL-Pfad-Versionings durch ihre Routing-Mechanismen recht natürlich.
Axum Beispiel
Axums Routing basiert auf Services, und die Definition von Routen mit Versionspräfixen ist sehr sauber.
use axum::* async fn get_users_v1() -> impl IntoResponse { "Getting users from API V1!" } async fn get_users_v2() -> impl IntoResponse { "Getting users from API V2!" } #[tokio::main] async fn main() { let app = Router::new() .nest("/api/v1", Router::new().route("/users", get(get_users_v1))) .nest("/api/v2", Router::new().route("/users", get(get_users_v2))); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); println!("Listening on {}", listener.local_addr().unwrap()); axum::serve(listener, app).await.unwrap(); }
In diesem Axum-Beispiel erstellen wir zwei verschachtelte Router
-Instanzen, eine für /api/v1
und eine für /api/v2
. Jede behandelt den /users
-Endpunkt unabhängig und leitet Anfragen an ihre jeweiligen versionsspezifischen Handler weiter.
Actix Web Beispiel
Actix Web verwendet Makros und Funktionsattribute für das Routing, was sich ebenfalls gut für pfadbasiertes Versioning eignet.
use actix_web::* #[get("/api/v1/users")] async fn get_users_v1_actix() -> impl Responder { "Getting users from API V1!" } #[get("/api/v2/users")] async fn get_users_v2_actix() -> impl Responder { "Getting users from API V2!" } #[actix_web::main] async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new() .service(get_users_v1_actix) .service(get_users_v2_actix) }) .bind(("127.0.0.1", 8080))?; .run() .await }
Hier definiert das #[get]
-Makro von Actix Web direkt den vollständigen Pfad einschließlich der Version, was sehr explizit ist. Jeder Handler einer Version wird als separater Service registriert.
Vorteile des URL-Pfad-Versionings
- Entdeckbarkeit: Die Version ist in der URL sofort ersichtlich, was sie für Entwickler und Dokumentationen leicht verständlich macht.
- Caching: Proxies und Caches können verschiedene Versionen als völlig separate Ressourcen behandeln, was die Caching-Strategien vereinfacht.
- Browserfreundlich: Sie können über einen Webbrowser direkt auf verschiedene API-Versionen zugreifen.
- Einfachheit: Das Routing ist im Allgemeinen einfach zu implementieren und zu verstehen.
Nachteile des URL-Pfad-Versionings
- URL-Verschmutzung: URLs können mit Versionsnummern länger und weniger elegant werden.
- Clientseitige "Verknüpfung": Clients, die Versionsnummern in URLs fest kodieren, müssen möglicherweise massenhaft aktualisiert werden, wenn eine neue Version zum Standard wird oder eine alte ausgemustert wird.
- Routing-Overhead: Potenziell mehr Routen zu verwalten, wenn viele Endpunkte versioniert sind.
Accept-Header-Versioning
Prinzip und Implementierung
Das Accept-Header-Versioning (auch Content Negotiation genannt) stützt sich auf den Accept
-HTTP-Header. Clients geben einen benutzerdefinierten Medientyp an, der die API-Version enthält, und der Server antwortet entsprechend. Dies hält die URL sauber und ermöglicht es demselben Pfad, mehrere Versionen einer Ressource bereitzustellen.
Der benutzerdefinierte Medientyp folgt typischerweise einer Konvention wie application/vnd.company.resource.vX+json
.
Die Implementierung dieser Strategie erfordert die Überprüfung des Accept
-Headers und das Routing von Anfragen basierend auf dessen Inhalt.
Axum Beispiel
Axums Extraktoren eignen sich hervorragend für die Implementierung von Header-basiertem Versioning. Wir können einen benutzerdefinierten Extraktor erstellen, der den Accept
-Header parst.
use axum::* // Benutzerdefinierter Extraktor für API-Version pub struct ApiVersion(pub u8); #[async_trait] impl<S> FromRequestParts<S> for ApiVersion where S: Send + Sync, { type Rejection = Response; async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> { let accept_header = parts.headers .get(header::ACCEPT) .and_then(|h| h.to_str().ok()) .unwrap_or_default(); // Beispiel: Parsen von "application/vnd.myapi.v1+json" if let Some(version_str) = accept_header.find("vnd.myapi.v") { let start = version_str + "vnd.myapi.v".len(); if let Some(end) = accept_header[start..].find('+') { if let Ok(version) = accept_header[start..start + end].parse::<u8>() { return Ok(ApiVersion(version)); } } } // Standard auf Version 1, wenn keine bestimmte Version oder eine nicht unterstützte Version angefordert wird Ok(ApiVersion(1)) } } async fn get_users_versioned(ApiVersion(version): ApiVersion) -> impl IntoResponse { match version { 1 => "Getting users from API V1 via Accept header!".to_string(), 2 => "Getting users from API V2 via Accept header!".to_string(), _ => format!("Unsupported API version: {}", version), } } #[tokio::main] async fn main() { let app = Router::new() .route("/api/users", get(get_users_versioned)); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); println!("Listening on {}", listener.local_addr().unwrap()); axum::serve(listener, app).await.unwrap(); }
In Axum definieren wir ApiVersion
als benutzerdefinierten Extraktor, der den Accept
-Header verarbeitet. Die Implementierung von from_request_parts
versucht, die Versionsnummer aus dem Header zu parsen. Der Handler get_users_versioned
verwendet dann diese ApiVersion
, um zu entscheiden, welche Logik ausgeführt werden soll.
Actix Web Beispiel
Actix Web erlaubt ebenfalls benutzerdefinierte Extraktoren und unterstützt die Überprüfung von Headern robust.
use actix_web::* // Benutzerdefinierter Extraktor für API-Version in Actix Web struct ApiVersionActix(u8); impl actix_web::FromRequest for ApiVersionActix { type Error = actix_web::Error; type Future = std::future::Ready<Result<Self, Self::Error>>; fn from_request(req: &HttpRequest, _: &mut actix_web::dev::Payload) -> Self::Future { let accept_header = req.headers() .get(ACCEPT) .and_then(|h| h.to_str().ok()) .unwrap_or_default(); let mut version = 1; // Standard auf V1 if let Some(version_str_idx) = accept_header.find("vnd.myapi.v") { let start = version_str_idx + "vnd.myapi.v".len(); if let Some(end_idx) = accept_header[start..].find('+') { if let Ok(parsed_version) = accept_header[start..start + end_idx].parse::<u8>() { version = parsed_version; } } } std::future::ready(Ok(ApiVersionActix(version))) } } async fn get_users_versioned_actix(api_version: ApiVersionActix) -> impl Responder { match api_version.0 { 1 => HttpResponse::Ok().body("Getting users from API V1 via Accept header!"), 2 => HttpResponse::Ok().body("Getting users from API V2 via Accept header!"), _ => HttpResponse::BadRequest().body(format!("Unsupported API version: {}", api_version.0)), } } #[actix_web::main] async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new() .service( web::resource("/api/users") .route(web::get().to(get_users_versioned_actix)) ) }) .bind(("127.0.0.1", 8080))?; .run() .await }
Die Actix Web ApiVersionActix
implementiert ebenfalls FromRequest
, was ihre Verwendung als Argument in Handlerfunktionen ermöglicht. Die Logik zum Parsen des Accept
-Headers ähnelt dem Axum-Beispiel.
Vorteile des Accept-Header-Versionings
- Saubere URLs: URLs bleiben über verschiedene API-Versionen hinweg stabil, was die Ästhetik und potenziell das SEO für öffentliche APIs verbessert.
- Einzelner Endpunkt für mehrere Versionen: Derselbe Pfad
/api/users
kann unterschiedliche Darstellungen von Benutzern basierend auf der vom Client angeforderten Version bereitstellen. - Flexibilität: Ermöglicht Clients, Versionen einfach auszutauschen, ohne die gesamte URL-Struktur zu ändern.
Nachteile des Accept-Header-Versionings
- Weniger entdeckerfreundlich: Die Version ist in einem Header verborgen, was es weniger offensichtlich macht, welche Version ohne Überprüfung von Netzwerkanfragen abgerufen wird.
- Caching-Komplexität: Caching-Proxies haben möglicherweise Schwierigkeiten, Antworten effektiv zu cachen, da der Cache-Schlüssel den
Accept
-Header enthalten muss, was potenziell zu Cache-Fehlern führt. - Browser-Inkompatibilität: Der direkte Zugriff auf Versionen über einen Webbrowser ist schwierig, da Browser im Allgemeinen keine Anpassung des
Accept
-Headers direkt erlauben. - Fehlerbehandlung: Es kann schwieriger sein, einem Client mitzuteilen, dass er eine nicht unterstützte Version anfordert oder dass sein
Accept
-Header falsch formatiert ist.
Auswahl der richtigen Strategie
Die Wahl zwischen URL-Pfad- und Accept-Header-Versioning hängt oft von einigen Schlüsselüberlegungen ab:
- API-Zielgruppe: Für öffentlich zugängliche APIs, bei denen Entdeckbarkeit und direkter Browserzugriff wichtig sind, ist das URL-Pfad-Versioning möglicherweise vorzuziehen. Für interne oder Machine-to-Machine-APIs bietet das Accept-Header-Versioning sauberere URLs.
- Caching-Anforderungen: Wenn Caching kritisch ist und mit externen Proxies implementiert wird, vereinfacht das URL-Pfad-Versioning die Caching-Strategien.
- Clientseitige Flexibilität: Wenn Clients häufig zwischen Versionen derselben Ressource wechseln müssen, kann das Accept-Header-Versioning anpassungsfähiger sein.
- Architektonische Einfachheit: Das URL-Pfad-Versioning ist für die meisten Entwickler im Allgemeinen einfacher zu verstehen und zunächst zu implementieren.
Es ist auch erwähnenswert, dass diese Strategien nicht gegenseitig ausschließend sind. Einige APIs verwenden möglicherweise einen hybriden Ansatz oder sogar datumsbasiertes Versioning für kleinere Änderungen. Für die meisten gängigen Anwendungsfälle wird jedoch eine dieser beiden ausreichen.
Fazit
API-Versioning ist ein unverzichtbarer Aspekt beim Entwurf skalierbarer und wartbarer Webdienste. Im Rust-Ökosystem bieten Axum und Actix Web die notwendigen Werkzeuge und Flexibilität, um sowohl URL-Pfad- als auch Accept-Header-Versioning effektiv zu implementieren. Während das URL-Pfad-Versioning Einfachheit und Entdeckbarkeit bietet, liefert das Accept-Header-Versioning sauberere URLs und größere Flexibilität bei der Ressourcenrepräsentation. Die sorgfältige Auswahl der richtigen Strategie für Ihr Projekt wird die Langlebigkeit und Benutzerfreundlichkeit Ihrer APIs erheblich verbessern.