Introduction & Architecture

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against axum 0.8.9 (axum-core 0.5.6, axum-extra 0.12.6, axum-macros 0.5.1) on Rust 1.99 with the 2024 edition, tokio 1.53, tower 0.5 and tower-http 0.7, checked on 2026-10-06. axum’s minimum supported Rust version is 1.80. Where axum 0.7 behaved differently the difference is called out; changes already merged for the unreleased 0.9 are flagged as upcoming.

Why Rust for backend services

A backend service spends its life doing three things: waiting on the network, transforming data, and holding state for many concurrent clients. Rust is a good fit for all three:

  • Predictable latency and low memory. There is no garbage collector, so there are no collection pauses in the tail of the latency distribution, and a typical axum service idles at a few megabytes of resident memory. Containers can be packed densely, and cold starts on serverless platforms (AWS Lambda) are short.

  • Safety without a runtime cost. The ownership and borrowing rules (see Ownership & Moves) rule out use-after-free, double frees and data races at compile time. The Send and Sync traits extend that to the multi-threaded tokio scheduler: a handler that would share non-thread-safe state across tasks does not compile.

  • Expressive types for the request boundary. serde derives turn JSON into typed structs, newtypes make invalid input unrepresentable, and Result forces every error path to become a response.

  • Throughput. tokio’s work-stealing scheduler and hyper’s HTTP implementation are among the fastest available in any language, so the framework is rarely the bottleneck of a service.

The trade-offs are real: compile times are longer than with Python or Node.js, the trait-heavy APIs produce long compiler errors, and the ecosystem is younger than Spring’s or Django’s. Yoon (Beginning Axum, chapter 1) collects public case studies of teams moving services to Rust for latency and cost; the Incremental Adoption & Refactoring page discusses when such a move pays off. One claim from that chapter needs correcting: Rust does not catch memory leaks at compile time. Leaking is memory-safe in Rust — an Rc cycle or std::mem::forget leaks without any unsafe — so long-running services still need memory monitoring.

What axum is

axum is a web application framework maintained by the tokio project. Its crate documentation lists its high-level features, and each of them shapes how you write a service:

  • Macro-free routing. Routes are built with ordinary method calls — Router::new().route("/books/{isbn}", get(show)) — rather than attribute macros on functions. The router is a value you can compose, test and pass around.

  • Declarative request parsing with extractors. A handler is an async fn whose parameters (Path, Query, Json, State, …​) describe what to pull out of the request. If extraction fails, the extractor’s rejection becomes the response, and the handler never runs.

  • A simple, predictable error model. Handlers cannot "throw": whatever they return must implement IntoResponse, so every error is turned into an HTTP response explicitly (see Error Handling).

  • Minimal boilerplate for responses. Strings, status codes, Json, Html, tuples of headers and bodies all implement IntoResponse.

  • Full use of the tower and tower-http ecosystem. axum has no middleware system of its own. Timeouts, tracing, compression, CORS, authorisation and rate limiting are tower Layers, so they work in axum, in tonic (gRPC), in a raw hyper server and in lambda_http alike.

The last point is the key architectural decision. Everything in axum — the Router, a MethodRouter, a handler once it is given its state — is a tower::Service<Request>. The Tower Service Model page explains that trait in detail.

The tokio → hyper → tower → axum stack

Diagram of the axum stack as four layers under the application. From the bottom: tokio 1 (multi-thread scheduler, TcpListener, timers, spawn and spawn_blocking, sync primitives) gives execution; hyper 1 with hyper-util (HTTP/1.1 and HTTP/2 connection state machines, the http crate’s Request and Response types) gives the protocol; tower 0.5 with tower-http 0.7 (the Service and Layer traits, ServiceBuilder, HTTP middleware such as TraceLayer, CorsLayer, CompressionLayer, ServeDir and CsrfLayer) gives composition; axum 0.8 with axum-core, axum-extra and axum-macros (Router, Handler, extractors, IntoResponse, State, from_fn, axum::serve) gives ergonomics. The application on top holds handlers, AppState, AppError, the shelf-core domain crate and sqlx repositories. Requests flow up the stack and responses flow down.
Layer Responsibility

tokio

The asynchronous runtime: a work-stealing scheduler that polls futures on a pool of worker threads, non-blocking sockets (tokio::net::TcpListener), timers and synchronisation primitives. See Async Runtimes & Tokio.

hyper

A correct, fast HTTP/1 and HTTP/2 implementation. hyper 1.0 (November 2023) removed its built-in server; the hyper-util crate provides the connection-serving glue that axum::serve uses.

http, http-body

Shared vocabulary types: http::Request, http::Response, StatusCode, HeaderMap, and the http_body::Body trait for streaming bodies. axum re-exports them (axum::http).

