What is the difference between axum::routing::MethodRouter::on and route for handling multiple HTTP methods?
MethodRouter::on builds a single router that can handle multiple HTTP methods on the same path by chaining method-specific handlers, while route creates a new route for each path and typically accepts a pre-built MethodRouter that specifies which methods to handle—the key difference is that on is a method on MethodRouter for adding method handlers, and route is a function on Router for attaching that router to a path. Understanding this distinction clarifies how axum composes routing: MethodRouter handles method dispatch for a single path, and Router maps paths to those method handlers.
The Two-Level Routing Problem
use axum::{
Router,
routing::{get, post, MethodRouter},
handler::Handler,
};
fn routing_levels() {
// Axum has two levels of routing:
// 1. PATH routing: Which path maps to which handler?
// 2. METHOD routing: Which HTTP method maps to which handler?
// Router handles path routing
// MethodRouter handles method routing
// The question: How do these compose?
// route() attaches a MethodRouter to a path
// on() adds a method handler to a MethodRouter
}Axum separates path routing from method routing, with distinct APIs for each.
Basic route Usage
use axum::{Router, routing::get, Json};
use std::net::SocketAddr;
async fn basic_route() {
// route() is a method on Router
// It maps a path to a MethodRouter
let app = Router::new()
// route() takes: (path, MethodRouter)
.route("/users", get(get_users));
// This creates a route that:
// - Matches path "/users"
// - Accepts only GET requests
// - Calls get_users for GET /users
}
async fn get_users() -> Json<Vec<String>> {
Json(vec!["Alice".to_string(), "Bob".to_string()])
}route attaches a MethodRouter to a specific path; the MethodRouter defines which methods are accepted.
Single Method with route
use axum::{Router, routing::{get, post, put, delete}};
async fn single_method_routes() {
// Each routing function (get, post, etc.) creates a MethodRouter
// for a single HTTP method
let app = Router::new()
// get() creates a MethodRouter that only accepts GET
.route("/items", get(get_items))
// post() creates a MethodRouter that only accepts POST
.route("/items", post(create_item))
// put() creates a MethodRouter that only accepts PUT
.route("/items/:id", put(update_item))
// delete() creates a MethodRouter that only accepts DELETE
.route("/items/:id", delete(delete_item));
// Note: This doesn't work as expected!
// Multiple route() calls with the same path will conflict
// The second route() for "/items" would replace the first
}Each method function (get, post, etc.) creates a MethodRouter handling only that method.
The Problem: Multiple Methods on Same Path
use axum::{Router, routing::{get, post}};
async fn multiple_methods_problem() {
// Problem: We want GET and POST on the same path
// WRONG APPROACH: Multiple route() calls
let app = Router::new()
.route("/items", get(get_items)) // This route...
.route("/items", post(create_item)); // ...replaced by this!
// The second route() replaces the first
// Result: Only POST /items works, GET /items returns 405
// CORRECT APPROACH: Build a MethodRouter with multiple methods
let app = Router::new()
.route("/items", get(get_items).post(create_item));
// Now both GET and POST work on /items
}
async fn get_items() {}
async fn create_item() {}Multiple route calls with the same path replace each other; you must combine methods in a single MethodRouter.
Using on to Add Method Handlers
use axum::{Router, routing::MethodRouter, http::Method};
async fn on_method() {
// on() is a method on MethodRouter
// It adds a handler for a specific HTTP method
// Starting with a MethodRouter from get()
let router: MethodRouter = get(get_items);
// on() adds another method handler
let router: MethodRouter = router.on(Method::POST, create_item);
// Attach to path with route()
let app = Router::new()
.route("/items", router);
// Now /items accepts both GET and POST
}
async fn get_items() {}
async fn create_item() {}on is a builder method on MethodRouter that adds handlers for additional HTTP methods.
Chaining Method Handlers
use axum::{Router, routing::{get, post, put, delete, patch}, http::Method};
async fn method_chaining() {
// The idiomatic way: chain method handlers on MethodRouter
let app = Router::new()
.route("/items",
get(get_items)
.post(create_item)
.put(update_item)
.delete(delete_item)
.patch(patch_item)
);
// Each method function (get, post, etc.) is a builder method
// that adds a handler to the MethodRouter
// Equivalent using on():
let app = Router::new()
.route("/items",
MethodRouter::new()
.on(Method::GET, get_items)
.on(Method::POST, create_item)
.on(Method::PUT, update_item)
.on(Method::DELETE, delete_item)
.on(Method::PATCH, patch_item)
);
}
async fn get_items() {}
async fn create_item() {}
async fn update_item() {}
async fn delete_item() {}
async fn patch_item() {}Method chaining with get().post().delete() is the idiomatic way to handle multiple methods on one path.
on vs Method Functions
use axum::{Router, routing::{get, MethodRouter}, http::Method};
async fn on_vs_method_functions() {
// Method functions (get, post, put, delete, patch, options, head, trace)
// These are convenience methods that call on() internally
// Using method functions (idiomatic):
let router: MethodRouter = get(get_handler).post(post_handler);
// Using on() (equivalent, more verbose):
let router: MethodRouter = MethodRouter::new()
.on(Method::GET, get_handler)
.on(Method::POST, post_handler);
// Method functions are shorthand:
// get(handler) = MethodRouter::new().on(Method::GET, handler)
// post(handler) = MethodRouter::new().on(Method::POST, handler)
// etc.
// When to use on() directly?
// 1. Custom/extension methods
let router = get(get_handler)
.on(Method::from_bytes(b"MYMETHOD").unwrap(), custom_handler);
// 2. Dynamic method configuration
let methods: Vec<Method> = vec![Method::GET, Method::POST];
let mut router = MethodRouter::new();
for method in methods {
router = router.on(method, dynamic_handler);
}
// 3. Bulk method registration from configuration
}
async fn get_handler() {}
async fn post_handler() {}
async fn custom_handler() {}
async fn dynamic_handler() {}Method functions (get, post, etc.) are convenience shorthands for on(Method::X, handler).
route Details: Attaching to Paths
use axum::{Router, routing::get};
async fn route_details() {
// route() is a method on Router
// Signature: fn route(path: &str, method_router: MethodRouter) -> Self
let app = Router::new()
// Each route() call adds a path -> MethodRouter mapping
.route("/users", get(list_users).post(create_user))
.route("/users/:id", get(get_user).put(update_user).delete(delete_user))
.route("/health", get(health_check));
// Key behaviors:
// 1. Path matching uses exact match (except for wildcards)
// 2. MethodRouter defines which methods are accepted
// 3. If method not in MethodRouter, returns 405 Method Not Allowed
// 4. If path not found, returns 404 Not Found
// You can add routes incrementally:
let mut app = Router::new();
app = app.route("/users", get(list_users));
app = app.route("/posts", get(list_posts));
}
async fn list_users() {}
async fn create_user() {}
async fn get_user() {}
async fn update_user() {}
async fn delete_user() {}
async fn health_check() {}
async fn list_posts() {}route takes a path and a MethodRouter, adding that mapping to the Router.
Handling Any Method
use axum::{Router, routing::MethodRouter, http::Method};
async fn handle_any_method() {
// MethodRouter::new() accepts no methods
// You then add methods with on() or method functions
let app = Router::new()
.route("/echo",
MethodRouter::new()
.on(Method::GET, echo)
.on(Method::POST, echo)
.on(Method::PUT, echo)
);
// Or use the any() shorthand to accept all methods:
let app = Router::new()
.route("/echo", MethodRouter::new().fallback(echo));
// Actually, there's a routing::any() function:
use axum::routing::any;
let app = Router::new()
.route("/echo", any(echo));
// any() creates a MethodRouter that accepts all methods
// It's like on() for every method
}
async fn echo() {}any() creates a MethodRouter that handles all HTTP methods with the same handler.
Fallback Handlers on MethodRouter
use axum::{Router, routing::get, http::Method};
async fn fallback_handlers() {
// MethodRouter has a fallback mechanism
// If a request method doesn't match, it can fall back to a handler
let app = Router::new()
.route("/items",
get(get_items)
.post(create_item)
.fallback(method_not_allowed)
);
// Now unsupported methods call method_not_allowed
// Instead of returning 405 Method Not Allowed
// This lets you customize the error response
// Common pattern: handle unsupported methods
async fn method_not_allowed() -> &'static str {
"Method not allowed. Supported: GET, POST"
}
// Another pattern: accept any method for certain endpoints
let app = Router::new()
.route("/webhook", get(webhook).fallback(webhook));
// Now both GET and any other method call webhook()
}
async fn get_items() {}
async fn create_item() {}
async fn webhook() {}MethodRouter::fallback provides a handler for unregistered methods instead of returning 405.
Method Routing and Extractors
use axum::{Router, routing::{get, post}, Json, extract::State, http::Method};
async fn method_extractors() {
// Handlers can access the request method via extractors
use axum::extract::OriginalUri;
use axum::http::Request;
let app = Router::new()
.route("/items", get(items_handler).post(items_handler));
async fn items_handler(
method: Method,
// Can also use Request for more details
) -> &'static str {
match method.as_str() {
"GET" => "Listing items",
"POST" => "Creating item",
_ => "Unknown method",
}
}
// Or use tower's Service with direct request access
use tower::Service;
use axum::body::Body;
let app = Router::new()
.route("/items",
get(|req: Request<Body>| async {
format!("GET request to {}", req.uri())
})
.post(|req: Request<Body>| async {
format!("POST request")
})
);
}Handlers can extract the HTTP method to vary behavior based on the request method.
Route Services vs Method Routers
use axum::{Router, routing::MethodRouter, handler::Handler};
use tower::Service;
use std::future::Future;
use http::{Request, Response};
use axum::body::Body;
async fn route_services() {
// route() can also accept a tower Service directly
// (via Route::new) instead of a MethodRouter
// MethodRouter: Dispatches based on HTTP method
let method_router: MethodRouter = get(get_handler).post(post_handler);
// Route<Service>: Handles all methods with the same service
use axum::routing::Route;
use tower::service_fn;
let service_route = Route::new(service_fn(|req: Request<Body>| async {
Ok::<_, &'static str>(Response::new(Body::from("All methods")))
}));
// When to use each:
// - MethodRouter: Different handlers for different methods (most common)
// - Route<Service>: Same handler for all methods, or custom dispatch logic
let app = Router::new()
.route("/api", method_router) // Method-based dispatch
.route("/ws", service_route); // Same service for all methods
}
async fn get_handler() {}
async fn post_handler() {}route can accept either a MethodRouter (method-based dispatch) or a Service (single handler for all methods).
Combining Multiple Routers
use axum::{Router, routing::get};
async fn combining_routers() {
// You can combine multiple Routers
let user_routes = Router::new()
.route("/users", get(list_users).post(create_user))
.route("/users/:id", get(get_user));
let post_routes = Router::new()
.route("/posts", get(list_posts).post(create_post))
.route("/posts/:id", get(get_post));
// Combine with merge
let app = Router::new()
.merge(user_routes)
.merge(post_routes);
// Or nest under a path prefix
let app = Router::new()
.nest("/api", user_routes)
.nest("/api", post_routes);
// nest() is different: it adds a path prefix to all routes
// So /users becomes /api/users
}
async fn list_users() {}
async fn create_user() {}
async fn get_user() {}
async fn list_posts() {}
async fn create_post() {}
async fn get_post() {}Multiple routers can be combined with merge or organized under path prefixes with nest.
Method Not Allowed Behavior
use axum::{Router, routing::get, http::StatusCode};
async fn method_not_allowed_behavior() {
// When a path exists but method doesn't match:
let app = Router::new()
.route("/items", get(get_items));
// GET /items -> 200 OK (handler runs)
// POST /items -> 405 Method Not Allowed
// DELETE /items -> 405 Method Not Allowed
// The 405 response includes an Allow header listing allowed methods
// To customize this behavior, use fallback:
let app = Router::new()
.route("/items",
get(get_items)
.fallback(|| async {
(StatusCode::METHOD_NOT_ALLOWED, "Use GET instead")
})
);
// Or accept specific additional methods:
let app = Router::new()
.route("/items",
get(get_items)
.options(options_handler) // Handle OPTIONS
);
}
async fn get_items() {}
async fn options_handler() {}When a path matches but method doesn't, axum returns 405 with an Allow header listing accepted methods.
Complete Example: REST API
use axum::{
Router,
routing::{get, post, put, delete},
extract::{Path, Json, State},
http::StatusCode,
};
use std::collections::HashMap;
use std::sync::Arc;
use parking_lot::Mutex;
// Complete REST API example demonstrating on and route
struct AppState {
items: Mutex<HashMap<u64, String>>,
}
async fn complete_rest_api() {
let state = Arc::new(AppState {
items: Mutex::new(HashMap::new()),
});
let app = Router::new()
// Collection endpoint: GET list, POST create
.route("/items",
get(list_items)
.post(create_item)
)
// Item endpoint: GET read, PUT update, DELETE delete
.route("/items/:id",
get(get_item)
.put(update_item)
.delete(delete_item)
)
// Health check: any method
.route("/health",
get(health_check)
.post(health_check)
)
.with_state(state);
// All method handlers for each path are combined into one MethodRouter
}
async fn list_items(
State(state): State<Arc<AppState>>,
) -> Json<Vec<(u64, String)>> {
let items = state.items.lock();
Json(items.iter().map(|(k, v)| (*k, v.clone())).collect())
}
async fn create_item(
State(state): State<Arc<AppState>>,
Json(body): Json<String>,
) -> (StatusCode, Json<u64>) {
let mut items = state.items.lock();
let id = items.len() as u64 + 1;
items.insert(id, body);
(StatusCode::CREATED, Json(id))
}
async fn get_item(
State(state): State<Arc<AppState>>,
Path(id): Path<u64>,
) -> Result<Json<String>, StatusCode> {
let items = state.items.lock();
items.get(&id)
.cloned()
.map(Json)
.ok_or(StatusCode::NOT_FOUND)
}
async fn update_item(
State(state): State<Arc<AppState>>,
Path(id): Path<u64>,
Json(body): Json<String>,
) -> StatusCode {
let mut items = state.items.lock();
if items.contains_key(&id) {
items.insert(id, body);
StatusCode::OK
} else {
StatusCode::NOT_FOUND
}
}
async fn delete_item(
State(state): State<Arc<AppState>>,
Path(id): Path<u64>,
) -> StatusCode {
let mut items = state.items.lock();
if items.remove(&id).is_some() {
StatusCode::NO_CONTENT
} else {
StatusCode::NOT_FOUND
}
}
async fn health_check() -> &'static str {
"OK"
}A REST API uses route to attach paths and chains method handlers on MethodRouter.
Dynamic Method Registration
use axum::{Router, routing::MethodRouter, http::Method};
use std::collections::HashMap;
async fn dynamic_registration() {
// Sometimes you want to register methods dynamically
// based on configuration or runtime state
struct EndpointConfig {
methods: Vec<Method>,
handler: fn() -> &'static str,
}
let endpoints: HashMap<&str, EndpointConfig> = [
("/api/v1/items", EndpointConfig {
methods: vec![Method::GET, Method::POST],
handler: || "items",
}),
("/api/v1/users", EndpointConfig {
methods: vec![Method::GET, Method::PUT, Method::DELETE],
handler: || "users",
}),
].into_iter().collect();
let mut app = Router::new();
for (path, config) in endpoints {
let mut router = MethodRouter::new();
for method in config.methods {
router = router.on(method, config.handler);
}
app = app.route(path, router);
}
// This is where on() shines: building MethodRouter dynamically
}on is useful when methods are determined at runtime rather than compile time.
Summary Table
fn summary() {
// | Concept | Type | Purpose |
// |----------------|---------------|--------------------------------|
// | route() | Router method | Maps path to MethodRouter |
// | on() | MethodRouter method | Adds handler for method |
// | get(), post() | MethodRouter builder | Convenience for on() |
// | any() | MethodRouter builder | Accepts all methods |
// | Pattern | Example |
// |----------------|--------------------------------------|
// | Single method | .route("/path", get(handler)) |
// | Multiple methods | .route("/path", get(h).post(h)) |
// | Dynamic method | .on(Method::GET, handler) |
// | All methods | .route("/path", any(handler)) |
// | MethodRouter method | Effect |
// |---------------------|-----------------------------------|
// | get(handler) | Add GET handler |
// | post(handler) | Add POST handler |
// | on(method, handler) | Add handler for specific method |
// | fallback(handler) | Handler for unregistered methods |
}Synthesis
Quick reference:
use axum::{Router, routing::{get, post, MethodRouter}, http::Method};
// route() attaches a MethodRouter to a path
let app = Router::new()
.route("/items", get(get_items)); // GET only
// Chain method functions on MethodRouter
let app = Router::new()
.route("/items", get(get_items).post(create_item).delete(delete_item));
// on() adds a method handler to MethodRouter
let router = MethodRouter::new()
.on(Method::GET, get_items)
.on(Method::POST, create_item);
// Attach to path
let app = Router::new()
.route("/items", router);
// Method functions are shorthand
// get(h) = MethodRouter::new().on(Method::GET, h)Key insight: The distinction is about composition at different levels. route is a Router method that maps a path to a MethodRouter. on is a MethodRouter method that adds a handler for a specific HTTP method. The method functions get, post, put, delete, patch, etc., are conveniences that build MethodRouter instances and internally use on. The pattern Router::new().route("/path", get(h).post(h)) is idiomatic: get(h) creates a MethodRouter for GET, .post(h) adds POST to that router, and route("/path", ...) attaches it to the path. Use on directly when you need dynamic method registration or custom HTTP methods; use the convenience functions for static, known methods. The fallback method on MethodRouter lets you handle all other methods instead of returning 405. This two-level dispatch (path → MethodRouter → handler) is axum's core routing architecture.
