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.