tower

The Service trait (an async function from request to response with back-pressure) and Layer (a function from Service to Service), plus generic middleware: timeouts, rate and concurrency limits, load shedding, buffering.

tower-http

HTTP-specific tower middleware: tracing, request IDs, CORS, compression, body limits, static files, CSRF protection and more (see tower-http Catalog).

axum

Routing, extractors, responses, state, middleware::from_fn adapters, WebSockets, SSE and axum::serve.

You rarely depend on hyper or tower directly in a small application — axum re-exports what you need — but as soon as you write middleware or test the router in process you meet them by name, so it pays to know where each type comes from.

The request lifecycle

The sequence below follows one GET /api/v1/books/9780441013593 request through Bookshelf. Every other core page has a short "Where it runs in the request" section that places its topic on this diagram:

sequenceDiagram autonumber participant C as Client participant L as TcpListener (tokio) participant H as hyper connection task participant T as tower layers around the Router participant R as Router participant M as Router::layer middleware participant E as Extractors participant F as Handler C->>L: TCP connect L->>H: accept, spawn one task per connection C->>H: GET /api/v1/books/9780441013593 H->>T: http::Request with a streaming Body T->>R: NormalizePathLayer, outer TraceLayer ... R->>R: match path and method (matchit) R->>M: matched route: TimeoutLayer, CorsLayer, from_fn ... M->>E: FromRequestParts: State, Path ... E-->>M: a rejection short-circuits to a response M->>E: FromRequest (body extractor, last) E->>F: typed arguments F->>F: async work: database, cache F-->>M: impl IntoResponse becomes a Response M-->>T: Response (each layer sees it on the way out) T-->>H: Response H-->>C: HTTP/1.1 200 OK + JSON body
  1. tokio: axum::serve accepts each TCP connection on a tokio::net::TcpListener and spawns a tokio task that drives a hyper connection.

  2. hyper parses the request head (HTTP/1.1 by default, HTTP/2 when the http2 feature is enabled and the client negotiates it) and hands an http::Request<axum::body::Body> to the service it was given.

  3. tower layers wrapped around the whole Router (ServiceBuilder::new().layer(...).service(router) or Layer::layer(router)) run before routing, so they can rewrite the URI — NormalizePathLayer is the usual example.

  4. The Router matches the path (with the matchit crate) and then the HTTP method. No match calls the fallback (404 Not Found by default); a path match with the wrong method yields 405 Method Not Allowed.

  5. Middleware added with Router::layer, route_layer or MethodRouter::layer runs after routing, around the matched route. This is where most of an application’s layers live (see Applying Middleware & Ordering); each one sees the request on the way in and the response on the way out.

  6. Extractors run in parameter order: all FromRequestParts extractors first, then at most one body extractor (FromRequest). The first rejection becomes the response.

  7. The handler runs and returns something that implements IntoResponse, which is converted into an http::Response<Body>. The response travels back out through the middleware and the outer layers to hyper, which writes it to the socket; the body may stream (SSE, file downloads) long after the handler has returned.

Where it runs in the request

This page is the map the other pages refer to: tokio accepts the connection, hyper parses the request, tower layers wrap the router, the router matches a route, middleware runs around the matched route, extractors run, the handler runs, and the response travels back out the same way. The later pages each pin their topic to one of those steps.

A first look at the code

The smallest useful Bookshelf exposes a health check and one JSON endpoint backed by the shelf-core domain crate from the Rust Reference:

[package]
name = "bookshelf-web"
version = "0.1.0"
edition = "2024"

[dependencies]
axum = "0.8.9"
tokio = { version = "1.53", features = ["full"] }
serde = { version = "1", features = ["derive"] }
shelf-core = { path = "../shelf-core", features = ["serde"] }
use std::sync::{Arc, RwLock};

use axum::{
    Json, Router,
    extract::{Path, State},
    http::StatusCode,
    routing::get,
};
use shelf_core::{Author, Book, Catalog, InMemoryCatalog, Isbn};

type SharedCatalog = Arc<RwLock<InMemoryCatalog>>;

async fn health() -> &'static str {
    "ok"
}

async fn show_book(
    State(catalog): State<SharedCatalog>,
    Path(isbn): Path<String>,
) -> Result<Json<Book>, StatusCode> {
    let isbn = Isbn::parse(&isbn).map_err(|_| StatusCode::BAD_REQUEST)?;
    let catalog = catalog.read().expect("catalog lock poisoned");
    catalog.get(&isbn).cloned().map(Json).ok_or(StatusCode::NOT_FOUND)
}

fn app(catalog: SharedCatalog) -> Router {
    Router::new()
        .route("/health", get(health))
        .route("/api/v1/books/{isbn}", get(show_book))
        .with_state(catalog)
}

