| Crates.io | route_controller |
| lib.rs | route_controller |
| version | 0.2.0 |
| created_at | 2025-01-08 18:21:00.185095+00 |
| updated_at | 2025-12-27 13:30:15.916046+00 |
| description | A procedural macro for generating Axum routers from controller-style implementations with support for route prefixing and middleware |
| homepage | https://github.com/athishaves/route_controller |
| repository | https://github.com/athishaves/route_controller |
| max_upload_size | |
| id | 1508902 |
| size | 246,534 |
Generate Axum routers from controller-style implementations with declarative extractors
extract() attributeJson, Form, Bytes, Text, Html, Xml, JavaScriptPath, QueryStateHeaderParam - Extract from HTTP headers (requires headers feature)CookieParam - Extract from cookies (requires cookies feature)SessionParam - Extract from session storage (requires sessions feature)header() and content_type() attributes
#[get], #[post], #[put], #[delete], #[patch], #[head], #[options], #[trace][dependencies]
route_controller = "0.2.0"
axum = "0.8" # Also works with axum 0.7 and earlier versions
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
The path parameter syntax depends on your Axum version:
{id} for path parameters#[get("/{id}", extract(id = Path))]
async fn get_user(id: u32) -> String {
format!("User {}", id)
}
:id for path parameters#[get("/:id", extract(id = Path))]
async fn get_user(id: u32) -> String {
format!("User {}", id)
}
For additional extractors, enable features and add required dependencies:
[dependencies]
route_controller = { version = "0.2.0", features = ["headers", "cookies", "sessions"] }
axum-extra = { version = "0.12", features = ["cookie"] } # Required for cookies
tower-sessions = "0.14" # Required for sessions
use route_controller::{controller, get, post};
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize)]
struct User {
name: String,
email: String,
}
struct UserController;
#[controller(path = "/users")]
impl UserController {
#[get]
async fn list() -> &'static str {
"User list"
}
#[get("/{id}", extract(id = Path))]
async fn get_one(id: u32) -> axum::Json<User> {
let user = User {
name: format!("User{}", id),
email: format!("user{}@example.com", id),
};
axum::Json(user)
}
#[post(extract(user = Json))]
async fn create(user: User) -> String {
format!("Created user: {} ({})", user.name, user.email)
}
}
#[tokio::main]
async fn main() {
let app = UserController::router();
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
.await
.unwrap();
println!("🚀 Server running on http://127.0.0.1:3000");
axum::serve(listener, app).await.unwrap();
}
extract() AttributeUse the extract() attribute to specify how each parameter should be extracted from the request. The order of extractors in the attribute can differ from the parameter order:
#[controller(path = "/api/users")]
impl ApiController {
// Single extractor
#[post("/", extract(user = Json))]
async fn create(user: User) -> String {
format!("Created: {}", user.name)
}
// Multiple Path extractors (order independent)
#[get("/{id}/posts/{post_id}", extract(post_id = Path, id = Path))]
async fn get_user_post(id: u32, post_id: u32) -> String {
format!("User {} - Post {}", id, post_id)
}
// Mixed extractors: Path + Json
#[put("/{id}", extract(id = Path, user = Json))]
async fn update(id: u32, user: User) -> String {
format!("Updated user {}", id)
}
// Path + Query extractors
#[get("/{id}/search", extract(id = Path, filters = Query))]
async fn search(id: u32, filters: SearchFilters) -> String {
format!("Searching for user {}", id)
}
}
Json - Extract JSON request body: extract(data = Json)
T where T: serde::Deserialize)application/jsonForm - Extract form data (form-data or x-www-form-urlencoded): extract(data = Form)
T where T: serde::Deserialize)application/x-www-form-urlencoded or multipart/form-dataBytes - Extract raw binary data: extract(data = Bytes)
Vec<u8>Text - Extract plain text: extract(content = Text)
Stringtext/plainHtml - Extract HTML content: extract(content = Html)
Stringtext/htmlXml - Extract XML content: extract(content = Xml)
Stringapplication/xml or text/xmlJavaScript - Extract JavaScript content: extract(code = JavaScript)
Stringapplication/javascript or text/javascriptPath - Extract path parameters: extract(id = Path)Query - Extract query parameters: extract(params = Query)State - Extract application state: extract(state = State)Enable additional extractors with Cargo features:
[dependencies]
route_controller = { version = "0.2.0", features = ["headers", "cookies", "sessions"] }
axum-extra = { version = "0.12", features = ["cookie"] } # Required for cookies
tower-sessions = "0.14" # Required for sessions
HeaderParam - Extract from HTTP headers (requires headers feature)
#[get("/api/data", extract(authorization = HeaderParam))]
async fn get_data(authorization: String) -> String {
format!("Auth: {}", authorization)
}
CookieParam - Extract from cookies (requires cookies feature + axum-extra)
#[get("/profile", extract(session_id = CookieParam))]
async fn get_profile(session_id: String) -> String {
format!("Session: {}", session_id)
}
SessionParam - Extract from session storage (requires sessions feature + tower-sessions)
#[get("/profile", extract(user_id = SessionParam))]
async fn get_profile(user_id: String) -> String {
format!("User ID: {}", user_id)
}
Extract application state in your handlers using the State extractor:
use std::sync::Arc;
use tokio::sync::RwLock;
#[derive(Clone)]
struct AppState {
counter: Arc<RwLock<i32>>,
}
struct CounterController;
#[controller(path = "/counter")]
impl CounterController {
#[get(extract(state = State))]
async fn get_count(state: AppState) -> axum::Json<i32> {
let count = *state.counter.read().await;
axum::Json(count)
}
#[post("/increment", extract(state = State))]
async fn increment(state: AppState) -> axum::Json<i32> {
let mut counter = state.counter.write().await;
*counter += 1;
axum::Json(*counter)
}
}
#[tokio::main]
async fn main() {
let app_state = AppState {
counter: Arc::new(RwLock::new(0)),
};
let app = axum::Router::new()
.merge(CounterController::router())
.with_state(app_state);
// Start server...
}
Handle form submissions with the Form extractor:
#[derive(Deserialize)]
struct LoginForm {
username: String,
password: String,
}
#[controller(path = "/auth")]
impl AuthController {
#[post("/login", extract(form = Form))]
async fn login(form: LoginForm) -> String {
format!("Logging in user: {}", form.username)
}
}
Test with:
curl -X POST http://localhost:3000/auth/login \
-d 'username=john&password=secret123'
Handle file uploads or binary data with the Bytes extractor:
#[controller(path = "/files")]
impl FileController {
#[post("/upload", extract(data = Bytes))]
async fn upload(data: Vec<u8>) -> String {
format!("Received {} bytes", data.len())
}
}
Handle various text-based content types:
#[controller(path = "/content")]
impl ContentController {
// Plain text
#[post("/text", extract(content = Text))]
async fn handle_text(content: String) -> String {
format!("Received text: {}", content)
}
// HTML content
#[post("/html", extract(html = Html))]
async fn handle_html(html: String) -> String {
format!("Received {} chars of HTML", html.len())
}
// XML content
#[post("/xml", extract(xml = Xml))]
async fn handle_xml(xml: String) -> String {
format!("Received XML: {}", xml)
}
// JavaScript code
#[post("/script", extract(code = JavaScript))]
async fn handle_script(code: String) -> String {
format!("Received {} chars of JavaScript", code.len())
}
}
Add custom headers to your responses using the header() and content_type() attributes at both the controller and route levels.
Apply headers to all routes in a controller. Route-level headers with the same name will override controller-level headers:
#[controller(
path = "/api",
header("x-api-version", "1.0"),
header("x-powered-by", "route-controller")
)]
impl ApiController {
// Inherits both controller headers
#[get("/data")]
async fn get_data() -> String {
"Data with controller headers".to_string()
}
// Overrides x-api-version, keeps x-powered-by
#[get("/v2", header("x-api-version", "2.0"))]
async fn get_data_v2() -> String {
"Data with overridden version".to_string()
}
// Adds route-specific header, keeps controller headers
#[get("/special", header("x-request-id", "abc-123"))]
async fn special() -> String {
"Special endpoint".to_string()
}
}
#[controller(path = "/api")]
impl ApiController {
#[get("/data", header("x-api-version", "1.0"))]
async fn get_data() -> String {
"Data with custom header".to_string()
}
}
#[controller(path = "/api")]
impl ApiController {
#[get(
"/info",
header("x-api-version", "2.0"),
header("x-request-id", "abc-123")
)]
async fn get_info() -> String {
"Info with multiple headers".to_string()
}
}
Set content-type at controller or route level:
// Controller-level content-type applies to all routes
#[controller(path = "/api", content_type("application/json"))]
impl ApiController {
// Inherits application/json content-type
#[get("/data")]
async fn get_data() -> String {
r#"{"status":"ok"}"#.to_string()
}
// Route overrides to XML
#[get("/xml", content_type("application/xml"))]
async fn get_xml() -> String {
r#"<?xml version="1.0"?>
<response>
<message>Hello XML</message>
</response>"#.to_string()
}
// Route overrides to plain text
#[get("/text", content_type("text/plain; charset=utf-8"))]
async fn get_text() -> String {
"Plain text response".to_string()
}
}
Controller headers provide a base set of headers, and routes can override or extend them:
// Controller provides base headers and content-type
#[controller(
path = "/api",
content_type("application/json"),
header("x-api-version", "1.0"),
header("x-service", "my-api")
)]
impl ApiController {
// Inherits all controller headers
#[get("/info")]
async fn get_info() -> axum::Json<Response> {
axum::Json(Response { status: "ok".to_string() })
}
// Override version and content-type, keep x-service
#[post(
"/data",
content_type("application/json; charset=utf-8"),
header("x-api-version", "2.0"),
header("x-rate-limit", "100")
)]
async fn post_data() -> axum::Json<Response> {
axum::Json(Response { status: "ok".to_string() })
}
}
Test with:
# Check inherited headers
curl -v http://localhost:3000/api/info
# Output: x-api-version: 1.0, x-service: my-api, content-type: application/json
# Check overridden headers
curl -v http://localhost:3000/api/data
# Output: x-api-version: 2.0, x-service: my-api, x-rate-limit: 100
The crate includes 15 comprehensive examples demonstrating different features:
# 1. Basic routing with different HTTP methods (GET, POST, PUT, DELETE)
cargo run --example 01_basic_routing
# 2. Path parameter extraction
cargo run --example 02_path_params
# 3. Query parameter extraction
cargo run --example 03_query_params
# 4. JSON body extraction
cargo run --example 04_json_body
# 5. Form data handling (form-data and x-www-form-urlencoded)
cargo run --example 05_form_data
# 6. Text body extraction
cargo run --example 06_text_body
# 7. Binary data (bytes) handling
cargo run --example 07_bytes
# 8. Header extraction (requires 'headers' feature)
cargo run --example 08_headers --features headers
# 9. Cookie handling (requires 'cookies' feature)
cargo run --example 09_cookies --features cookies
# 10. Session management (requires 'sessions' feature)
cargo run --example 10_sessions --features sessions
# 11. Application state management
cargo run --example 11_state
# 12. Response headers and content types
cargo run --example 12_response_headers
# 13. Middleware application
cargo run --example 13_middleware
# 14. Mixed extractors (Path + Query + Json)
cargo run --example 14_mixed_extractors
# 15. Multiple controllers with merged routers
cargo run --example 15_multiple_controllers
Each example includes:
Apply middleware at the controller level:
use axum::{
middleware::Next,
extract::Request,
response::Response,
};
async fn log_middleware(request: Request, next: Next) -> Response {
println!("Request: {} {}", request.method(), request.uri());
next.run(request).await
}
#[controller(path = "/api", middleware = log_middleware)]
impl ApiController {
#[get("/data")]
async fn get_data() -> String {
"Protected data".to_string()
}
}
You can also apply multiple middlewares:
#[controller(
path = "/api",
middleware = middleware_a,
middleware = middleware_b
)]
impl MultiMiddlewareController {
#[get("/test")]
async fn test() -> &'static str {
"ok"
}
}
See examples/13_middleware.rs for a complete example.
Enable verbose logging during compilation by setting the ROUTE_CONTROLLER_VERBOSE environment variable:
ROUTE_CONTROLLER_VERBOSE=1 cargo build
ROUTE_CONTROLLER_VERBOSE=1 cargo run --example basic
This shows detailed information about route registration during compilation.
MIT