Ferris Talk #12: Web-APIs mit Rust erstellen

Dank Frameworks wie Rocket und Axum macht Rust auch jenseits der systemnahen Programmierung beim Gestalten von Web-APIs eine gute Figur.

In Pocket speichern vorlesen Druckansicht 1 Kommentar lesen
Lesezeit: 16 Min.
Von
  • Rainer Stropek
Inhaltsverzeichnis

Rust eignet sich als sichere Alternative zu C gut für die systemnahe Programmierung, ist jedoch keinesfalls darauf beschränkt. Die Programmiersprache und das dazugehörige Ökosystem an Bibliotheken – in Rust Crates genannt – bieten sich ebenso für die Anwendungsentwicklung an. Da Rust durch das Kompilieren in nativen Code sowie durch den Ansatz der Zero-Cost-Abstractions besonders effiziente und performante Anwendungen erzeugen kann, ziehen mehr und mehr Teams die Sprache jenseits von Anwendungsfällen mit besonderer Systemnähe in Betracht.

Ferris Talks – die Kolumne für Rustaceans

Die Entwicklung von Web-APIs ist ein solcher Anwendungsbereich, in dem Rust seine Stärken hinsichtlich Effizienz ausspielen kann. Viele Unternehmen betreiben APIs in der Cloud, und effizienter Code bedeutet dort weniger Ressourcenbedarf und damit geringere monatliche Kosten. Hinzu kommt, dass schnell startende Anwendungen für Serverless Computing wichtig sind, da die Plattform je nach Last dynamisch Serverinstanzen hinzufügt oder entfernt. Ein Serverprozess, der Minuten zum Starten braucht, ist in einer Serverless-Umgebung unbrauchbar. Mit Rust lassen sich auch bei nicht trivialen APIs ohne weiteres Startzeiten deutlich unter einer Sekunde erreichen.

Eine Programmiersprache alleine reicht aber noch nicht, um praxistaugliche Web API-Entwicklung betreiben zu können. Daneben braucht es die richtigen Frameworks, und in dem Bereich gibt es im Umfeld von Rust aktuell viel Bewegung. Im September 2022 kam der zweite Release Candidate des immer populärer werdenden Axum-Frameworks heraus. Der Fortschrittsbalken des Meilensteins für die Version 0.5 des etablierten Rocket-Frameworks steht auf GitHub mittlerweile auf 95 Prozent.

In dieser Ausgabe unserer Kolumne nehmen wir diese Entwicklungen zum Anlass, die Entwicklung von Web-APIs mit Rust zu zeigen. Wir vergleichen dabei die unterschiedlichen API-Ansätze der Frameworks Axum und Rocket und betrachten zum Abschluss den Stand der Dinge in Rust im Vergleich zum ASP.NET-Core-Framework der .NET-Plattform, das unter anderem laut einer aktuellen Stack-Overflow-Umfrage zu den verbreitetsten Webframeworks weltweit gehört.

Neben Axum und Rocket gibt es noch zahlreiche weitere Frameworks für die Web-API-Entwicklung in Rust. Vielfalt ist ein charakteristischer Wesenszug des Rust-Ökosystems. Eine komplettere Liste von Frameworks für Webentwicklung mit Rust findet man auf den Webseiten Are We Web Yet und Awesome Rust. Dieser Artikel konzentriert sich ausschließlich auf HTTP-Web-APIs und behandelt keine APIs mit anderen Protokollen wie Websockets oder gRPC.

Naturgemäß spielt asynchrone Programmierung eine wichtige Rolle bei der Entwicklung von Web-APIs. Um diese Herausforderung zu meistern, bauen Axum und Rocket sowie andere populäre Web-API-Frameworks wie Actix Web und Warp auf Tokio auf. Dabei handelt es sich im Kern um eine Laufzeitumgebung für asynchrones Programmieren in Rust mit darauf aufbauenden Erweiterungen. Eine solche Basis ist wichtig, da Rust für die asynchrone Entwicklung zwar die grundlegenden Abstraktionen wie Futures bietet, die asynchrone Laufzeitumgebung jedoch anders als bei anderen Sprachen wählbar und austauschbar ist. Mit der asynchronen Programmierung in Rust beschäftigt sich ein früherer Ferris-Talk-Beitrag.

