What is the difference between axum::routing::get and MethodRouter::on for HTTP method-specific handler registration?

axum::routing::get is a convenience function that creates a MethodRouter pre-configured for the GET method, while MethodRouter::on is a method on an existing MethodRouter that adds a route for a specified HTTP method—get is for the common case of a single-method route, while on enables method-specific routing on a combined router. The get function creates a fresh MethodRouter that handles only GET requests; on allows you to add handlers for specific methods to an existing router. Under the hood, get(handler) is equivalent to MethodRouter::new().on(Method::GET, handler), but the convenience functions are more ergonomic for the common case of single-method routes.

Basic GET Route Registration

use axum::{routing::get, Router, handler::Handler;
 
async fn handle_get() -> &'static str {
    "GET response"
}
 
fn basic_get_route() -> Router {
    // Using the convenience function
    Router::new().route("/path", get(handle_get))
}

The get function creates a MethodRouter that handles GET requests for this route.

MethodRouter::on for Custom Methods

use axum::{routing::{get, MethodRouter}, Router, http::Method};
 
async fn handle_get() -> &'static str {
    "GET response"
}
 
async fn handle_post() -> &'static str {
    "POST response"
}
 
fn using_on() -> Router {
    // Using on() with explicit method
    Router::new().route("/path", 
        MethodRouter::new()
            .on(Method::GET, handle_get)
            .on(Method::POST, handle_post)
    )
}
 
fn equivalent_convenience() -> Router {
    // Equivalent using convenience functions
    Router::new().route("/path", 
        get(handle_get).post(handle_post)
    )
}

on allows specifying the method as a parameter; convenience functions embed the method.

The get Function Implementation

