How does axum::Router::nest differ from axum::Router::merge for composing routes?

axum::Router::nest mounts a router at a path prefix, where the nested router's routes are prefixed but the prefix is stripped before matching within the nested router. axum::Router::merge combines two routers at the same path level, where all routes from the merged router are added directly without path modification. The key difference is that nest creates a hierarchical path structure (/api/users from nest at /api with route /users), while merge flattens routes into the existing router at their original paths. Use nest when you want to organize routers under a common prefix with shared middleware or state; use merge when combining independent routers that should coexist at the same routing level.

Basic Router Setup

use axum::{Router, routing::get, Json};
 
async fn handler() -> Json<&'static str> {
    Json("Hello")
}
 
fn main() {
    let app = Router::new()
        .route("/hello", get(handler));
}

Both nest and merge are for composing multiple routers together.

Using merge to Combine Routers

use axum::{Router, routing::get, Json};
 
async fn users() -> Json<&'static str> {
    Json("Users")
}
 
async fn posts() -> Json<&'static str> {
    Json("Posts")
}
 
async fn comments() -> Json<&'static str> {
    Json("Comments")
}
 
fn main() {
    let users_router = Router::new()
        .route("/users", get(users));
    
    let posts_router = Router::new()
        .route("/posts", get(posts))
        .route("/comments", get(comments));
    
    // merge combines routes at the same level
    let app = Router::new()
        .merge(users_router)
        .merge(posts_router);
    
    // Routes available:
    // GET /users
    // GET /posts
    // GET /comments
}

merge adds all routes from one router to another at their original paths.

Using nest for Hierarchical Routing

use axum::{Router, routing::get, Json};
 
async fn users() -> Json<&'static str> {
    Json("Users")
}
 
async fn user_detail() -> Json<&'static str> {
    Json("User Detail")
}
 
fn main() {
    let users_router = Router::new()
        .route("/", get(users))
        .route("/:id", get(user_detail));
    
    let app = Router::new()
        .nest("/api/users", users_router);
    
    // Routes available:
    // GET /api/users/      -> users_router route "/"
    // GET /api/users/:id   -> users_router route "/:id"
}

nest prefixes all routes in the nested router with the given path.

Path Handling: nest Strips Prefix

use axum::{Router, routing::get, Json};
use axum::extract::Path;
 
async fn user_by_id(Path(id): Path<String>) -> Json<String> {
    Json(format!("User {}", id))
}
 
fn main() {
    let users_router = Router::new()
        .route("/:id", get(user_by_id));
    
    let app = Router::new()
        .nest("/users", users_router);
    
    // Request: GET /users/123
    // Nest strips "/users", then routes to "/:id"
    // The handler receives "123" as the path parameter
}

The nested router sees paths without the nesting prefix.

Path Handling: merge Keeps Original Paths

use axum::{Router, routing::get, Json};
use axum::extract::Path;
 
async fn user_by_id(Path(id): Path<String>) -> Json<String> {
    Json(format!("User {}", id))
}
 
fn main() {
    let users_router = Router::new()
        .route("/users/:id", get(user_by_id));
    
    let app = Router::new()
        .merge(users_router);
    
    // Request: GET /users/123
    // merge keeps original path, routes to "/users/:id"
    // The handler receives "123" as the path parameter
}

Merged routes keep their exact original paths.

Nest for API Versioning

use axum::{Router, routing::get, Json};
 
async fn list_users() -> Json<&'static str> {
    Json("Users v1")
}
 
async fn get_user() -> Json<&'static str> {
    Json("User v1")
}
 
async fn list_users_v2() -> Json<&'static str> {
    Json("Users v2")
}
 
fn main() {
    let v1_router = Router::new()
        .route("/users", get(list_users))
        .route("/users/:id", get(get_user));
    
    let v2_router = Router::new()
        .route("/users", get(list_users_v2));
    
    let app = Router::new()
        .nest("/v1", v1_router)
        .nest("/v2", v2_router);
    
    // Routes:
    // GET /v1/users
    // GET /v1/users/:id
    // GET /v2/users
}

nest is ideal for organizing routers under prefixes like API versions.

Merge for Modular Route Organization

use axum::{Router, routing::get, Json};
 
async fn health() -> Json<&'static str> {
    Json("OK")
}
 
fn users_routes() -> Router {
    Router::new()
        .route("/users", get(|| async { "List users" }))
        .route("/users/:id", get(|| async { "Get user" }))
}
 
fn posts_routes() -> Router {
    Router::new()
        .route("/posts", get(|| async { "List posts" }))
        .route("/posts/:id", get(|| async { "Get post" }))
}
 
fn main() {
    let app = Router::new()
        .route("/health", get(health))
        .merge(users_routes())
        .merge(posts_routes());
    
    // Routes:
    // GET /health
    // GET /users
    // GET /users/:id
    // GET /posts
    // GET /posts/:id
}

