Loading pageā¦
Rust walkthroughs
Loading pageā¦
axum::Router::merge combine multiple routers and handle route conflicts?axum::Router::merge combines multiple routers into a single router by recursively merging their route tables, allowing modular organization of routes across different modules or files. The method takes ownership of another router and absorbs all its routes, state, and fallback handlers, while the merged router's paths are preserved as-is without prefix modification. Route conflictsāwhere the same path is defined in both routersāare resolved at runtime with the merged router's routes taking precedence, but Axon provides no compile-time warning for conflicts. The merging process combines middleware and layers from both routers, applying them in order. To organize routes with path prefixes, use Router::nest instead of merge, which creates explicit parent-child relationships. The key distinction is that merge is a flat combination while nest creates hierarchical routing structures.
use axum::{Router, routing::get, handler::Handler};
async fn users_list() -> &'static str {
"Users list"
}
async fn users_create() -> &'static str {
"Create user"
}
async fn products_list() -> &'static str {
"Products list"
}
async fn products_create() -> &'static str {
"Create product"
}
#[tokio::main]
async fn main() {
// Create separate routers for different modules
let users_router = Router::new()
.route("/users", get(users_list))
.route("/users", axum::routing::post(users_create));
let products_router = Router::new()
.route("/products", get(products_list))
.route("/products", axum::routing::post(products_create));
// Merge routers into one
let app = Router::new()
.merge(users_router)
.merge(products_router);
// The combined router now has all routes
// GET /users -> users_list
// POST /users -> users_create
// GET /products -> products_list
// POST /products -> products_create
}merge combines routes from multiple routers into a single unified router.
// src/users/routes.rs
use axum::{Router, routing::get};
pub fn routes() -> Router {
Router::new()
.route("/users", get(list_users))
.route("/users/{id}", get(get_user))
}
async fn list_users() -> &'static str { "List users" }
async fn get_user() -> &'static str { "Get user" }
// src/products/routes.rs
use axum::{Router, routing::get};
pub fn routes() -> Router {
Router::new()
.route("/products", get(list_products))
.route("/products/{id}", get(get_product))
}
async fn list_products() -> &'static str { "List products" }
async fn get_product() -> &'static str { "Get product" }
// src/main.rs
mod users;
mod products;
use axum::Router;
#[tokio::main]
async fn main() {
let app = Router::new()
.merge(users::routes())
.merge(products::routes());
// Each module defines its own router, merged in main
}merge enables modular route organization with each module defining its own router.
use axum::{Router, routing::get};
async fn handler_a() -> &'static str {
"Handler A"
}
async fn handler_b() -> &'static str {
"Handler B"
}
#[tokio::main]
async fn main() {
let router_a = Router::new()
.route("/api/users", get(handler_a));
let router_b = Router::new()
.route("/api/users", get(handler_b));
// Merge order matters: last router wins on conflicts
let app = Router::new()
.merge(router_a) // /api/users -> handler_a
.merge(router_b); // /api/users -> handler_b (overwrites)
// Final result: GET /api/users -> handler_b
// No compile-time warning about the conflict!
}When routes conflict, the last merged router's handler wins silently at runtime.
use axum::{Router, routing::get, routing::post};
async fn get_users_a() -> &'static str { "GET users A" }
async fn post_users_a() -> &'static str { "POST users A" }
async fn get_users_b() -> &'static str { "GET users B" }
#[tokio::main]
async fn main() {
let router_a = Router::new()
.route("/users", get(get_users_a).post(post_users_a));
let router_b = Router::new()
.route("/users", get(get_users_b)); // Only defines GET
let app = Router::new()
.merge(router_a)
.merge(router_b);
// GET /users -> get_users_b (overwritten by router_b)
// POST /users -> post_users_a (from router_a, not in router_b)
// The conflict only affects overlapping methods
}Conflicts only overwrite handlers for the same path AND method combination.
use axum::{Router, routing::get, extract::Path};
use std::collections::HashSet;
// Helper to detect potential conflicts before merging
fn detect_conflicts(routers: &[(&str, Router<(), ()>)]) -> Vec<String> {
// Note: This is a simplified conceptual example
// Actual implementation would need to inspect route tables
// which requires internal knowledge of axum's structure
// In practice, conflicts are detected at runtime when routing
// The best approach is to use clear path prefixes
vec![]
}
// Better approach: use clear prefixes to avoid conflicts
#[tokio::main]
async fn main() {
// Each router has distinct paths - no conflicts
let users_router = Router::new()
.route("/users", get(|| async { "users" }))
.route("/users/{id}", get(|| async { "user" }));
let admin_router = Router::new()
.route("/admin/users", get(|| async { "admin users" }));
let app = Router::new()
.merge(users_router)
.merge(admin_router);
// No conflicts: /users vs /admin/users are distinct
}Use distinct path prefixes to avoid conflicts; Axum doesn't warn about them.
use axum::{Router, routing::get, extract::State};
use std::sync::Arc;
#[derive(Clone)]
struct AppState {
db: String,
}
#[derive(Clone)]
struct AdminState {
admin_level: i32,
}
async fn user_handler(State(state): State<AppState>) -> String {
format!("User DB: {}", state.db)
}
async fn admin_handler(State(state): State<AdminState>) -> String {
format!("Admin level: {}", state.admin_level)
}
#[tokio::main]
async fn main() {
// Problem: merged routers must have compatible state types
// This won't compile - state types differ
// Solution: use the same state type across all routers
let state = Arc::new(AppState { db: "users_db".to_string() });
let users_router = Router::new()
.route("/users", get(user_handler))
.with_state(state.clone());
let admin_router = Router::new()
.route("/admin", get(|| async { "Admin" }));
// No state needed - uses () by default
// This won't compile if state types differ
// let app = Router::new()
// .merge(users_router)
// .merge(admin_router);
// All routers in a merge must have the same state type
}Merged routers must share the same state type; use with_state before merging.
use axum::{Router, routing::get, extract::State};
use std::sync::Arc;
#[derive(Clone)]
struct SharedState {
db: String,
cache: String,
}
async fn api_users(State(state): State<Arc<SharedState>>) -> String {
format!("API Users - DB: {}", state.db)
}
async fn api_products(State(state): State<Arc<SharedState>>) -> String {
format!("API Products - Cache: {}", state.cache)
}
#[tokio::main]
async fn main() {
let state = Arc::new(SharedState {
db: "postgres".to_string(),
cache: "redis".to_string(),
});
// Both routers use the same state type
let users_router = Router::new()
.route("/api/users", get(api_users))
.with_state(state.clone());
let products_router = Router::new()
.route("/api/products", get(api_products))
.with_state(state.clone());
// Merge works because state types match
let app = Router::new()
.merge(users_router)
.merge(products_router);
}All routers being merged must have the same state type S.
use axum::{Router, routing::get};
// Routers without state use () as the default state type
async fn handler_a() -> &'static str { "Handler A" }
async fn handler_b() -> &'static str { "Handler B" }
#[tokio::main]
async fn main() {
// State-less routers can be freely merged
let router_a = Router::new()
.route("/a", get(handler_a));
let router_b = Router::new()
.route("/b", get(handler_b));
let app = Router::new()
.merge(router_a)
.merge(router_b);
// All routes use () state (default)
}State-less routers use () as the state type and can be merged directly.
use axum::{Router, routing::get};
async fn list_users() -> &'static str { "List users" }
async fn get_user() -> &'static str { "Get user" }
async fn list_products() -> &'static str { "List products" }
#[tokio::main]
async fn main() {
// MERGE: flat combination, no prefix modification
let users_router = Router::new()
.route("/users", get(list_users))
.route("/users/{id}", get(get_user));
let app_merge = Router::new()
.merge(users_router);
// Routes: /users, /users/{id}
// NEST: hierarchical combination with prefix
let users_router_nest = Router::new()
.route("/", get(list_users))
.route("/{id}", get(get_user));
let app_nest = Router::new()
.nest("/users", users_router_nest);
// Routes: /users/, /users/{id}
// Key differences:
// - merge: paths preserved as-is
// - nest: paths get prefix added
}merge preserves paths; nest adds a prefix to nested router's paths.
use axum::{Router, routing::get, routing::post};
async fn list() -> &'static str { "List" }
async fn create() -> &'static str { "Create" }
async fn get_one() -> &'static str { "Get one" }
async fn update() -> &'static str { "Update" }
async fn delete() -> &'static str { "Delete" }
#[tokio::main]
async fn main() {
// Define router with relative paths
let crud_router = Router::new()
.route("/", get(list).post(create))
.route("/{id}", get(get_one).put(update).delete(delete));
// Nest under different prefixes
let app = Router::new()
.nest("/api/users", crud_router.clone())
.nest("/api/products", crud_router.clone())
.nest("/api/orders", crud_router);
// Results in:
// GET /api/users/ -> list
// POST /api/users/ -> create
// GET /api/users/{id} -> get_one
// etc.
// nest() is cleaner than merge() when you want prefixes
}Use nest when you want to add a path prefix to a group of routes.
use axum::{Router, routing::get};
use tower::{ServiceBuilder, ServiceExt};
use tower_http::{trace::TraceLayer, cors::CorsLayer};
async fn handler() -> &'static str { "Handler" }
#[tokio::main]
async fn main() {
// Router A with its own layers
let router_a = Router::new()
.route("/a", get(handler))
.layer(TraceLayer::new_for_http());
// Router B with its own layers
let router_b = Router::new()
.route("/b", get(handler))
.layer(CorsLayer::permissive());
// Merging combines layers from both routers
let app = Router::new()
.merge(router_a) // Has TraceLayer
.merge(router_b); // Has CorsLayer
// Final router has:
// - /a with TraceLayer
// - /b with CorsLayer
// Layers are per-router, not global after merge
}Each merged router retains its own middleware layers.
use axum::{Router, routing::get};
use tower::ServiceBuilder;
use tower_http::trace::TraceLayer;
async fn handler() -> &'static str { "OK" }
#[tokio::main]
async fn main() {
// Layers on individual routers
let router_a = Router::new()
.route("/a", get(handler))
.layer(TraceLayer::new_for_http()); // Only applies to /a
let router_b = Router::new()
.route("/b", get(handler))
.layer(TraceLayer::new_for_http()); // Only applies to /b
let app = Router::new()
.merge(router_a)
.merge(router_b)
.layer(TraceLayer::new_for_http()); // Applies to ALL routes
// Layer order matters:
// - Per-router layers: apply to that router's routes only
// - Post-merge layers: apply to all merged routes
}Add shared layers after merging for consistent middleware across all routes.
use axum::{Router, routing::get, handler::Handler};
async fn handler() -> &'static str { "Found" }
async fn fallback() -> &'static str { "Not found" }
async fn fallback_a() -> &'static str { "Fallback A" }
async fn fallback_b() -> &'static str { "Fallback B" }
#[tokio::main]
async fn main() {
let router_a = Router::new()
.route("/a", get(handler))
.fallback(fallback_a);
let router_b = Router::new()
.route("/b", get(handler))
.fallback(fallback_b);
// Merging fallbacks: last merged router's fallback wins
let app = Router::new()
.merge(router_a) // Has fallback_a
.merge(router_b); // Has fallback_b - overwrites
// Final fallback: fallback_b
// Better: set fallback on the final merged router
let app_better = Router::new()
.merge(router_a.without_fallback()) // Remove fallback_a
.merge(router_b.without_fallback()) // Remove fallback_b
.fallback(fallback); // Set unified fallback
}Fallback handlers are overwritten by later merges; set on the final router.
use axum::{Router, routing::get};
async fn handler() -> &'static str { "Found" }
async fn global_fallback() -> &'static str { "Global fallback" }
#[tokio::main]
async fn main() {
// Router A with its own fallback
let router_a = Router::new()
.route("/a", get(handler))
.fallback(|| async { "Fallback A" });
// Router B with its own fallback
let router_b = Router::new()
.route("/b", get(handler))
.fallback(|| async { "Fallback B" });
// Clean merge without conflicting fallbacks
let app = Router::new()
.merge(router_a.without_fallback())
.merge(router_b.without_fallback())
.fallback(global_fallback);
// Now all routes use global_fallback for 404s
}Use without_fallback() to strip individual fallbacks before merging.
use axum::{Router, routing::get};
use std::env;
async fn debug_handler() -> &'static str { "Debug info" }
async fn api_handler() -> &'static str { "API response" }
#[tokio::main]
async fn main() {
let mut app = Router::new()
.route("/api", get(api_handler));
// Conditionally merge debug routes based on environment
if env::var("DEBUG_MODE").is_ok() {
let debug_router = Router::new()
.route("/debug/health", get(|| async { "OK" }))
.route("/debug/config", get(debug_handler));
app = app.merge(debug_router);
}
// Debug routes only available in debug mode
}Conditionally merge routers based on configuration or environment.
use axum::{Router, routing::get, extract::State};
use std::sync::Arc;
struct AppState {
// App state fields
db: String,
}
struct AdminState {
// Admin-specific state
permissions: Vec<String>,
}
// Use a wrapper state that combines both
#[derive(Clone)]
struct CombinedState {
app: AppState,
admin: AdminState,
}
async fn app_handler(State(state): State<Arc<CombinedState>>) -> String {
format!("App DB: {}", state.app.db)
}
async fn admin_handler(State(state): State<Arc<CombinedState>>) -> String {
format!("Admin permissions: {:?}", state.admin.permissions)
}
#[tokio::main]
async fn main() {
let combined = Arc::new(CombinedState {
app: AppState { db: "postgres".to_string() },
admin: AdminState { permissions: vec!["read".to_string(), "write".to_string()] },
});
let app_router = Router::new()
.route("/api/data", get(app_handler));
let admin_router = Router::new()
.route("/admin/data", get(admin_handler));
let app = Router::new()
.merge(app_router)
.merge(admin_router)
.with_state(combined);
// Both routers share the combined state type
}Use combined state types when merging routers with different state requirements.
use axum::{Router, routing::get};
async fn handler() -> &'static str { "OK" }
#[tokio::main]
async fn main() {
let router_a = Router::new()
.route("/users", get(handler))
.route("/users/{id}", get(handler));
let router_b = Router::new()
.route("/products", get(handler))
.route("/products/{id}", get(handler));
let app = Router::new()
.merge(router_a)
.merge(router_b);
// Axum doesn't provide direct route introspection
// Debug by checking routes in tests:
// - Use axum::test utilities
// - Log route registrations in your module structure
// - Keep route documentation per router
println!("Router merged successfully");
// Routes: /users, /users/{id}, /products, /products/{id}
}Keep documentation of routes per router since Axum doesn't provide runtime introspection.
use axum::{Router, routing::get};
use tower::ServiceExt;
use http::{Request, StatusCode};
async fn users() -> &'static str { "users" }
async fn products() -> &'static str { "products" }
#[tokio::main]
async fn main() {
let users_router = Router::new()
.route("/users", get(users));
let products_router = Router::new()
.route("/products", get(products));
let app = Router::new()
.merge(users_router)
.merge(products_router);
// Test merged routes
async fn test_routes() {
use axum::body::Body;
let app = Router::new()
.route("/users", get(users))
.route("/products", get(products));
// Test users route
let response = app
.oneshot(Request::builder().uri("/users").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
// Test products route
let response = app
.oneshot(Request::builder().uri("/products").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
}Test merged routers to verify all routes are accessible.
// src/users/mod.rs
use axum::{Router, routing::{get, post, put, delete}};
pub fn router() -> Router {
Router::new()
.route("/users", get(list_users).post(create_user))
.route("/users/{id}", get(get_user).put(update_user).delete(delete_user))
}
async fn list_users() -> &'static str { "list users" }
async fn create_user() -> &'static str { "create user" }
async fn get_user() -> &'static str { "get user" }
async fn update_user() -> &'static str { "update user" }
async fn delete_user() -> &'static str { "delete user" }
// src/products/mod.rs
use axum::{Router, routing::{get, post, put, delete}};
pub fn router() -> Router {
Router::new()
.route("/products", get(list_products).post(create_product))
.route("/products/{id}", get(get_product).put(update_product).delete(delete_product))
}
async fn list_products() -> &'static str { "list products" }
async fn create_product() -> &'static str { "create product" }
async fn get_product() -> &'static str { "get product" }
async fn update_product() -> &'static str { "update product" }
async fn delete_product() -> &'static str { "delete product" }
// src/main.rs
mod users;
mod products;
use axum::Router;
use tower_http::trace::TraceLayer;
#[tokio::main]
async fn main() {
let app = Router::new()
.merge(users::router())
.merge(products::router())
.layer(TraceLayer::new_for_http());
// Each module defines its routes independently
// Main combines them with shared middleware
}Organize code by feature module, each exposing a router function.
Merge behavior summary:
| Aspect | Behavior | |--------|----------| | Path handling | Paths preserved as-is from merged router | | Conflicts | Last merged router wins, no warning | | State type | All routers must have same state type | | Layers | Each router keeps its own layers | | Fallback | Last merged router's fallback wins |
Merge vs Nest comparison:
| Feature | merge | nest |
|---------|---------|--------|
| Path modification | None | Adds prefix |
| Use case | Combining same-level routes | Creating route hierarchy |
| Conflicts | Last wins (runtime) | Clear separation |
| State | Must match | Must match |
| Typical pattern | Module organization | API versioning, resource grouping |
Key insight: Router::merge provides flat composition of routers where each router's paths are preserved exactly, making it ideal for modular organization where different modules define different routes. The lack of compile-time conflict detection means developers must carefully manage route paths across modulesāusing distinct prefixes or clear module boundaries is essential. When route prefixes are needed, nest is the appropriate choice as it explicitly adds a prefix to nested router paths. State type compatibility is a compile-time requirement: all merged routers must use the same S in Router<S>, often solved by using a shared Arc<AppState> or () for stateless routers. Middleware layers are preserved per-router, so each merged router's routes keep their individual middleware, while layers added after merge() apply to all routes. Fallback handlers follow the same "last wins" rule as route conflicts, so setting a single fallback on the final merged router (using without_fallback() on sub-routers) provides consistent 404 handling. The pattern of exposing Router from each module and merging in main() enables clean separation of concerns while maintaining a single application router.