| Crates.io | router-radix |
| lib.rs | router-radix |
| version | 0.4.0 |
| created_at | 2025-10-08 12:57:34.210163+00 |
| updated_at | 2025-10-23 12:26:46.458139+00 |
| description | A high-performance radix tree based HTTP router for Rust |
| homepage | |
| repository | https://github.com/cj2a7t/routerix |
| max_upload_size | |
| id | 1873971 |
| size | 273,005 |
A high-performance, thread-safe radix tree based HTTP router for Rust
Based on Redis's radix tree implementation
router_radix is a Rust port of lua-resty-radixtree, providing fast and flexible HTTP routing. The underlying radix tree (rax) is the same battle-tested data structure used in Redis for Redis Streams and internal routing.
Why router_radix?
:id), wildcards (*path)*.example.com)anyhow error handlingAdd to your Cargo.toml:
[dependencies]
router_radix = "0.4.0"
use router_radix::{RadixRouter, RadixNode, RadixHttpMethod, RadixMatchOpts};
fn main() -> anyhow::Result<()> {
// Create routes
let routes = vec![
RadixNode {
id: "get_users".to_string(),
paths: vec!["/api/users".to_string()],
methods: Some(RadixHttpMethod::GET),
hosts: None,
remote_addrs: None,
vars: None,
filter_fn: None,
priority: 0,
metadata: serde_json::json!({"handler": "list_users"}),
},
];
// Initialize router
let mut router = RadixRouter::new()?;
router.add_routes(routes)?;
// Match a request
let opts = RadixMatchOpts {
method: Some("GET".to_string()),
..Default::default()
};
if let Some(result) = router.match_route("/api/users", &opts)? {
println!("โ Matched! Route ID: {}", result.id);
println!(" Handler: {}", result.metadata["handler"]);
println!(" Params: {:?}", result.matched);
}
Ok(())
}
Match exact paths:
let routes = vec![
RadixNode {
id: "home".to_string(),
paths: vec!["/".to_string()],
methods: None,
hosts: None,
remote_addrs: None,
vars: None,
filter_fn: None,
priority: 0,
metadata: serde_json::json!({"page": "home"}),
},
];
let mut router = RadixRouter::new()?;
router.add_routes(routes)?;
let result = router.match_route("/", &RadixMatchOpts::default())?;
Extract dynamic segments from paths:
let routes = vec![
RadixNode {
id: "user_detail".to_string(),
paths: vec!["/user/:id/post/:pid".to_string()],
// ... other fields
metadata: serde_json::json!({"handler": "get_user_post"}),
},
];
let mut router = RadixRouter::new()?;
router.add_routes(routes)?;
let result = router.match_route("/user/123/post/456", &RadixMatchOpts::default())?
.expect("should match");
// Access route ID directly
assert_eq!(result.id, "user_detail");
// Extract path parameters
assert_eq!(result.matched.get("id").unwrap(), "123");
assert_eq!(result.matched.get("pid").unwrap(), "456");
Match remaining path segments:
let routes = vec![
RadixNode {
id: "static_files".to_string(),
paths: vec!["/files/*path".to_string()],
// ... other fields
metadata: serde_json::json!({"handler": "serve_file"}),
},
];
let mut router = RadixRouter::new()?;
router.add_routes(routes)?;
let result = router.match_route("/files/css/main.css", &RadixMatchOpts::default())?
.expect("should match");
assert_eq!(result.matched.get("path").unwrap(), "css/main.css");
Match specific HTTP methods:
let routes = vec![
RadixNode {
id: "users_api".to_string(),
paths: vec!["/api/users".to_string()],
methods: Some(RadixHttpMethod::GET | RadixHttpMethod::POST), // Multiple methods
// ... other fields
metadata: serde_json::json!({"handler": "users"}),
},
];
let mut router = RadixRouter::new()?;
router.add_routes(routes)?;
// GET - matches
let opts = RadixMatchOpts {
method: Some("GET".to_string()),
..Default::default()
};
assert!(router.match_route("/api/users", &opts)?.is_some());
// DELETE - doesn't match
let opts = RadixMatchOpts {
method: Some("DELETE".to_string()),
..Default::default()
};
assert!(router.match_route("/api/users", &opts)?.is_none());
Route based on hostname with wildcard support:
let routes = vec![
RadixNode {
id: "api_subdomain".to_string(),
paths: vec!["/api".to_string()],
methods: None,
hosts: Some(vec!["*.example.com".to_string()]), // Wildcard
// ... other fields
metadata: serde_json::json!({"handler": "api"}),
},
];
let mut router = RadixRouter::new()?;
router.add_routes(routes)?;
let opts = RadixMatchOpts {
host: Some("api.example.com".to_string()),
..Default::default()
};
assert!(router.match_route("/api", &opts)?.is_some());
Higher priority routes are matched first:
let routes = vec![
RadixNode {
id: "catch_all".to_string(),
paths: vec!["/api/*".to_string()],
priority: 0, // Lower priority
metadata: serde_json::json!({"handler": "fallback"}),
// ... other fields
},
RadixNode {
id: "specific".to_string(),
paths: vec!["/api/users".to_string()],
priority: 10, // Higher priority - matches first
metadata: serde_json::json!({"handler": "users"}),
// ... other fields
},
];
let mut router = RadixRouter::new()?;
router.add_routes(routes)?;
let result = router.match_route("/api/users", &RadixMatchOpts::default())?
.expect("should match");
assert_eq!(result.id, "specific"); // Higher priority route ID
assert_eq!(result.metadata["handler"], "users"); // Higher priority wins
Add custom matching logic:
use std::sync::Arc;
use std::collections::HashMap;
let routes = vec![
RadixNode {
id: "v2_api".to_string(),
paths: vec!["/api/data".to_string()],
filter_fn: Some(Arc::new(|vars, _opts| {
// Custom logic: check API version
vars.get("version").map(|v| v == "v2").unwrap_or(false)
})),
// ... other fields
metadata: serde_json::json!({"handler": "api_v2"}),
},
];
let mut router = RadixRouter::new()?;
router.add_routes(routes)?;
// With version variable - matches
let mut vars = HashMap::new();
vars.insert("version".to_string(), "v2".to_string());
let opts = RadixMatchOpts {
vars: Some(vars),
..Default::default()
};
assert!(router.match_route("/api/data", &opts)?.is_some());
// Without version - doesn't match
assert!(router.match_route("/api/data", &RadixMatchOpts::default())?.is_none());
Match based on request variables:
use router_radix::Expr;
use regex::Regex;
let routes = vec![
RadixNode {
id: "prod_api".to_string(),
paths: vec!["/api/users".to_string()],
vars: Some(vec![
Expr::Eq("env".to_string(), "production".to_string()),
Expr::Regex("user_agent".to_string(), Regex::new("Chrome")?),
]),
// ... other fields
metadata: serde_json::json!({"handler": "prod_users"}),
},
];
let mut router = RadixRouter::new()?;
router.add_routes(routes)?;
let mut vars = HashMap::new();
vars.insert("env".to_string(), "production".to_string());
vars.insert("user_agent".to_string(), "Chrome/120.0".to_string());
let opts = RadixMatchOpts {
vars: Some(vars),
..Default::default()
};
assert!(router.match_route("/api/users", &opts)?.is_some());
The MatchResult struct contains all information about a matched route:
pub struct MatchResult {
pub id: String, // Route ID - NEW in v0.4.0!
pub metadata: serde_json::Value, // Route metadata
pub matched: HashMap<String, String>, // Extracted parameters
}
if let Some(result) = router.match_route("/api/user/123", &opts)? {
// Direct access to route ID (no need to extract from metadata)
println!("Matched route ID: {}", result.id);
// Access route metadata
println!("Handler: {}", result.metadata["handler"]);
println!("Version: {}", result.metadata["version"]);
// Access extracted path parameters
println!("User ID: {}", result.matched.get("id").unwrap());
// Access system-provided information
println!("Full path: {}", result.matched.get("_path").unwrap());
println!("HTTP method: {}", result.matched.get("_method").unwrap());
}
The router uses anyhow::Result for proper error handling:
use router_radix::{RadixRouter, RadixMatchOpts};
use anyhow::Context;
fn handle_request(router: &RadixRouter, path: &str) -> anyhow::Result<String> {
let opts = RadixMatchOpts::default();
match router.match_route(path, &opts)? {
Some(result) => {
Ok(format!("Route ID: {}, Handler: {}", result.id, result.metadata["handler"]))
}
None => {
Ok("404 Not Found".to_string())
}
}
// System errors (e.g., lock errors) propagate via ?
}
Return Value Semantics:
Ok(Some(MatchResult)) โ Route found and matchedOk(None) โ No matching route (normal case, not an error)Err(anyhow::Error) โ System error (e.g., internal lock failure)The router is fully thread-safe and optimized for concurrent access:
use std::sync::Arc;
use std::thread;
fn main() -> anyhow::Result<()> {
let routes = vec![/* your routes */];
let mut router = RadixRouter::new()?;
router.add_routes(routes)?;
let router = Arc::new(router);
// Share across threads
let mut handles = vec![];
for i in 0..8 {
let router = Arc::clone(&router);
handles.push(thread::spawn(move || {
let opts = RadixMatchOpts {
method: Some("GET".to_string()),
..Default::default()
};
// Lock-free concurrent access
router.match_route("/api/users", &opts)
}));
}
for handle in handles {
handle.join().unwrap()?;
}
Ok(())
}
For dynamic route updates, wrap in an additional RwLock:
use std::sync::{Arc, RwLock};
let router = Arc::new(RwLock::new(RadixRouter::new()?));
// Write (exclusive)
router.write().unwrap().add_route(new_route)?;
// Read (shared, many concurrent readers)
router.read().unwrap().match_route("/path", &opts)?;
โ ๏ธ Best Practice: Initialize routes at startup for best performance.
Single Thread:
Multi-threaded (8 threads):
# Performance benchmark
cargo run --example benchmark --release
# Concurrency test
cargo run --example concurrency_test --release
# Stress test (10,000 routes, 16 threads)
cargo run --example stress_test --release
The project includes comprehensive examples:
| Example | Description | Lines |
|---|---|---|
basic.rs |
Basic usage and core features | 235 |
edge_cases.rs |
Boundary conditions and edge cases | 460 |
integration.rs |
Real-world API gateway scenarios | 630 |
vars_filter_test.rs |
Advanced filters and expressions | 506 |
benchmark.rs |
Performance benchmarks | 413 |
concurrency_test.rs |
Multi-threaded performance | 174 |
stress_test.rs |
Large-scale stress testing | 319 |
# Basic examples
cargo run --example basic
cargo run --example edge_cases
cargo run --example integration
cargo run --example vars_filter_test
# Performance tests (use --release)
cargo run --example benchmark --release
cargo run --example concurrency_test --release
cargo run --example stress_test --release
# Run all tests
./run_all_tests.sh --release
cargo test
๐ For detailed documentation, see examples/README.md
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Rust API Layer โ
โ โข Route matching & parameter extraction โ
โ โข Filter evaluation & priority sorting โ
โ โข Error handling (anyhow) โ
โ โข Thread-safe querying (RwLock + iterators) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Rust FFI Layer โ
โ โข Safe wrappers around C functions โ
โ โข RAII for resource management โ
โ โข Memory safety guarantees โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ C Layer (Redis rax) โ
โ โข Radix tree implementation โ
โ โข Battle-tested in Redis production โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
RadixRouter: Main router struct with thread-safe APIRadixNode: Route node definition with matching rulesRadixMatchOpts: Request matching optionsMatchResult: Matched route with extracted parametersExpr: Variable expression for conditional matchingFilterFn: Custom filter function typeApache-2.0
Built with โค๏ธ for high-performance routing