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: u8u128, i8i128, 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 |