You are reading the docs for rws 18.30 The latest release is 18.34. Read this page for the latest release.

Routing

Path-based routing with named parameters, wildcards, and virtual-host scoping using Router and the routes! macro.

routes! macro

The routes! macro is the recommended way to declare routes for new code. It builds an AppWithState<S> (or any builder that exposes .get(), .post(), etc.) from a declarative table.

use rust_web_server::app::App;
use rust_web_server::routes;
use rust_web_server::request::Request;
use rust_web_server::router::PathParams;
use rust_web_server::server::ConnectionInfo;
use rust_web_server::response::{Response, STATUS_CODE_REASON_PHRASE};
use rust_web_server::core::New;

struct Db;

fn list_users(_req: &Request, _params: &PathParams, _conn: &ConnectionInfo, _db: &Db) -> Response {
    let mut r = Response::new();
    r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
    r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
    r
}

fn create_user(_req: &Request, _params: &PathParams, _conn: &ConnectionInfo, _db: &Db) -> Response {
    let mut r = Response::new();
    r.status_code = *STATUS_CODE_REASON_PHRASE.n201_created.status_code;
    r.reason_phrase = STATUS_CODE_REASON_PHRASE.n201_created.reason_phrase.to_string();
    r
}

let app = routes! {
    App::with_state(Db),
    GET  "/users"     => list_users,
    POST "/users"     => create_user,
};

Syntax:

routes! {
    <builder_expression>,
    METHOD "pattern" => handler_or_closure,
    ...
}

Valid methods: GET, POST, PUT, PATCH, DELETE (all caps). A trailing comma after the last route is optional.

Router — direct usage

Use Router directly when you need to compose multiple routers or call handle inside a custom Application::execute.

use rust_web_server::router::{Router, PathParams};
use rust_web_server::response::{Response, STATUS_CODE_REASON_PHRASE};
use rust_web_server::range::Range;
use rust_web_server::mime_type::MimeType;
use rust_web_server::core::New;

let router = Router::new()
    .get("/users/:id", |_req, params, _conn| {
        let id = params.get("id").unwrap_or("unknown");
        let mut r = Response::new();
        r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
        r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
        r.content_range_list = vec![Range::get_content_range(
            format!("user {}", id).into_bytes(),
            MimeType::TEXT_PLAIN.to_string(),
        )];
        r
    })
    .delete("/users/:id", |_req, params, _conn| {
        let mut r = Response::new();
        r.status_code = *STATUS_CODE_REASON_PHRASE.n204_no_content.status_code;
        r.reason_phrase = STATUS_CODE_REASON_PHRASE.n204_no_content.reason_phrase.to_string();
        r
    });

// Inside Application::execute:
// if let Some(response) = router.handle(request, connection) {
//     return Ok(response);
// }

router.handle(request, connection) returns Some(Response) on the first match, None if no route matches. Routes are tried in registration order.

Pattern syntax

Syntax Description Example
/literal Exact path segment /users
/:name or /{name} Named segment — captures one segment /users/:id, /users/{id}
/*name or /{*name} Wildcard — captures everything after the prefix /files/*path, /files/{*path}
/{{text}} Literal braces /raw/{{id}} matches /raw/{id}

The braced forms (since 18.7.0) are what axum 0.8 and OpenAPI use; the two syntaxes mean the same thing and can be mixed. A brace parameter takes a whole segment — /file.{ext} is a literal, not a parameter.

Rules:

  • A wildcard (*name, {*name}) must be the last segment in a pattern.
  • The query string is stripped before matching; only the path is used.
  • Patterns are matched segment-by-segment; /users does not match /users/42.
// /users/42/posts/7 → id="42", post_id="7"
router.get("/users/:id/posts/:post_id", handler);

// /files/a/b/c.txt → path="a/b/c.txt"
router.get("/files/*path", handler);

PathParams extraction

Inside a handler, call params.get("name") to retrieve a named segment value. It returns Option<&str>.

.get("/articles/:slug", |_req, params, _conn| {
    let slug = params.get("slug").unwrap_or("unknown");
    // use slug ...
    Response::new()
})

Virtual-host routing

Call .with_host("example.com") before registering routes to restrict a router to requests whose SNI hostname (TLS) or Host header (plain HTTP) matches that value.

let api_router = Router::new()
    .with_host("api.example.com")
    .get("/status", api_status_handler);

let www_router = Router::new()
    .with_host("www.example.com")
    .get("/", home_handler);

// In Application::execute:
// api_router.handle(request, connection)
//     .or_else(|| www_router.handle(request, connection))

handle() returns None immediately when .with_host() is set and the incoming request's hostname does not match, so composing multiple host-scoped routers is safe and efficient.

Relationship to App's built-in routes

App's built-in controllers — static file serving, /healthz//readyz//metrics, form handling, the 404 fallback — deliberately don't go through Router. That set is small, static, and known at compile time, so a fixed if-chain is simpler and just as fast as a segment matcher would be there. Router is for your routes, with dynamic path params and wildcards a fixed if-chain can't express cleanly.

AppWithState and AsyncAppWithState already compose the two: they hold a Router internally, try it first, and fall through to App's built-in controller chain for anything the router doesn't match — so static files, health checks, and metrics keep working automatically even in a fully custom, stateful app.

Typed handlers: #[get] with extractors

Since 18.28.0 a handler can take extractors as arguments and return anything that implements IntoResponse; the route attribute (macros feature) generates the adapter and the app takes the method and pattern from it:

# Cargo.toml
rust-web-server = { version = "18", features = ["macros", "serde"] }
use rust_web_server::app::App;
use rust_web_server::error::AppError;
use rust_web_server::handler::{Json, Path, State};
use rust_web_server::{get, post};

struct Db { /* pool, … */ }

