What are the trade-offs between axum::Router::merge and nest for combining route handlers?
merge combines routers at the same path level, flattening their routes into a single router without introducing a path prefix, while nest mounts a router under a specific path prefix, requiring all nested routes to be accessed through that prefix. The fundamental distinction is path handling: merge is a union operation that preserves original paths, whereas nest is a hierarchical composition that prefixes all routes.
Basic merge Usage
use axum::{Router, routing::get};
fn basic_merge() {
let user_routes = Router::new()
.route("/users", get(list_users))
.route("/users/:id", get(get_user));
let product_routes = Router::new()
.route("/products", get(list_products))
.route("/products/:id", get(get_product));
// merge combines routes at the same level
let app = Router::new()
.merge(user_routes)
.merge(product_routes);
// Routes are now:
// GET /users
// GET /users/:id
// GET /products
// GET /products/:id
}
async fn list_users() {}
async fn get_user() {}
async fn list_products() {}
async fn get_product() {}merge combines multiple routers into one, preserving all original paths.
Basic nest Usage
use axum::{Router, routing::get};
fn basic_nest() {
let user_routes = Router::new()
.route("/", get(list_users)) // Note: relative paths
.route("/:id", get(get_user));
let product_routes = Router::new()
.route("/", get(list_products))
.route("/:id", get(get_product));
// nest mounts routers under a prefix
let app = Router::new()
.nest("/users", user_routes)
.nest("/products", product_routes);
// Routes are now:
// GET /users (from /users + /)
// GET /users/:id (from /users + /:id)
// GET /products (from /products + /)
// GET /products/:id (from /products + /:id)
}
async fn list_users() {}
async fn get_user() {}
async fn list_products() {}
async fn get_product() {}nest prefixes all nested routes with the specified path.
Path Structure Differences
use axum::{Router, routing::get};
fn path_structure() {
// With merge: define full paths in each router
let api_v1 = Router::new()
.route("/api/v1/users", get(users_v1))
.route("/api/v1/products", get(products_v1));
let api_v2 = Router::new()
.route("/api/v2/users", get(users_v2))
.route("/api/v2/products", get(products_v2));
let app = Router::new()
.merge(api_v1)
.merge(api_v2);
// With nest: define relative paths, prefix at mount
let api_routes_v1 = Router::new()
.route("/users", get(users_v1))
.route("/products", get(products_v1));
let api_routes_v2 = Router::new()
.route("/users", get(users_v2))
.route("/products", get(products_v2));
let app = Router::new()
.nest("/api/v1", api_routes_v1)
.nest("/api/v2", api_routes_v2);
// Both produce same routes, but nest is more modular
}
async fn users_v1() {}
async fn users_v2() {}
async fn products_v1() {}
async fn products_v2() {}merge requires full paths in each router; nest uses relative paths with prefix.
Handler Access to Path Parameters
use axum::{Router, routing::get, extract::Path};
fn path_parameters() {
// merge: handlers define their own full paths
let routes = Router::new()
.route("/users/:id", get(get_user))
.merge(Router::new().route("/posts/:post_id", get(get_post)));
// Handlers see /users/:id and /posts/:post_id directly
// nest: prefix path is stripped before reaching handler
let nested_routes = Router::new()
.route("/:id", get(get_user_nested));
let app = Router::new()
.nest("/users", nested_routes);
// Handler sees only :id, not /users/:id
}
async fn get_user(Path(id): Path<String>) {
// Full path parameter access
}
async fn get_post(Path(post_id): Path<String>) {
// Direct access to post_id
}
async fn get_user_nested(Path(id): Path<String>) {
// Only sees :id from the nested router's path
// The /users prefix is already matched and stripped
}With nest, the prefix path is matched before the nested router sees the request.
Middleware Application
use axum::{Router, routing::get, middleware};
use tower::ServiceBuilder;
use tower_http::trace::TraceLayer;
fn middleware_application() {
let user_routes = Router::new()
.route("/users", get(list_users));
let admin_routes = Router::new()
.route("/admin/settings", get(get_settings));
// merge: apply middleware to combined router
let app = Router::new()
.merge(user_routes)
.merge(admin_routes)
.layer(TraceLayer::new_for_http());
// TraceLayer applies to ALL routes
// nest: apply middleware to specific nested router
let admin_with_auth = Router::new()
.route("/settings", get(get_settings))
.layer(TraceLayer::new_for_http());
// This layer only applies to /admin/* routes
let app = Router::new()
.nest("/admin", admin_with_auth)
.route("/users", get(list_users));
// /users has no TraceLayer, /admin/settings does
}
async fn list_users() {}
async fn get_settings() {}nest enables scoped middleware; merge requires middleware on the combined router.
Route Organization Patterns
use axum::{Router, routing::{get, post, put, delete}};
fn organization_patterns() {
// Pattern 1: Domain-based routers merged together
let users_router = Router::new()
.route("/users", get(list_users).post(create_user))
.route("/users/:id", get(get_user).put(update_user).delete(delete_user));
let products_router = Router::new()
.route("/products", get(list_products).post(create_product))
.route("/products/:id", get(get_product).put(update_product));
let orders_router = Router::new()
.route("/orders", get(list_orders).post(create_order));
let app = Router::new()
.merge(users_router)
.merge(products_router)
.merge(orders_router);
// Flat structure, all routes at top level
// Pattern 2: Hierarchical with nest
let users_api = Router::new()
.route("/", get(list_users).post(create_user))
.route("/:id", get(get_user).put(update_user).delete(delete_user));
let products_api = Router::new()
.route("/", get(list_products).post(create_product))
.route("/:id", get(get_product).put(update_product));
let orders_api = Router::new()
.route("/", get(list_orders).post(create_order));
let api_routes = Router::new()
.nest("/users", users_api)
.nest("/products", products_api)
.nest("/orders", orders_api);
let app = Router::new()
.nest("/api", api_routes);
// Hierarchical: /api/users, /api/products, /api/orders
}merge creates flat organizations; nest creates hierarchical structures.
Fallback Handlers
use axum::{Router, routing::get, handler::Handler};
fn fallback_handlers() {
// merge: single fallback for entire router
let routes = Router::new()
.route("/users", get(list_users));
let app = Router::new()
.merge(routes)
.fallback(fallback_handler);
// All unmatched routes go to fallback_handler
// nest: fallback only applies to nested router's scope
let nested_routes = Router::new()
.route("/users", get(list_users))
.fallback(nested_fallback);
// This fallback only handles /api/* unmatched routes
let app = Router::new()
.nest("/api", nested_routes)
.route("/other", get(other_handler))
.fallback(root_fallback);
// /api/unmatched -> nested_fallback
// /unmatched -> root_fallback
}
async fn list_users() {}
async fn fallback_handler() {}
async fn nested_fallback() {}
async fn root_fallback() {}
async fn other_handler() {}nest allows scoped fallback handlers; merge uses a single router-wide fallback.
State and Extensions
use axum::{Router, routing::get, extract::State};
#[derive(Clone)]
struct AppState {
db: Db,
}
#[derive(Clone)]
struct AdminState {
db: Db,
admin_secret: String,
}
fn state_handling() {
let state = AppState { db: Db::new() };
let admin_state = AdminState {
db: Db::new(),
admin_secret: "secret".to_string()
};
// merge: single state type for all routes
let user_routes = Router::new()
.route("/users", get(list_users));
let admin_routes = Router::new()
.route("/admin", get(admin_panel));
let app = Router::new()
.merge(user_routes)
.merge(admin_routes)
.with_state(state);
// All handlers must use AppState
// nest: can have different state for nested router
let admin_routes = Router::new()
.route("/panel", get(admin_panel))
.with_state(admin_state);
let app = Router::new()
.nest("/admin", admin_routes)
.route("/users", get(list_users))
.with_state(state);
// /admin/* uses AdminState
// /users uses AppState
}
async fn list_users(State(state): State<AppState>) {}
async fn admin_panel(State(state): State<AdminState>) {}
struct Db;
impl Db {
fn new() -> Self { Self }
}nest allows different state types per nested router; merge requires a single state type.
Combining merge and nest
use axum::{Router, routing::get};
fn combined_approach() {
let v1_routes = Router::new()
.route("/users", get(users_v1))
.route("/products", get(products_v1));
let v2_routes = Router::new()
.route("/users", get(users_v2))
.route("/products", get(products_v2))
.route("/orders", get(orders_v2));
// Use merge for same-level routes
let api_routes = Router::new()
.nest("/v1", v1_routes)
.nest("/v2", v2_routes);
// Use nest for hierarchical prefix
let app = Router::new()
.nest("/api", api_routes)
.route("/health", get(health));
// Routes:
// GET /api/v1/users
// GET /api/v1/products
// GET /api/v2/users
// GET /api/v2/products
// GET /api/v2/orders
// GET /health
}
async fn users_v1() {}
async fn users_v2() {}
async fn products_v1() {}
async fn products_v2() {}
async fn orders_v2() {}
async fn health() {}Use both together: nest for hierarchical prefixes, merge for combining routers at the same level.
Route Conflicts
use axum::{Router, routing::get};
fn route_conflicts() {
// merge: Conflicts possible, last one wins
let router1 = Router::new()
.route("/users", get(users_v1));
let router2 = Router::new()
.route("/users", get(users_v2));
let app = Router::new()
.merge(router1)
.merge(router2);
// /users -> users_v2 (later router wins)
// nest: Conflicts isolated to nested scope
let v1 = Router::new()
.route("/users", get(users_v1));
let v2 = Router::new()
.route("/users", get(users_v2));
let app = Router::new()
.nest("/v1", v1)
.nest("/v2", v2);
// /v1/users -> users_v1
// /v2/users -> users_v2
// No conflict - different paths
}
async fn users_v1() {}
async fn users_v2() {}merge can have route conflicts; nest isolates routes under different prefixes.
Performance Characteristics
use axum::{Router, routing::get};
fn performance() {
// merge: Flat routing table
// O(1) route lookup in the combined trie
let app = Router::new()
.merge(router_a())
.merge(router_b())
.merge(router_c());
// Single routing table for all routes
// nest: Hierarchical routing
// Lookup traverses prefix, then nested router's trie
let app = Router::new()
.nest("/api", api_routes())
.nest("/admin", admin_routes());
// First match /api, then routes within api_routes
// Performance difference is typically negligible
// Both use efficient trie-based matching
// merge is slightly faster for very large numbers of routes
// nest provides cleaner organization with minimal overhead
}
fn router_a() -> Router { Router::new() }
fn router_b() -> Router { Router::new() }
fn router_c() -> Router { Router::new() }
fn api_routes() -> Router { Router::new() }
fn admin_routes() -> Router { Router::new() }Both use efficient routing; merge is marginally faster for very large route counts.
Testing and Modularity
use axum::{Router, routing::get};
// With nest: each module exports a router with relative paths
mod users {
use axum::{Router, routing::get};
pub fn routes() -> Router {
Router::new()
.route("/", get(list))
.route("/:id", get(get_user))
}
async fn list() {}
async fn get_user() {}
}
mod products {
use axum::{Router, routing::get};
pub fn routes() -> Router {
Router::new()
.route("/", get(list))
.route("/:id", get(get_product))
}
async fn list() {}
async fn get_product() {}
}
fn modular_organization() {
let app = Router::new()
.nest("/users", users::routes())
.nest("/products", products::routes());
// Each module defines routes relative to its mount point
// Easy to test each router in isolation
// Can change mount point without modifying module
}nest enables better modularity; each module's router is self-contained with relative paths.
When to Use Each
use axum::{Router, routing::get};
fn usage_guidelines() {
// Use merge when:
// 1. Combining routers at the same path level
// 2. Routes are independent and should be flattened
// 3. Single state type for all routes
// 4. Simple route organization without hierarchy
let public_routes = Router::new()
.route("/health", get(health))
.route("/metrics", get(metrics));
let api_routes = Router::new()
.route("/api/users", get(users))
.route("/api/products", get(products));
let app = Router::new()
.merge(public_routes)
.merge(api_routes);
// Use nest when:
// 1. Hierarchical API structure (/api/v1, /api/v2)
// 2. Different middleware for different sections
// 3. Different state types for different sections
// 4. Modular code organization
// 5. Versioned APIs
let v1 = Router::new()
.route("/users", get(users_v1))
.route("/products", get(products_v1));
let v2 = Router::new()
.route("/users", get(users_v2))
.route("/products", get(products_v2));
let app = Router::new()
.nest("/api/v1", v1)
.nest("/api/v2", v2);
}
async fn health() {}
async fn metrics() {}
async fn users() {}
async fn products() {}
async fn users_v1() {}
async fn products_v1() {}
async fn users_v2() {}
async fn products_v2() {}Choose based on organization needs and whether you want hierarchical or flat routing.
Synthesis
Quick comparison:
| Aspect | merge |
nest |
|---|---|---|
| Path handling | Preserves original paths | Prefixes all routes |
| Path in handlers | Full path | Relative (prefix stripped) |
| Middleware scope | Entire router | Per nested router |
| State types | Single type | Different types per nest |
| Fallback | Router-wide | Scoped per nest |
| Organization | Flat | Hierarchical |
| Modularity | Less isolated | Highly modular |
Decision matrix:
use axum::Router;
fn choose_approach() {
// FLAT: Use merge
// - Simple API with few routes
// - All routes share same middleware/state
// - Routes defined in different modules but same level
// - Example: public routes + api routes at root
// HIERARCHICAL: Use nest
// - API versioning (/v1, /v2)
// - Admin area with separate auth middleware
// - Different state types for different areas
// - Modular codebase with isolated routers
// - Example: /api/v1/users, /admin/settings
// HYBRID: Both together
// - nest for hierarchy (versions, areas)
// - merge for same-level routers (public + health)
}Key insight: The choice between merge and nest is fundamentally about path ownership and scope. merge is a union operationāit says "combine these routes at the same level, treating them as if they were defined together." This is useful for combining independently developed routers that should appear as one flat API. nest is a composition operationāit says "mount this router under this prefix, with its own scope for middleware, state, and fallbacks." This is essential for hierarchical APIs, versioned endpoints, or separating areas like public/ authenticated/ admin with different concerns. The critical difference in practice is that with nest, the nested router's handlers receive paths relative to the mount point, and the prefix is matched before the nested router sees the request. This enables modularity: a user routes module can define / and /:id without knowing it will be mounted at /api/v1/users. With merge, every router must be aware of the full paths, making it harder to compose independently. Use nest when you want encapsulation and hierarchy; use merge when you want a flat union of routes at the same level.