merge combines routers from different modules at the same level.

Middleware Inheritance with Nest

use axum::{Router, routing::get, Json};
use tower_http::trace::TraceLayer;
 
async fn handler() -> Json<&'static str> {
    Json("Protected")
}
 
fn main() {
    let api_router = Router::new()
        .route("/data", get(handler));
    
    let app = Router::new()
        .layer(TraceLayer::new_for_http())
        .nest("/api", api_router);
    
    // The TraceLayer applies to ALL routes including nested ones
    // GET /api/data has the TraceLayer applied
}

Middleware on the parent router applies to nested routers.

Middleware Inheritance with Merge

use axum::{Router, routing::get, Json};
use tower_http::trace::TraceLayer;
 
async fn handler() -> Json<&'static str> {
    Json("Protected")
}
 
fn main() {
    let api_router = Router::new()
        .route("/api/data", get(handler));
    
    let app = Router::new()
        .layer(TraceLayer::new_for_http())
        .merge(api_router);
    
    // Merged routes inherit middleware from the parent
    // GET /api/data has the TraceLayer applied
}

Both nest and merge inherit middleware from the parent router.

State Sharing with Nest

use axum::{Router, routing::get, Json, Extension};
use std::sync::Arc;
 
struct AppState {
    db: String,
}
 
async fn handler(Extension(state): Extension<Arc<AppState>>) -> Json<String> {
    Json(state.db.clone())
}
 
fn main() {
    let state = Arc::new(AppState { db: "connected".into() });
    
    let nested_router = Router::new()
        .route("/info", get(handler));
    
    let app = Router::new()
        .nest("/api", nested_router)
        .layer(Extension(state));
    
    // State is available in nested router handlers
}

State is shared with both nested and merged routers.

Route Conflicts: merge Behavior

use axum::{Router, routing::get};
 
fn main() {
    let router1 = Router::new()
        .route("/users", get(|| async { "Router 1" }));
    
    let router2 = Router::new()
        .route("/users", get(|| async { "Router 2" }));
    
    // merge will panic at runtime on route conflict
    let app = Router::new()
        .merge(router1)
        .merge(router2);
    // Panics: "route collision: `/users` already exists"
}

merge panics on route conflicts.

Route Conflicts: nest Behavior

use axum::{Router, routing::get};
 
fn main() {
    let nested1 = Router::new()
        .route("/data", get(|| async { "Nested 1" }));
    
    let nested2 = Router::new()
        .route("/data", get(|| async { "Nested 2" }));
    
    // Different nest paths avoid conflict
    let app = Router::new()
        .nest("/api/v1", nested1)
        .nest("/api/v2", nested2);
    
    // Routes: /api/v1/data and /api/v2/data - no conflict
}

Different nest prefixes prevent conflicts.

Fallback Handlers

use axum::{Router, routing::get, Json};
 
async fn fallback() -> Json<&'static str> {
    Json("Not found")
}
 
fn main() {
    let nested_router = Router::new()
        .route("/specific", get(|| async { "Found" }));
    
    let app = Router::new()
        .nest("/api", nested_router)
        .fallback(fallback);
    
    // Request: GET /api/specific -> "Found"
    // Request: GET /api/unknown -> "Not found" (from fallback)
    // Request: GET /other -> "Not found" (from fallback)
}

Fallback handlers apply after nested routing fails.

Deep Nesting

use axum::{Router, routing::get};
 
fn main() {
    let inner_router = Router::new()
        .route("/item", get(|| async { "Item" }));
    
    let middle_router = Router::new()
        .nest("/category", inner_router);
    
    let app = Router::new()
        .nest("/api/v1", middle_router);
    
    // Routes:
    // GET /api/v1/category/item
    
    // Each nest adds its prefix
}

Nesting can be chained for deep hierarchies.

Combining Nest and Merge

use axum::{Router, routing::get};
 
fn main() {
    let api_routes = Router::new()
        .route("/users", get(|| async { "Users" }));
    
    let web_routes = Router::new()
        .route("/", get(|| async { "Home" }));
    
    let health_route = Router::new()
        .route("/health", get(|| async { "OK" }));
    
    let app = Router::new()
        .nest("/api", api_routes)      // /api/users
        .merge(web_routes)              // /
        .merge(health_route);           // /health
}

Combine nest and merge based on organizational needs.

Path Parameters in Nested Routers

use axum::{Router, routing::get, extract::Path};
 
async fn get_post(
    Path((user_id, post_id)): Path<(String, String)>,
) -> String {
    format!("User {} Post {}", user_id, post_id)
}
 