#[get("/posts/{id}")]
fn show_post(Path(id): Path<u64>, State(db): State<Db>) -> Result<Json<Post>, AppError> {
    db.find(id).map(Json).ok_or_else(|| AppError::NotFound(format!("no post {id}")))
}

#[post("/posts")]
fn create_post(Json(new): Json<NewPost>, db: &Db) -> Result<Json<Post>, AppError> {
    db.insert(new).map(Json)
}

let app = App::with_state(Db { /* … */ })
    .route(show_post)
    .route(create_post);
  • #[get], #[post], #[put], #[patch], #[delete] and #[route(METHOD, "/path")] turn the function into a unit struct of the same name implementing handler::Handler<S> (and AsyncHandler<S>). .route(h) registers it on AppWithState, on Router (handlers without state arguments) and on AsyncAppWithState, where async fn handlers go too. The function as written stays callable as show_post::call(Path(1), State(db)), e.g. in unit tests.
  • The generated handle runs the extractors in argument order and calls the function: the same work as a handler that calls them by hand, behind the same route dispatch, with no extra allocation for a classic handler's path.
  • The first extractor that fails answers the request with its rejection (400, 415, 422, … as problem details); the function doesn't run.
  • show_post::METHOD and show_post::PATTERN (trait handler::RouteSpec) are the attribute's arguments.

Checked at compile time — the route's parameters against the Path argument:

Mistake Error
#[get("/posts/{id}")] fn f(Path(post_id): Path<u64>) `post_id` is not a parameter of route `GET /posts/{id}`
#[get("/posts/{id}")] fn f(Path((id, slug)): Path<(u64, String)>) the tuple takes 2, the route has 1
#[get("/posts/{id}/{slug}")] fn f(Path(id): Path<u64>) a scalar takes one parameter
#[post("/posts/{id}/comments")] fn f(State(db): State<Db>) {id} isn't taken by any Path (or &PathParams) argument
#[get("/posts")] fn f(Path(id): Path<u64>) the route has no parameters
a #[derive(FromPath)] struct whose fields aren't exactly the route's parameters a const assertion naming the route

A name bound inside Path(…) names the route parameter it takes (as in Rocket), so a tuple's bindings may come in any order: Path((slug, id)) for /posts/{id}/{slug}. A plain binding (p: Path<(u64, String)>) or _ takes them in route order. /posts/:id and /posts/{id} are the same route.

Classic handlers are unchanged. On a function with the classic signature — (&Request, &PathParams, &ConnectionInfo) plus the state for AppWithState (or AsyncAppWithState's owned form) — the attribute only adds a Route: METHOD /path doc comment, as before 18.28.0, and you register it with .get(pattern, handler) or routes!.

Route conflicts

A route registered after one with the same method that matches every path it does never runs: routes are tried in order and the first match wins. AppWithState, AsyncAppWithState and Router report these when the route is added — one line on stderr per distinct conflict — and list them with route_conflicts():

let app = App::with_state(db)
    .get("/users/:id", show_user)
    .get("/users/me", me)          // shadowed: /users/:id answers /users/me
    .get("/users/{uid}", other);   // duplicate: same pattern as /users/:id

for conflict in app.route_conflicts() {
    eprintln!("{conflict}");
}
// rws: warning: route GET /users/me never runs: GET /users/:id, registered before it, matches every path it does

Each RouteConflict has the method, the pattern that never runs, the earlier one that answers instead, and a kind (Duplicate: the same pattern, parameter names aside; Shadowed: the earlier one has a parameter or wildcard where it has a literal). It's a warning only — the app is built and serves as before; register the specific route first (/users/me, then /users/:id). Patterns that only overlap (/a/:x and /:y/b) aren't reported. A test can assert app.route_conflicts().is_empty().

See the API reference for the full list of public types.

Edit this page on GitHub