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: 0Trailing-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+JSONfallback 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:
| Area | 1.x | 2.0 |
|---|---|---|
| Sub-routing | Router::merge (mutates a shared Arc<Route>) | nest / scope |
| Wrong-method response | 404 Not Found | 405 + Allow |
| Macro path syntax | {id: u64} only, always materialised as Params | {id} and {id: u64} both supported |
| Per-router state | GLOBAL_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.