Obwohl sowohl Axum als auch Rocket Tokio als Basis haben, unterscheiden sie sich in der Tiefe der Integration stark. Rocket verwendet Tokio im Hintergrund: Beim Entwickeln kommt man nicht zwangsläufig mit Tokio in Berührung. Im Gegensatz dazu ist Axum direkt im Tokio-Technologie-Stack zu Hause. Das Team hinter dem Framework versucht nicht, Tokio zu verstecken, sondern betrachtet die enge Einbindung in Tokio-Technologien wie Tower als Stärke. Das ist ein Pluspunkt für diejenigen, die Tokio gut kennen und vielleicht sogar schon beispielsweise Tower-basierende HTTP-Middleware-Komponenten entwickelt haben.

Ein weiteres Unterscheidungsmerkmal von Web-API-Frameworks auf Rust-Basis ist der Einsatz von Makros. Wir haben bewusst zwei Frameworks gewählt, die in dieser Hinsicht unterschiedlich sind. Die Rocket-API ist geprägt von Makros, die an allen Ecken und Enden zum Einsatz kommen, um die Entwicklung von Web-APIs zu vereinfachen. Im Gegensatz dazu kommt die Axum-API komplett ohne Makros aus. Sie ist deshalb nicht automatisch schwieriger zu erlernen oder weniger angenehm in der Handhabung. Die APIs der beiden Frameworks sind einfach von Grund auf unterschiedlich gestaltet. Wer kein Problem mit Makros in Rust hat, kann beide Frameworks gleichermaßen verwenden. Wer jedoch auf Makros weitgehend verzichten möchte, tendiert wahrscheinlich zu Axum.

Die Heise-Konferenz zu Rust

Am 9. November findet die zweite Auflage der betterCode() Rust statt. Die Online-Konferenz fokussiert sich dieses Jahr auf einen praxisnahen Einstieg in die Programmiersprache Rust. Die von heise Developer und dpunkt.verlag ausgerichtete Veranstaltung vermittelt in sechs Vorträgen die Grundlagen, um Desktop- und WebAssembly-Anwendungen oder Web-APIs zu erstellen, ohne sich in der Komplexität der Tiefen von Rust zu verlieren.

Zum Thema Web-APIs gibt es nicht nur einen Vortrag, sondern Rainer Stropke, der Autor dieses Artikels, bietet einen Workshop für diejenigen an, die tiefer in das Thema einsteigen und selbst Hand anlegen möchten.

Wie viele Makros die jeweiligen APIs verwenden, ist übrigens eine Unterscheidung, die über Rocket und Axum hinausgeht. Die Frameworks Actix Web und Warp setzen beispielsweise Makros ein, jedoch deutlich sparsamer als Rocket, und sie bieten alternative API-Varianten ohne Makros an.

Um die Unterschiede und Gemeinsamkeiten von Rocket und Axum kennenzulernen, dient im Folgenden auszugsweise Beispielcode für eine RESTful-Web-API zur Verwaltung einer To-do-Liste. Der Code setzt die üblichen Operationen wie Hinzufügen, Abfragen, Ändern und Löschen um. Die Web-API-Schnittstelle ist für beide Frameworks identisch. Der komplette Quellcode steht auf GitHub zur Verfügung. Das Repository enthält nicht nur den Code für Rocket und Axum, sondern darüber hinaus Implementierungen mit Actix Web, Warp und Spin, einem auf WebAssembly basierenden Framework.

Wie andere zeitgemäße Webframeworks benötigt Rust keinen externen Webserver. Eine Web-API ist technisch gesehen eine Kommandozeilenanwendung, die über die zugrundeliegenden Komponenten (Crates) Webserver und dazugehörige Technologien wie TLS-Verschlüsselung der übertragenen Daten mitbringt.

Bei Rocket markiert man die Einstiegsmethode mit dem Makro launch, das die main-Methode (asynchron dank Tokio) erzeugt:

