🐙 tako

Routing

Map (method, path) pairs to handlers, run middleware, nest sub-routers, and emit method-aware responses with Tako's Router.

Routing

Router is the core dispatch type. It maps (method, path) pairs to handlers, runs middleware, and answers 405 Method Not Allowed (with a populated Allow header) when the path matches but the method does not. Path syntax is matchit-compatible: {name} for a free segment, {*rest} for a catch-all, and {name: T} only inside the #[tako::route] macro family for compile-time-typed slots.

Building a router

use tako::Method;
use tako::router::Router;

let mut router = Router::new();

// Register routes with the explicit method form, …
router.route(Method::GET, "/", root);
router.route(Method::POST, "/users", create_user);

// … or the shorthand methods.
router.get("/health", health);
router.put("/users/{id}", update_user);
router.delete("/users/{id}", delete_user);

async fn root() -> &'static str { "ok" }
async fn health() -> &'static str { "ok" }
async fn create_user() -> &'static str { "created" }
async fn update_user() -> &'static str { "updated" }
async fn delete_user() -> &'static str { "deleted" }

Router::route returns Arc<Route>. Attach per-route middleware by chaining .middleware(mw) on the returned route (the Router::middleware on the router itself adds global middleware that runs on every request).

Dynamic path parameters

use serde::Deserialize;
use tako::Method;
use tako::extractors::path::Path;
use tako::responder::Responder;
use tako::router::Router;

#[derive(Deserialize)]
struct UserPath { id: u64 }

async fn show_user(Path(p): Path<UserPath>) -> impl Responder {
  format!("user_id={}", p.id)
}

let mut router = Router::new();
router.route(Method::GET, "/users/{id}", show_user);

For catch-all suffixes use {*rest}:

router.get("/static/{*rest}", serve_static);

See Extractors for the full catalog of types you can bind to handler arguments, including Path<T>.

Typed slots via the route macro

The #[tako::get] / #[tako::post] / #[tako::route] macros parse {name: T} at registration time and reject paths whose parameters don't deserialise:

use tako::get;
use tako::extractors::typed_params::TypedParams;
use tako::responder::Responder;
use tako::router::Router;

#[get("/users/{id: u64}/posts/{post_id: u64}")]
async fn show_post(TypedParams(p): TypedParams<ShowPost>) -> impl Responder {
  format!("user={} post={}", p.user_id, p.post_id)
}

#[derive(serde::Deserialize)]
struct ShowPost { id: u64, post_id: u64 }

#[tokio::main]
async fn main() -> anyhow::Result<()> {
  let mut router = Router::new();
  router.mount_all(); // picks up every macro-registered route

  let listener = tako::bind_with_port_fallback("127.0.0.1:3000").await?;
  tako::serve(listener, router).await;
  Ok(())
}

mount_all() collects every route registered via the macro through the linkme distributed-slice. To mount them under a prefix use mount_all_into("/api/v1"). See examples/typed-routes.

Sub-routing: nest and scope

Both attach a prefix to a group of routes. They differ in when the grouping happens:

  • nest(prefix, child_router) — mount an entire existing router under a path prefix. Useful when you want a self-contained child router that can be tested independently.
  • scope(prefix, |r| { ... }) — declare a temporary prefix and add routes inside the closure. Useful for inline grouping inside one builder.
use tako::router::Router;
use tako::responder::Responder;

async fn list_users() -> impl Responder { "users" }
async fn create_user() -> impl Responder { "created" }
async fn dashboard() -> impl Responder { "admin home" }

// child router, mounted with `nest`
let mut api_v1 = Router::new();
api_v1.get("/users", list_users);
api_v1.post("/users", create_user);

let mut root = Router::new();
root.nest("/api/v1", api_v1);

// inline grouping with `scope`
root.scope("/admin", |r| {
  r.get("/", dashboard);
});

nest clones each child route via Route::cloned_with_path, so re-nesting the same child does not double-stack its middleware. The child router's global middleware chain is prepended to each nested route's middleware chain at registration time. Route-level plugins and the child's fallback / error handlers are not inherited — they are router-scoped.

Method-aware responses

When a path matches but the method does not, Tako returns 405 Method Not Allowed with an Allow header listing the supported methods. This is different from v1, which returned plain 404 Not Found for the same case.

$ curl -s -X DELETE http://127.0.0.1:8080/users -i
HTTP/1.1 405 Method Not Allowed
allow: GET, POST
content-length: 0

Trailing-slash redirects (route_with_tsr)

route(Method::GET, "/foo", h) matches only /foo. If you also want /foo/ to redirect to the canonical form (308 Permanent Redirect), register the route with route_with_tsr:

router.route_with_tsr(Method::GET, "/api", api_handler);
// "/api"  -> handler
// "/api/" -> 308 -> "/api"

The root path ("/") is rejected at registration time because it has no canonical sibling to redirect to.

Fallback and error handlers

router.fallback(|_req: tako::types::Request| async {
  (http::StatusCode::NOT_FOUND, "not found")
});

router.client_error_handler(|err| async move {
  (http::StatusCode::BAD_REQUEST, format!("{err}"))
});

router.use_problem_json();   // map all errors through RFC 7807 Problem+JSON

fallback runs when no route matches at all. error_handler / client_error_handler shape how extractor / handler errors hit the wire. use_problem_json() is a convenience preset.

What changed since 1.x

A short summary; the full table lives in the Migration guide:

Area1.x2.0
Sub-routingRouter::merge (mutates a shared Arc<Route>)nest / scope
Wrong-method response404 Not Found405 + Allow
Macro path syntax{id: u64} only, always materialised as Params{id} and {id: u64} both supported
Per-router stateGLOBAL_STATE (one slot per TypeId per process)Router::with_state (instance-local)

Server bootstrap and TLS knobs also changed substantially — see the Transports overview and the migration guide.

On this page