fn main() {
    let posts_router = Router::new()
        .route("/:post_id", get(get_post));
    
    let users_router = Router::new()
        .nest("/:user_id/posts", posts_router);
    
    let app = Router::new()
        .nest("/users", users_router);
    
    // GET /users/123/posts/456
    // Parameters: user_id=123, post_id=456
    
    // The /:user_id is in users_router (before the nest of posts_router)
    // The /:post_id is in posts_router
}

Path parameters accumulate across nesting levels.

Route Ordering: nest

use axum::{Router, routing::get};
 
fn main() {
    let nested = Router::new()
        .route("/specific", get(|| async { "Nested specific" }));
    
    let app = Router::new()
        .route("/api/specific", get(|| async { "Root specific" }))
        .nest("/api", nested);
    
    // Request: GET /api/specific
    // Matches: "Root specific" (defined before nest)
    // The root route takes precedence because it's added first
}

Route ordering matters when paths overlap.

Route Ordering: merge

use axum::{Router, routing::get};
 
fn main() {
    let router1 = Router::new()
        .route("/data", get(|| async { "Router 1" }));
    
    let router2 = Router::new()
        .route("/data", get(|| async { "Router 2" }));
    
    // This would panic due to route conflict
    // let app = Router::new()
    //     .merge(router1)
    //     .merge(router2);
}

merge panics on exact route conflicts.

URL Generation Awareness

use axum::{Router, routing::get};
 
fn main() {
    // With nest: nested router defines routes without prefix
    let api = Router::new()
        .route("/users", get(|| async { "Users" }));
    // When mounted at "/api", the route becomes "/api/users"
    
    // With merge: routes are defined with full paths
    let routes = Router::new()
        .route("/api/users", get(|| async { "Users" }));
    // Route stays "/api/users"
    
    // For URL generation:
    // - nest: prepend the nesting prefix
    // - merge: use the route as-is
}

Consider URL generation when choosing between nest and merge.

When to Use nest

use axum::{Router, routing::get};
 
fn main() {
    // Use nest when:
    // 1. Organizing routes under a common prefix
    // 2. API versioning
    // 3. Grouping routes with shared middleware/state
    // 4. Modular code organization where each module defines relative paths
    
    let v1 = Router::new()
        .route("/users", get(|| async { "v1 users" }))
        .route("/posts", get(|| async { "v1 posts" }));
    
    let v2 = Router::new()
        .route("/users", get(|| async { "v2 users" }))
        .route("/posts", get(|| async { "v2 posts" }));
    
    let app = Router::new()
        .nest("/v1", v1)
        .nest("/v2", v2);
    
    // Each version router defines clean, relative paths
    // Easy to change version prefix in one place
}

Use nest for hierarchical organization.

When to Use merge

use axum::{Router, routing::get};
 
fn main() {
    // Use merge when:
    // 1. Combining independent routers with different paths
    // 2. Adding health/metrics routes alongside API routes
    // 3. Modular organization where modules define absolute paths
    // 4. Avoiding redundant prefixes in route definitions
    
    let users_api = Router::new()
        .route("/users", get(|| async { "users" }));
    
    let posts_api = Router::new()
        .route("/posts", get(|| async { "posts" }));
    
    let health = Router::new()
        .route("/health", get(|| async { "OK" }));
    
    let app = Router::new()
        .merge(users_api)
        .merge(posts_api)
        .merge(health);
    
    // All routes at same level, no prefix manipulation
}

Use merge for flat composition of independent routers.

Comparison Summary

Aspect nest merge
Path prefix Added to all routes Not modified
Route visibility Nested router sees paths without prefix Routes keep original paths
Primary use case Hierarchical organization Flat composition
Path parameters Prefix can contain parameters No prefix modification
Route conflicts Different prefixes avoid conflicts Panics on conflicts
Middleware Inherited from parent Inherited from parent
State Shared from parent Shared from parent

Synthesis

nest and merge solve different composition problems:

nest("/prefix", router) creates a hierarchical structure. The nested router defines routes relative to its own context (like /users instead of /api/users), and the parent adds the prefix. This is powerful for:

  • API versioning where all v1 routes live under /v1
  • Domain separation where user routes live under /users
  • Middleware scoping where a group of routes shares authentication

The nested router doesn't know about its prefix—it just sees /users/:id, making it reusable at different mount points.

merge(router) flattens routes into the existing router. Each route keeps its original path. This is useful for:

  • Combining independent feature routers (users, posts, comments) into one app
  • Adding health/metrics routes that don't share prefixes
  • Modular organization where each module owns its complete paths

Key insight: nest creates parent-child relationships with prefix semantics; merge creates sibling relationships. When you want to change where a group of routes is mounted, nest lets you change one prefix. When you want to combine routers that should coexist at the same level, merge adds them without modification.

Both inherit middleware and state from the parent, so the difference is purely about path handling and organizational structure.