Migration & Framework Comparison
|
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 (released 2026-04-14) with axum-extra 0.12.6 on Rust 1.99 (edition 2024), checked on 2026-10-06. Tutorials, books and answers on the web span axum 0.4 to 0.8, and each minor release before 1.0 breaks something. This page collects what changed, shows the current form, and compares axum with the other Rust web frameworks. The authoritative list of changes is the axum CHANGELOG (and axum-core’s, for the extractor traits).
From 0.6 to 0.7
axum 0.7 (November 2023) moved to hyper 1.0 and http-body 1.0. Code in the 0.6 style:
// axum 0.6 with hyper 0.14 -- does not compile with axum 0.7 or 0.8
use axum::{
Router, Server, TypedHeader,
body::Body,
extract::Path,
headers::UserAgent,
http::Request,
middleware::{self, Next},
response::Response,
routing::get,
};
async fn book(Path(id): Path<u32>, TypedHeader(agent): TypedHeader<UserAgent>) -> String {
format!("book {id} for {agent}\n")
}
async fn timing<B>(req: Request<B>, next: Next<B>) -> Response {
next.run(req).await
}
#[tokio::main]
async fn main() {
let app: Router<(), Body> = Router::new().route("/books/:id", get(book)).layer(middleware::from_fn(timing));
Server::bind(&"0.0.0.0:3000".parse().unwrap()).serve(app.into_make_service()).await.unwrap();
}
Source: axum CHANGELOG and Announcing axum 0.7.0
-
axum::Server(a re-export of hyper 0.14’s server) is gone: useaxum::serve(listener, app)with atokio::net::TcpListener, or hyper-util for full control. -
axum has its own
axum::body::Body;Router,Next,MethodRouterand the extractor traits are no longer generic over the body type, soRouter<S, B>becomesRouter<S>andNext<B>becomesNext.axum::extract::RequestisRequest<Body>.BoxBody,FullandEmptyare replaced byBody::new,Body::fromandBody::empty. -
TypedHeaderand theheadersre-export moved to axum-extra (featuretyped-header):axum_extra::TypedHeaderandaxum_extra::headers::UserAgent. -
hyper::body::to_bytesis gone; useaxum::body::to_bytes(body, limit)orhttp_body_util::BodyExt::collect. -
tower-http had to move to 0.5 with it; it is at 0.7 now.
From 0.7 to 0.8
axum 0.8 (January 2025) changed the path syntax and the extractor traits. The 0.7 style:
// axum 0.7 -- compiles with 0.7, panics or fails to compile with 0.8
#[async_trait]
impl<S: Send + Sync> FromRequestParts<S> for CurrentUser {
type Rejection = StatusCode;
async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, Self::Rejection> { /* ... */ }
}
async fn files(Path(path): Path<String>, Host(host): Host, user: Option<CurrentUser>) -> String { /* ... */ }
let app = Router::new()
.route("/books/:id", get(book))
.route("/files/*path", get(files))
.route("/ws", get(echo)); // Message::Text(String)
Source: axum CHANGELOG, axum-core CHANGELOG and Announcing axum 0.8.0
The same in 0.8:
use axum::{
Router,
extract::{
FromRequestParts, OptionalFromRequestParts, Path,
ws::{Message, WebSocket, WebSocketUpgrade},
},
http::{StatusCode, header, request::Parts},
response::Response,
routing::{any, get},
};
use axum_extra::{TypedHeader, headers::UserAgent};
struct CurrentUser(String);
// 0.8: native async fn in the trait -- no #[async_trait]
impl<S: Send + Sync> FromRequestParts<S> for CurrentUser {
type Rejection = StatusCode;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
let value = parts.headers.get("x-user").ok_or(StatusCode::UNAUTHORIZED)?;
Ok(CurrentUser(value.to_str().map_err(|_| StatusCode::BAD_REQUEST)?.to_owned()))
}
}
// 0.8: Option<CurrentUser> needs its own impl, so "absent" and "invalid" can differ
impl<S: Send + Sync> OptionalFromRequestParts<S> for CurrentUser {
type Rejection = StatusCode;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Option<Self>, Self::Rejection> {
if parts.headers.contains_key("x-user") {
<CurrentUser as FromRequestParts<S>>::from_request_parts(parts, state).await.map(Some)
} else {
Ok(None)
}
}
}
async fn book(Path(id): Path<u32>, TypedHeader(agent): TypedHeader<UserAgent>) -> String {
format!("book {id} for {agent}\n")
}
async fn files(Path(path): Path<String>, parts: Parts, user: Option<CurrentUser>) -> String {
// the Host extractor moved to axum-extra; reading the header is often enough
let host = parts.headers.get(header::HOST).and_then(|h| h.to_str().ok()).unwrap_or("-");
let who = user.map_or("anonymous".to_owned(), |u| u.0);
format!("{path} on {host} for {who}\n")
}
async fn echo(ws: WebSocketUpgrade) -> Response {
ws.on_upgrade(|mut socket: WebSocket| async move {
while let Some(Ok(Message::Text(text))) = socket.recv().await {
// 0.8: Text carries Utf8Bytes (was String); .as_str() reads it, .into() builds one
let reply = format!("echo: {}", text.as_str());
if socket.send(Message::Text(reply.into())).await.is_err() {
break;
}
}
})
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/books/{id}", get(book)) // was "/books/:id"
.route("/files/{*path}", get(files)) // was "/files/*path"
.route("/ws", any(echo)); // any(): WebSockets over HTTP/1.1 and HTTP/2
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
axum::serve(listener, app).await.unwrap(); // was axum::Server::bind(..).serve(..)
}
Source: axum CHANGELOG, axum-core CHANGELOG and Announcing axum 0.8.0
$ curl -A curl/8.5 localhost:3000/books/42
book 42 for curl/8.5
$ curl -H 'x-user: ada' localhost:3000/files/covers/42.png
covers/42.png on localhost:3000 for ada
# and with the old syntax, at start-up:
thread 'main' panicked: 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.
Source: axum CHANGELOG, axum-core CHANGELOG and Announcing axum 0.8.0
-
Path syntax:
/:idbecomes/{id}and/*restbecomes/{*rest}. The old syntax panics when the route is added, so the mistake shows up at start-up rather than as silent 404s.{{and}}escape literal braces. -
Extractors:
FromRequestandFromRequestPartsuse nativeasync fnin traits; remove#[async_trait], or the impl fails withE0195(see Custom Extractors). -
Optional extractors:
Option<T>works only for types implementingOptionalFromRequestParts(orOptionalFromRequest), and it no longer swallows every error:Option<Path<T>>rejects malformed values instead of returningNone.Option<Query<T>>was removed; useQuery<Option<T>>fields oraxum_extra::extract::OptionalQuery. -
Synchandlers: handlers and services added to aRoutermust beSync. A handler future holding a non-Syncvalue is fine; a handler type capturing, say, aCellis not. -
WebSockets:
Message::TextcarriesUtf8BytesandMessage::BinarycarriesBytes(wasStringandVec<u8>);WebSocket::closewas removed (send aMessage::Close). Routing withanyinstead ofgetenables WebSockets over HTTP/2. -
Host: the
Hostextractor moved to axum-extra, where it is now deprecated; read theHostheader (orX-Forwarded-Hostfrom a trusted proxy) yourself. -
serve:
axum::serveis generic over the listener;Serve::tcp_nodelaywas replaced byserve::ListenerExt::tap_io. -
Additions worth adopting:
method_not_allowed_fallback,NoContent,Router::reset_fallback(0.8.4) and WebSocket subprotocol selection (0.8.9).
Upcoming in 0.9
The axum repository’s main branch lists breaking changes for 0.9 under "Unreleased"; there is no release date, and
none of this applies to 0.8.9:
-
nested routers' fallbacks are merged into the outer router’s;
-
axum::serveapplies a header-read timeout (closing the slow-loris gap described in Request Limits & Timeouts) and the output type of its future changes; -
new
serve::ListenerExt::limit_connections,Serve::with_executor, connection-age limits and support for the HTTPQUERYmethod.
Ecosystem upgrades that move together
| Crate | Versions | What to change |
|---|---|---|
tower-http |
0.6 → 0.7 |
|
sqlx |
0.8 → 0.9 |
non-literal SQL needs |
SeaORM |
1.x → 2.0 |
new entity format with |
jsonwebtoken |
9 → 11 |
a crypto backend feature ( |
utoipa |
4/5 → 6 |
OpenAPI 3.2 output, utoipa-axum 0.3 and utoipa-swagger-ui 10 (OpenAPI with utoipa) |
askama |
0.12 → 0.13+ |
|
OpenTelemetry |
0.31 → 0.33 |
upgrade opentelemetry, the SDK, the exporter and tracing-opentelemetry 0.34 as one set (OpenTelemetry & Distributed Tracing) |
tower-sessions / axum-login |
0.14 / 0.18 |
axum-login 0.18 requires tower-sessions 0.14, not the current 0.15 (Sessions & Cookies) |
Upgrade one row at a time, with cargo tree -d to spot duplicated versions, and keep Cargo.lock committed.
Outdated in the books
| Area | In the books → current |
|---|---|
axum |
|
tower-http |
0.6 (Yoon; Gangappa & Rangaraj) → 0.7: |
Databases |
SeaORM 1.0 (Yoon, chapter 3) → 2.0; sqlx 0.8 (Gangappa & Rangaraj) → 0.9 |
Authentication |
bcrypt (Yoon, chapter 4) → argon2, hashed in |
Templates |
|
OpenTelemetry |
0.31 (Gangappa & Rangaraj) → 0.33 with tracing-opentelemetry 0.34 |
Docker |
|
Practices |
plain-text passwords and a JWT secret in a global (Yoon, chapters 3-4), CPU-heavy hashing inside async handlers,
|
Comparing Rust web frameworks
| Framework | Latest (2026-10-06) | Character | Choose it when |
|---|---|---|---|
axum |
0.8.9 (2026-04) |
Tokio project; handlers are plain async functions, extractors are types, middleware is tower; no macros needed |
you want the tower/hyper ecosystem, composable middleware and the largest share of current examples |
actix-web |
4.15.0 (2026-08) |
its own actor-free runtime on top of Tokio (one single-threaded runtime per worker), routing macros, stable 4.x API since 2022 |
you value a long-stable API and its mature middleware set |
Rocket |
0.5.1 (2024-05) |
batteries-included, macro-driven routing and request guards, typed forms, fairings |
you want Rails-like conveniences and accept a slow release cadence |
warp |
0.4.3 (2026-05) |
composable filters combined with |
you like the filter style; few new projects choose it |
poem |
3.1.12 (2025-07) |
axum-like handlers with an integrated OpenAPI layer ( |
OpenAPI-first APIs with code generation from types |
Loco |
1.2.0 (2026-09) |
a Rails-like framework on top of axum: generators, SeaORM models, mailers, background workers |
a full-stack application with conventions rather than assembling crates yourself |
The same ideas in other stacks this site covers:
| Concept | axum | FastAPI | NestJS | Spring Boot | ASP.NET Core minimal APIs |
|---|---|---|---|---|---|
Handler |
async fn |
|
controller method |
|
|
Input binding |
extractors ( |
type hints + Pydantic |
decorators + DTOs |
|
parameter binding |
Middleware |
tower layers |
ASGI middleware, dependencies |
middleware, guards, interceptors |
filters, interceptors |
middleware pipeline |
Dependency injection |
|
|
providers (container) |
beans (container) |
services (container) |
Laravel follows the controller-and-container model of Spring Boot and NestJS. See FastAPI, NestJS, Spring Boot, ASP.NET Core and Laravel on this site. A broader comparison of backend and web frameworks across languages is planned (issue #263).