#[tokio::main]
async fn main() {
    let mut catalog = InMemoryCatalog::new();
    let dune = Isbn::parse("978-0-441-01359-3").expect("valid ISBN");
    catalog
        .add(Book::new(dune, "Dune", vec![Author::new("Frank Herbert")], 1965))
        .expect("empty catalogue");

    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.expect("port 3000 is free");
    axum::serve(listener, app(Arc::new(RwLock::new(catalog)))).await.expect("server error");
}

Source: axum: Example

$ curl -s localhost:3000/api/v1/books/9780441013593
{"isbn":"9780441013593","title":"Dune","authors":[{"name":"Frank Herbert"}],"year":1965}
$ curl -si localhost:3000/api/v1/books/123 | head -1
HTTP/1.1 400 Bad Request

Source: axum: Example

Every axum concept is already present: a Router built from method routers, two extractors (State and Path), a Result return type whose both arms implement IntoResponse, shared state, and axum::serve. The lock guard is dropped before the handler returns and is never held across an .await, which is why a std::sync::RwLock is the right choice here (see State & Dependency Injection).

The crate map

Crate Contents

axum 0.8.9

The framework: Router, routing, extract, response, middleware, handler, error_handling, body, serve, and the ws (WebSockets), multipart, json, form, query and macros feature-gated modules.

axum-core 0.5.6

The stable traits other crates implement: FromRequest, FromRequestParts, OptionalFromRequestParts, IntoResponse, IntoResponseParts, and Body. Library authors depend on axum-core instead of axum so their extractors do not pin an axum version.

axum-extra 0.12.6

Opt-in extras: TypedHeader, cookie jars, typed routing, Host, multi-value Query/Form, Either, JsonLines, ErasedJson, Attachment, FileStream, Protobuf (see axum-extra).

axum-macros 0.5.1

#[debug_handler], #[debug_middleware], #[derive(FromRef)], #[derive(FromRequest)], #[derive(FromRequestParts)] and #[derive(TypedPath)]; enabled through axum’s macros feature.

The repository also carries dozens of runnable examples at the axum-v0.8.9 tag; the pages of this section link the relevant ones, and the ecosystem list (ECOSYSTEM.md) points to community crates.

Versioning and release cadence

axum is still at 0.x, and Cargo treats every minor version (0.7 → 0.8) as a breaking release. The pattern so far has been a breaking release roughly once a year — 0.6 (November 2022), 0.7 (November 2023, with hyper 1.0), 0.8 (January 2025) — with frequent compatible 0.8.x patch releases in between. Pin the minor version (axum = "0.8.9" allows any 0.8.x at or above 0.8.9) and upgrade axum, axum-extra and tower-http together, because they share types.

No 0.9 has been released. The main branch already contains breaking changes for it: nested-router fallbacks are merged differently, serve applies hyper’s header-read timeout by default and its future’s output type changes, and new APIs such as ListenerExt::limit_connections, Serve::with_executor and the HTTP QUERY method appear. The Migration & Framework Comparison page collects the 0.6 → 0.7 → 0.8 migrations and these upcoming items.

Where axum fits

axum sits in the middle of the Rust web spectrum: lower-level than a batteries-included framework, higher-level than writing hyper services by hand.

  • actix-web 4 has its own actor-derived runtime layer and middleware system, and a long track record; it does not share tower middleware.

  • Rocket 0.5 uses attribute macros (#[get("/")]) and request guards and emphasises ergonomics; its release cadence has slowed.

  • Loco is a Rails-like framework built on axum, with generators, an ORM (SeaORM), background workers and mailers — a good choice when you want conventions rather than a toolkit.

  • Leptos and Dioxus use axum as the server for full-stack Rust applications (see Full-stack with Leptos & Dioxus).

If you know other stacks: an axum handler is close to a FastAPI path operation (typed parameters, a function per route), the extractor model resembles NestJS pipes and parameter decorators, and tower layers play the role of ASP.NET Core middleware or Spring filters. The comparison table on the Migration & Framework Comparison page goes into detail.

This section does not re-explain async Rust: read Async/Await & Futures and Async Runtimes & Tokio first if .await, Send futures and tokio::spawn are new to you.

Where the books are outdated

Crossley et al. (Rust for Scientific Computing, ch. 15, which the Rust Reference uses for topic coverage only) builds web examples on axum::Server and AddExtensionLayer; both are gone. Since axum 0.7, axum::serve(listener, router) replaces axum::Server::bind(...).serve(...), and shared data is passed with State (or the Extension extractor/layer). Yoon (Beginning Axum) targets axum 0.8 and is current on the core API, but uses tower-http 0.6; the tower-http 0.7 changes are covered on the middleware pages.