axum-core-router
Router<S> is the central routing type in Axum. It maps request paths to
handlers through method-routers, composes sub-routers, and is itself a
tower::Service. The generic S is the state type the router still requires;
a serveable router is Router<()>.
When To Use This Skill
- Defining routes with
.route()and method-routers - Mounting sub-routers with
.nest()or flattening them with.merge() - Wiring middleware with
.layer()or.route_layer() - The server panics at startup with a routing or path message
- A request returns
404where405is expected, or the reverse - Middleware runs on some routes but not others
Quick Reference
Router methods (axum 0.7 and 0.8 share these signatures)
| Method | Purpose |
|---|---|
Router::new() |
Empty router, answers 404 to every request |
.route(path, mr) |
Register one path to a MethodRouter |
.route_service(path, svc) |
Register a path to a raw tower::Service |
.nest(path, router) |
Mount a sub-router under a prefix (prefix stripped) |
.nest_service(path, svc) |
Mount a tower::Service under a prefix |
.merge(other) |
Flatten another router's routes into this one |
.fallback(handler) |
Handler for unmatched paths (true 404) |
.fallback_service(svc) |
Service variant of .fallback |
.method_not_allowed_fallback(h) |
Router-wide 405 handler |
.reset_fallback() |
Clear the fallback, required before some .merge() calls |
.with_state(state) |
Provide state, turning Router<S> into Router<()> |
.layer(L) |
Wrap all routes registered SO FAR in middleware |
.route_layer(L) |
Wrap routes, but run only when a route matches |
.without_v07_checks() |
Disable the 0.7-path-syntax startup panic |
Full signatures, where-clauses, and documented panics: references/methods.md.
Method-router functions (axum::routing)
get, post, put, delete, patch, head, options, connect, trace,
any (matches every method), and on(filter, handler) (takes a MethodFilter).
Each function has a *_service sibling (get_service, post_service, ...) that
accepts a tower::Service instead of a handler. Each returns a MethodRouter;
the same names chain on a MethodRouter to register several methods on one path.
Path parameter syntax
| Capture | axum 0.7 | axum 0.8 |
|---|---|---|
| Single segment | /users/:id |
/users/{id} |
| Catch-all wildcard | /assets/*path |
/assets/{*path} |
ALWAYS match the path syntax to the target Axum version. The 0.7 syntax on a 0.8
router panics at router construction (startup), never at request time. This is
deliberate, to prevent a silent routing regression. NEVER expect /:id to be
treated as a literal segment on 0.8.
404 versus 405
| Situation | Response | Customize with |
|---|---|---|
| No route path matches at all | 404 Not Found |
Router::fallback |
| Path matched, method not registered | 405 Method Not Allowed |
MethodRouter::fallback (one path) or Router::method_not_allowed_fallback (router-wide) |
A path that matched with an unsupported method NEVER reaches Router::fallback.
Decision Trees
Choosing the composition method
Need to add request handling to a Router?
├── A single path to a handler?
│ └── .route(path, get(handler)...)
├── A single path to a raw tower::Service (not a handler, not a Router)?
│ └── .route_service(path, service)
├── A whole sub-Router under a URL prefix (prefix stripped from child)?
│ └── .nest(prefix, sub_router)
├── A tower::Service mounted under a URL prefix?
│ └── .nest_service(prefix, service) // e.g. ServeDir
└── Another Router's routes flattened in at the SAME level (no prefix)?
└── .merge(other_router)
NEVER pass a Router to .route_service(); it panics. Use .nest() for a
nested Router. .nest() adds a prefix; .merge() does not.
.layer() versus .route_layer()
Applying a tower::Layer to a Router?
├── Middleware must run on EVERY request, including unmatched paths?
│ │ (logging, tracing, CORS, compression, timeout)
│ └── .layer(L) // place AFTER all routes it must cover
└── Middleware must run ONLY when a route matched, and a genuine 404
must stay 404 (not become 401/403)?
│ (authentication and authorization guards)
└── .route_layer(L) // place AFTER the routes it guards
.layer() wraps only the routes registered BEFORE the call. ALWAYS register
.layer() after every route it must cover. .route_layer() runs only on a
matched route, so an auth layer added with it can NEVER turn a real 404 into a
401. .route_layer() panics if no routes have been added yet.
Path syntax for the target version
Writing a route path string?
├── Targeting axum 0.8?
│ ├── Single capture -> "/users/{id}"
│ └── Catch-all -> "/assets/{*path}"
├── Targeting axum 0.7?
│ ├── Single capture -> "/users/:id"
│ └── Catch-all -> "/assets/*path"
└── Need a LITERAL ':' or '*' segment on axum 0.8?
└── .without_v07_checks() // escape hatch ONLY, never a migration shortcut
Patterns
Pattern: basic router and method chaining
// axum 0.8 - method routers chained on one path
use axum::{Router, routing::get};
let app: Router = Router::new()
.route("/", get(root))
.route("/users", get(list_users).post(create_user))
.route("/users/{id}", get(show_user).put(update_user).delete(delete_user));
// axum 0.7 - identical, except the path-capture syntax
let app: Router = Router::new()
.route("/users/:id", get(show_user).put(update_user));
Chaining several method-routers on one path registers multiple HTTP methods.
A get route also answers HEAD requests, with the response body removed.
Pattern: nesting and recovering the mount prefix
// axum 0.8 - nest strips the matched prefix from the child's view
use axum::{Router, routing::get, extract::NestedPath};
async fn show(nested: NestedPath) -> String {
// NestedPath recovers the prefix this router was mounted under
format!("mounted under {}", nested.as_str()) // "/api/v1"
}
let api = Router::new().route("/show", get(show)); // child path is relative
let app: Router = Router::new().nest("/api/v1", api); // serves GET /api/v1/show
.nest() strips the matched prefix before the child router sees the request, so
child routes are written relative to the mount point. ALWAYS use the NestedPath
extractor (axum 0.8) when a nested handler must build an absolute URL, because the
child does not otherwise know its own prefix. .nest() panics if the prefix is
empty or contains a wildcard.
Pattern: merging routers and the double-fallback fix
// axum 0.8 - merge flattens routes at the same level (no prefix added)
let users = Router::new().route("/users", get(list_users));
let posts = Router::new().route("/posts", get(list_posts));
let app: Router = Router::new().merge(users).merge(posts);
// axum 0.8 - two routers that BOTH set a fallback cannot merge directly
let a = Router::new().route("/a", get(ha)).fallback(fallback_a);
let b = Router::new().route("/b", get(hb)).fallback(fallback_b);
let app: Router = a.merge(b.reset_fallback()); // clear one fallback first
.merge() requires both routers to share the state type S. Merging two routers
that each define a fallback panics, because a Router allows only one fallback.
ALWAYS call .reset_fallback() on one router before merging when both have one.
Pattern: distinct 404 and 405 handling
// axum 0.8 - separate handlers for unmatched path and wrong method
use axum::{Router, routing::get, http::StatusCode};
async fn not_found() -> (StatusCode, &'static str) {
(StatusCode::NOT_FOUND, "no such resource")
}
async fn wrong_method() -> (StatusCode, &'static str) {
(StatusCode::METHOD_NOT_ALLOWED, "method not allowed")
}
let app: Router = Router::new()
.route("/users", get(list_users)) // GET ok; POST /users -> 405
.fallback(not_found) // unknown path -> 404
.method_not_allowed_fallback(wrong_method); // matched path, wrong method -> 405
A freshly built MethodRouter responds 405 Method Not Allowed by default and
sets the Allow header. Router::method_not_allowed_fallback applies one 405
handler across every method-router registered so far. NEVER expect a wrong-method
request to reach Router::fallback; it produces a 405, not a 404.
Pattern: layer placement
// axum 0.8 - route_layer guards matched routes; layer wraps everything above
use axum::{Router, routing::get};
use tower_http::trace::TraceLayer;
let app: Router = Router::new()
.route("/public", get(public))
.route("/private", get(private))
.route_layer(require_auth()) // runs only on a matched route
.layer(TraceLayer::new_for_http()); // wraps both routes, runs after routing
Stacked Router::layer() calls nest bottom-to-top: the LAST .layer() is the
outermost. A tower::ServiceBuilder stack is the opposite (first = outermost).
NEVER mix the two mental models; see axum-impl-middleware for the full ordering
rule.
Pattern: serving the router with state
// axum 0.8 and 0.7 - identical
#[derive(Clone)]
struct AppState { /* db pool, config */ }
let app: Router = Router::new()
.route("/", get(handler))
.with_state(AppState { /* ... */ }); // Router<AppState> -> Router<()>
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
axum::serve accepts only a Router<()>. A Router<S> with S != () does not
type-check against serve, so a forgotten .with_state() is a compile error,
never a runtime surprise. State details: see axum-core-state.
Pattern: route precedence and catch-all
// axum 0.8 - a static route always beats a dynamic capture
let app: Router = Router::new()
.route("/users/me", get(current_user)) // GET /users/me hits THIS
.route("/users/{id}", get(show_user)) // GET /users/42 hits this
.route("/assets/{*path}", get(serve)); // catch-all: /assets/a, /assets/a/b
Static segments take precedence over dynamic captures; /users/me and
/users/{id} do not overlap. A catch-all /{*path} does NOT match the empty
segment, so /assets/{*path} matches /assets/a but not /assets. Two routes
that genuinely overlap (two captures at the same position) panic at router
construction. An empty path string panics.
Common Mistakes
- 0.7 path syntax (
/:id,/*path) on a 0.8 router: panics at startup. Convert every path to{id}/{*path}. Seereferences/anti-patterns.md. .layer()placed before some routes: those later routes are not wrapped.- Merging two routers that both set a fallback: panics. Call
.reset_fallback(). - Expecting a wrong-method request to return
404: it returns405. - Passing a
Routerto.route_service(): panics. Use.nest().
Reference Links
references/methods.md: completeRouterandMethodRouterAPI signatures, where-clauses, and every documented panic.references/examples.md: full working, version-annotated routing examples.references/anti-patterns.md: real routing mistakes with the WHY behind each failure and the fix.
Related Skills
axum-core-version-migration: the full 0.7 to 0.8 breaking-change matrix, including the path-syntax change.axum-impl-middleware:.layer()versus.route_layer()ordering in depth.axum-core-state:with_state, theState<S>extractor, andFromRef.