use axum::{routing::MethodRouter, http::Method, handler::Handler;
 
// The get function is essentially:
fn get_approx<H, T>(handler: H) -> MethodRouter
where
    H: Handler<T>,
{
    MethodRouter::new().on(Method::GET, handler)
}
 
// Similarly for post, put, delete, etc.:
fn post_approx<H, T>(handler: H) -> MethodRouter
where
    H: Handler<T>,
{
    MethodRouter::new().on(Method::POST, handler)
}
 
// The convenience functions are shortcuts for on()

Convenience functions like get wrap MethodRouter::on with the method pre-specified.

Combining Multiple Methods

use axum::{routing::get, Router, http::Method};
 
async fn handle_get() -> &'static str { "GET" }
async fn handle_post() -> &'static str { "POST" }
async fn handle_put() -> &'static str { "PUT" }
 
fn combining_methods() {
    // Convenience function chaining
    let router1 = Router::new().route("/api", 
        get(handle_get)
            .post(handle_post)
            .put(handle_put)
    );
    
    // Using on() explicitly
    use axum::routing::MethodRouter;
    let router2 = Router::new().route("/api",
        MethodRouter::new()
            .on(Method::GET, handle_get)
            .on(Method::POST, handle_post)
            .on(Method::PUT, handle_put)
    );
    
    // Both are equivalent
}

Chaining convenience functions or calling on multiple times both work.

When to Use on vs Convenience Functions

use axum::{routing::{get, MethodRouter}, Router, http::Method};
 
fn when_to_use_which() {
    // Use convenience functions (get, post, etc.) when:
    // - Standard HTTP methods
    // - Single method per route
    // - Code readability is priority
    
    // Single method - use convenience
    let single = Router::new().route("/users", get(list_users));
    
    // Multiple methods - chain convenience functions
    let multiple = Router::new().route("/users", 
        get(list_users).post(create_user)
    );
    
    // Use on() when:
    // - Non-standard HTTP methods
    // - Dynamic method selection
    // - Building routers programmatically
    
    // Non-standard method
    let custom = Router::new().route("/webhook",
        MethodRouter::new().on(Method::from_bytes(b"WEBHOOK").unwrap(), handle_webhook)
    );
    
    // Dynamic method selection
    let methods = vec
![Method::GET, Method::POST];
    let mut router = MethodRouter::new();
    for method in methods {
        router = router.on(method, dynamic_handler);
    }
}
 
async fn list_users() -> &'static str { "users" }
async fn create_user() -> &'static str { "created" }
async fn handle_webhook() -> &'static str { "webhook" }
async fn dynamic_handler() -> &'static str { "dynamic" }

Use convenience functions for standard cases; on for flexibility.

MethodRouter Construction

use axum::{routing::{get, MethodRouter}, http::Method};
 
fn method_router_construction() {
    // Convenience function creates MethodRouter
    let router1: MethodRouter = get(handler);
    
    // on() also returns MethodRouter (builder pattern)
    let router2: MethodRouter = MethodRouter::new().on(Method::GET, handler);
    
    // They're the same type
    let router3: MethodRouter = get(handler).post(handler);
    let router4: MethodRouter = MethodRouter::new()
        .on(Method::GET, handler)
        .on(Method::POST, handler);
    
    // Both produce identical MethodRouter instances
}
 
async fn handler() -> &'static str { "ok" }

Both approaches produce MethodRouter values that can be used interchangeably.

Standard Convenience Functions

use axum::routing::{get, post, put, patch, delete, head, options, trace, connect};
 
fn standard_functions() {
    // axum provides convenience functions for standard methods:
    
    // get(handler) -> MethodRouter for GET
    // post(handler) -> MethodRouter for POST
    // put(handler) -> MethodRouter for PUT
    // patch(handler) -> MethodRouter for PATCH
    // delete(handler) -> MethodRouter for DELETE
    // head(handler) -> MethodRouter for HEAD
    // options(handler) -> MethodRouter for OPTIONS
    // trace(handler) -> MethodRouter for TRACE
    // connect(handler) -> MethodRouter for CONNECT
    
    // These are the common methods; on() covers everything else
}

Convenience functions cover standard HTTP methods; on handles any method.

Dynamic Method Routing

use axum::{routing::MethodRouter, http::Method, Router};
 
fn dynamic_routing() {
    // Build router dynamically based on configuration
    let config = vec
![
        (Method::GET, list_items as fn() -> &'static str),
        (Method::POST, create_item as fn() -> &'static str),
        (Method::DELETE, delete_item as fn() -> &'static str),
    ];
    
    let mut router = MethodRouter::new();
    for (method, handler) in config {
        router = router.on(method, handler);
    }
    
    // This pattern allows runtime configuration of routes
    
    // Can't do this easily with convenience functions:
    // The method is embedded in the function name
}
 
async fn list_items() -> &'static str { "list" }
async fn create_item() -> &'static str { "create" }
async fn delete_item() -> &'static str { "delete" }

on enables runtime-determined method routing; convenience functions are compile-time.

MethodRouter State

use axum::{routing::{get, MethodRouter}, Router, http::Method};
 
fn method_router_state() {
    // MethodRouter tracks:
    // 1. Which methods have handlers
    // 2. The handlers for each method
    // 3. Any fallback for unhandled methods
    
    // Convenience functions initialize this state
    let get_only: MethodRouter = get(handler);
    // Only GET is handled
    
    // on() adds to the state
    let get_and_post: MethodRouter = MethodRouter::new()
        .on(Method::GET, handler)
        .on(Method::POST, handler);
    // GET and POST are handled
    
    // Chaining adds to existing state
    let all_three: MethodRouter = get(handler)
        .post(handler)
        .put(handler);
    // GET, POST, PUT handled
}
 
async fn handler() -> &'static str { "ok" }

MethodRouter accumulates method-handler pairs; both approaches add to this state.

Fallback Behavior

use axum::{routing::{get, MethodRouter}, Router, http::StatusCode};
 
fn fallback_behavior() {
    // Unhandled methods return 405 Method Not Allowed
    
    // This applies to both approaches:
    let router = Router::new().route("/path", get(handler));
    // POST /path -> 405 Method Not Allowed
    
    let router = Router::new().route("/path",
        MethodRouter::new().on(axum::http::Method::GET, handler)
    );
    // POST /path -> 405 Method Not Allowed (same behavior)
    
    // Can add fallback handler:
    let router = Router::new().route("/path",
        get(handler).fallback(fallback_handler)
    );
}
 
async fn handler() -> &'static str { "handled" }
async fn fallback_handler() -> (StatusCode, &'static str) {
    (StatusCode::METHOD_NOT_ALLOWED, "not allowed")
}

Both produce the same 405 behavior for unhandled methods.

Method Guarding with on

use axum::{routing::MethodRouter, http::Method, Router};
 
fn method_guarding() {
    // on() can be used for method validation patterns
    
    // Custom method checking
    fn methods_router(
        get_handler: fn() -> &'static str,
        post_handler: fn() -> &'static str,
    ) -> MethodRouter {
        MethodRouter::new()
            .on(Method::GET, get_handler)
            .on(Method::POST, post_handler)
    }
    
    // Programmatic method filtering
    fn safe_methods_only(handler: fn() -> &'static str) -> MethodRouter {
        let mut router = MethodRouter::new();
        for method in [Method::GET, Method::HEAD, Method::OPTIONS] {
            router = router.on(method, handler);
        }
        router
    }
    
    // This pattern is harder with convenience functions
}
 
async fn generic_handler() -> &'static str { "safe" }

on enables programmatic construction of method routing patterns.

Nesting and Composition

use axum::{routing::{get, MethodRouter}, Router, http::Method};
 
fn composition() {
    // Both approaches compose the same way
    
    // Convenience function approach
    let api_routes = Router::new()
        .route("/users", get(list_users).post(create_user))
        .route("/items", get(list_items));
    
    // on() approach
    let api_routes = Router::new()
        .route("/users", 
            MethodRouter::new()
                .on(Method::GET, list_users)
                .on(Method::POST, create_user)
        )
        .route("/items",
            MethodRouter::new().on(Method::GET, list_items)
        );
    
    // Both produce equivalent routers
}
 
async fn list_users() -> &'static str { "users" }
async fn create_user() -> &'static str { "created" }
async fn list_items() -> &'static str { "items" }

Both approaches work identically when composing routes.

Type Signatures

use axum::{routing::MethodRouter, http::Method};
 
fn type_signatures() {
    // Both produce MethodRouter<_, _, _>
    
    // Convenience functions:
    // fn get<H, T, B>(handler: H) -> MethodRouter<_, B, Infallible>
    // where H: Handler<T, B>
    
    // on method:
    // fn on<H, T, B>(self, method: Method, handler: H) -> MethodRouter<_, B, Infallible>
    // where H: Handler<T, B>
    
    // The key difference in signature:
    // - get: takes handler, returns new MethodRouter
    // - on: takes self + method + handler, returns MethodRouter
    
    // This means:
    // - get() creates from scratch
    // - on() extends existing (self)
}

get creates a new MethodRouter; on extends an existing one.

Any Method with on

use axum::{routing::MethodRouter, http::Method, Router};
 
fn any_method() {
    // Method::ANY is not a real HTTP method
    // But some frameworks use "any" to match all methods
    
    // In axum, you handle this differently:
    // Use .fallback() or specify methods explicitly
    
    // To accept ANY method (including extensions):
    let router = Router::new().route("/path",
        MethodRouter::new().fallback(any_handler)
    );
    
    // Or explicitly list all standard methods:
    let router = Router::new().route("/path",
        MethodRouter::new()
            .on(Method::GET, any_handler)
            .on(Method::POST, any_handler)
            .on(Method::PUT, any_handler)
            // ... etc
    );
}
 
async fn any_handler() -> &'static str { "any method" }

axum uses fallback for handling arbitrary methods; on still takes specific methods.

Readability Comparison

use axum::{routing::get, Router, http::Method, routing::MethodRouter};
 
fn readability() {
    // Convenience functions: clear and concise
    let readable = Router::new()
        .route("/users", get(list_users).post(create_user))
        .route("/items", get(list_items).delete(delete_item));
    
    // on(): more explicit but verbose
    let explicit = Router::new()
        .route("/users",
            MethodRouter::new()
                .on(Method::GET, list_users)
                .on(Method::POST, create_user)
        )
        .route("/items",
            MethodRouter::new()
                .on(Method::GET, list_items)
                .on(Method::DELETE, delete_item)
        );
    
    // Readable code prefers convenience functions
    // Unless there's a specific reason for on()
}
 
async fn list_users() -> &'static str { "users" }
async fn create_user() -> &'static str { "create" }
async fn list_items() -> &'static str { "items" }
async fn delete_item() -> &'static str { "delete" }

Convenience functions are more readable for standard cases.

Synthesis

The relationship:

// get(handler) is essentially:
// MethodRouter::new().on(Method::GET, handler)
 
// The convenience functions are thin wrappers around on()
// get(handler) <=> MethodRouter::new().on(Method::GET, handler)
// post(handler) <=> MethodRouter::new().on(Method::POST, handler)
// put(handler) <=> MethodRouter::new().on(Method::PUT, handler)
// ... etc

When to use convenience functions:

// Standard HTTP methods
Router::new().route("/api", get(handler))
Router::new().route("/api", get(get_handler).post(post_handler))
 
// Multiple standard methods
Router::new().route("/api", 
    get(get_handler)
        .post(post_handler)
        .put(put_handler)
        .delete(delete_handler)
)
 
// When readability matters (most cases)

When to use on:

// Non-standard HTTP methods
MethodRouter::new().on(Method::from_bytes(b"CUSTOM").unwrap(), handler)
 
// Dynamic method configuration
let mut router = MethodRouter::new();
for (method, handler) in configured_routes {
    router = router.on(method, handler);
}
 
// When building routers programmatically
// Method::GET is dynamic, not hardcoded

Key insight: axum::routing::get is a convenience function that creates a MethodRouter configured for GET requests—internally it calls MethodRouter::new().on(Method::GET, handler). The MethodRouter::on method is the underlying mechanism that all convenience functions use. Use convenience functions (get, post, put, delete, etc.) for standard HTTP methods and readable code; use on when you need non-standard methods, dynamic method selection at runtime, or are building routers programmatically. Both approaches produce identical MethodRouter values and compose the same way.