Routing
Neutron uses a compressed radix tree (trie) router for O(1) route matching. Routes are defined with method-specific builders and type-safe extractors.
Defining Routes
let router = Router::new()
.get("/users", list_users)
.get("/users/:id", get_user)
.post("/users", create_user)
.put("/users/:id", update_user)
.delete("/users/:id", delete_user)
.patch("/items/:id", patch_item)
.head("/status", check_status)
.options("/*", cors_preflight)
.any("/fallback", any_method_handler);
Path Parameters
Extract typed values from URL segments:
// Single parameter
async fn get_user(Path(id): Path<u64>) -> Json<User> {
// /users/42 → id = 42
}
// Multiple parameters
async fn get_repo(Path((org, repo)): Path<(String, String)>) -> String {
// /repos/neutron/framework → org = "neutron", repo = "framework"
format!("{}/{}", org, repo)
}
// Wildcard (catch-all)
async fn serve_file(Path(path): Path<String>) -> Response {
// /files/images/logo.png → path = "images/logo.png"
}
Supported types: u8–u128, i8–i128, f32, f64, bool, String.
Query Parameters
#[derive(Deserialize)]
struct Filters {
q: String,
limit: Option<u32>,
offset: Option<u32>,
}
async fn search(Query(filters): Query<Filters>) -> Json<Vec<Item>> {
// /search?q=rust&limit=10
}
Request Body
JSON
#[derive(Deserialize)]
struct CreateUser {
name: String,
email: String,
}
async fn create_user(Json(body): Json<CreateUser>) -> (StatusCode, Json<User>) {
(StatusCode::CREATED, Json(User { id: 1, name: body.name }))
}
Form
#[derive(Deserialize)]
struct LoginForm {
username: String,
password: String,
}
async fn login(Form(form): Form<LoginForm>) -> Response {
// application/x-www-form-urlencoded
}
State
Share application state across handlers:
struct AppState {
db: DatabasePool,
config: Config,
}
let router = Router::new()
.state(AppState { db, config })
.get("/users", list_users);
async fn list_users(State(state): State<AppState>) -> Json<Vec<User>> {
let users = state.db.query("SELECT * FROM users").await;
Json(users)
}
Extensions
Per-request data set by middleware:
async fn handler(Extension(claims): Extension<Claims>) -> String {
// Claims injected by JwtAuth middleware
format!("Hello, {}", claims.sub.unwrap_or_default())
}
Nested Routers
Group routes with shared middleware:
let api = Router::new()
.middleware(JwtAuth::new(config))
.get("/items", list_items)
.post("/items", create_item);
let admin = Router::new()
.middleware(AdminOnly)
.get("/users", list_all_users)
.delete("/users/:id", delete_user);
let router = Router::new()
.middleware(Logger)
.nest("/api", api) // JwtAuth applies to /api/*
.nest("/admin", admin) // AdminOnly applies to /admin/*
.get("/health", health);
Static Files
let router = Router::new()
.static_files("/assets", "./public");
Fallback Handler
let router = Router::new()
.get("/", home)
.fallback(|| async { (StatusCode::NOT_FOUND, "Not Found") });
Handler Signatures
Handlers are async functions that take up to 12 extractors and return anything implementing IntoResponse:
// Zero extractors
|| async { "ok" }
// Multiple extractors
async fn handler(
Path(id): Path<u64>,
Query(q): Query<Filters>,
State(db): State<Pool>,
Json(body): Json<CreateItem>,
) -> Result<Json<Item>, AppError> {
// ...
}
IntoResponse Implementations
| Return Type | Status | Content-Type |
|-------------|--------|--------------|
| String / &str | 200 | text/plain |
| Json<T> | 200 | application/json |
| (StatusCode, T) | custom | from T |
| Response | custom | custom |
| Result<T, E> | 200 or error | from T |