/// Type for our shared state
///
/// In our sample application, we store the to-do
/// list in memory. As the state is shared 
/// between concurrently running web requests, 
/// we need to make it thread-safe. 
type Db = Arc<RwLock<TodoStore>>;

/// Rocket relies heavily on macros. 
/// The launch macro will generate a
/// tokio main function for us.
#[launch]
fn rocket() -> _ {
  // Initialize logging.
  // Rocket uses the log crate 
  // (https://crates.io/crates/log) to log requests. 
  // You can use any compatible logger, but for 
  // this example we'll use simplelog.
  // Enhancements in terms
  // of more flexible logging are planned 
  // for future releases
  // (https://github.com/SergioBenitez/Rocket/issues/21).
  SimpleLogger::init(LevelFilter::Debug, 
                     Config::default()).unwrap();

  // Create shared data store
  let db = Db::default();

  rocket::build()
    // Here we mount our routes. More details
    // about route mounting at 
    // https://rocket.rs/v0.5-rc/guide/overview/#mounting.
    .mount(
      "/",
      routes![get_todos, get_todo, add_todo, 
              update_todo, delete_todo, persist],
    )
    // Register our shared state.
    // More about using shared state at 
    // https://rocket.rs/v0.5-rc/guide/state/.
    .manage(db)
}

Der Code zeigt, wie intensiv Rocket auf Makros setzt. Neben dem launch-Makro findet man ein weiteres wichtiges Makro namens routes im Aufruf der mount-Methode. Dort erfolgt die Registrierung der API-Handler für die Web-API-Endpunkte.

Folgender Ausschnitt zeigt im Vergleich den Code zum Starten der Web-API in Axum:

/// Type for our shared state
///
/// In our sample application, we store the 
/// to-do list in memory. As the state is shared 
/// between concurrently running web requests, 
/// we need to make it thread-safe. 
type Db = Arc<RwLock<TodoStore>>;

#[tokio::main]
async fn main() {
  // Enable tracing using Tokio's tracing crate
  // (https://tokio.rs/#tk-lib-tracing)
  tracing_subscriber::registry()
    .with(tracing_subscriber::EnvFilter::new(
      std::env::var("RUST_LOG")
        .unwrap_or_else(
          |_| "todo_axum=debug,tower_http=debug".into()),
    ))
    .with(tracing_subscriber::fmt::layer())
    .init();

  // Create shared data store
  let db = Db::default();

  // We register our shared state so that 
  // handlers can get it using the State extractor.
  // Note that this will change in Axum 0.6. See more at
  // https://docs.rs/axum/0.6.0-rc.2/axum/
  //   index.html#sharing-state-with-handlers
  let app = Router::with_state(db)
    // Here we set up the routes. Note: no macros
    .route("/todos", get(get_todos).post(add_todo))
    .route("/todos/:id", 
           delete(delete_todo)
             .patch(update_todo)
             .get(get_todo))
    .route("/todos/persist", post(persist))
    // Using tower to add tracing layer
      .layer(ServiceBuilder::new()
               .layer(TraceLayer::new_for_http())
               .into_inner());

  let addr = SocketAddr::from(([0, 0, 0, 0], 3000));
  axum::Server::bind(&addr)
    .serve(app.into_make_service()).await.unwrap();
}

Auch wenn die Codepassagen von Rocket und Axum konzeptionell ähnlich sind, unterscheidet sich die Umsetzung deutlich. Zunächst ist erkennbar, dass Axum keine Makros einsetzt. Das einzige Makro im Code ist tokio::main, das nicht von Axum kommt. Das Framework verbirgt es aber nicht, da es bewusst dicht mit dem Tokio-Stack verwoben ist. Die enge Beziehung ist auch beim Logging sichtbar: Axum verwendet die tracing-Crate von Tokio. Die Logging-Middleware ist als Service mit den Tower-Crates aus dem Tokio-Stack umgesetzt und lässt sich daher in Axum verwenden, wie im Aufruf der Methode layer ersichtlich ist. Ebenso lassen sich andere Tower-Services in Verbindung mit Axum nutzen.