Coming from Axum
Map Axum routers, extractors, state, middleware, serving, and tests to their Tako equivalents, with the differences that matter when porting.
Tako's handler model is close to Axum's: handlers are async functions, their
arguments are typed extractors, and their return values implement a response
trait. Path syntax is the same {id} form Axum 0.8 uses. Most ports are
mechanical; this page maps each Axum building block to Tako and calls out the
places where the two differ.
At a glance
| Axum 0.8 | Tako 2.x |
|---|---|
Router::new().route("/", get(h)) | router.get("/", h) |
get(show).post(update) | router.get(p, show), router.post(p, update) |
Path, Query, Json, State | Same names in tako::extractors |
http::HeaderMap | header_map::HeaderMap wrapper |
IntoResponse | Responder |
Router::with_state | router.with_state(value) per type |
middleware::from_fn | router.middleware(f) |
route_layer | route.middleware(f) |
tower-http layers | Bundled middleware and plugins |
nest, fallback | router.nest, router.fallback |
axum::serve | Server::builder() |
with_graceful_shutdown | handle.shutdown_on_signal() |
WebSocketUpgrade | TakoWs::new(req, handler) |
sse::Sse, Event | Sse::events, SseEvent |
ServiceExt::oneshot | router.dispatch(request) |
Dependencies
[dependencies]
serde = { version = "1", features = ["derive"] }
tako-rs = { version = "2.4", features = ["plugins", "ws", "sse"] }
tokio = { version = "1", features = ["macros", "net", "rt-multi-thread"] }The package is tako-rs and the Rust import is tako. The crate named tako on
crates.io is an unrelated project. Transports and integrations are Cargo
features; the feature reference lists them all.
Routes and handlers
Axum chains routes onto a router value:
let app = Router::new()
.route("/users", get(list_users).post(create_user))
.route("/users/{id}", get(show_user));Tako registers each method on a mutable router:
use tako::router::Router;
let mut router = Router::new();
router.get("/users", list_users);
router.post("/users", create_user);
router.get("/users/{id}", show_user);Handlers keep their shape. As in Axum, only the last argument may consume the request body.
Extractors
use serde::Deserialize;
use tako::extractors::{header_map::HeaderMap, json::Json, path::Path, query::Query, state::State};
use tako::{responder::Responder, StatusCode};
#[derive(Deserialize)]
struct Pagination {
page: u32,
}
async fn list_users(Query(p): Query<Pagination>, State(state): State<AppState>) -> String {
format!("page {} from {}", p.page, state.db_url)
}
async fn show_user(Path(id): Path<u64>) -> String {
format!("user {id}")
}
async fn user_agent(HeaderMap(headers): HeaderMap) -> String {
format!("{:?}", headers.get("user-agent"))
}
async fn create_user(Json(body): Json<serde_json::Value>) -> impl Responder {
(StatusCode::CREATED, Json(body))
}Three differences matter when porting:
State<T>holds anArc<T>, and state is keyed by type. Callwith_stateonce per type and extract any subset in each handler; there is noFromRef.HeaderMapis a tuple wrapper aroundhttp::HeaderMap, so destructure it.- Buffered body extractors reject bodies over 2 MiB by default, much like Axum's
DefaultBodyLimit. Change it withrouter.body_limit(bytes).
The extractor catalog covers the rest, including cookies, forms, multipart, JWT claims, and protobuf.
Responses and errors
Return impl Responder where Axum returns impl IntoResponse. Strings, JSON,
status codes, and (StatusCode, R) tuples all implement it. A handler can return
Result<T, E> when both T and E implement Responder, so a custom error type
implements Responder the way it would implement IntoResponse in Axum. See
Building a REST API for a full error type.
State
#[derive(Clone)]
struct AppState {
db_url: String,
}
let mut router = Router::new();
router.with_state(AppState { db_url: "postgres://localhost/app".into() });Routers nested under this one see the same state. Because state is looked up at
runtime instead of being part of the router's type, a handler that extracts a
type nobody registered compiles, and responds with 500 Internal Server Error.
Middleware
An Axum middleware::from_fn function ports with the same signature:
use tako::middleware::Next;
use tako::types::{Request, Response};
async fn require_api_key(req: Request, next: Next) -> Response {
if req.headers().contains_key("x-api-key") {
next.run(req).await
} else {
StatusCode::UNAUTHORIZED.into_response()
}
}
router.middleware(require_api_key);
router.get("/admin", admin).middleware(require_api_key);router.middleware wraps every route; .middleware on a route wraps only that
route, like Axum's route_layer.
Tako does not implement Tower's Service and Layer traits, so tower-http
layers do not plug in. The common ones have bundled equivalents:
| Axum ecosystem | Tako |
|---|---|
CorsLayer | tako::plugins::cors::CorsBuilder (plugins) |
CompressionLayer | tako::plugins::compression::CompressionBuilder (plugins) |
DefaultBodyLimit, RequestBodyLimitLayer | router.body_limit(bytes) |
TimeoutLayer | router.timeout(duration) or route.timeout(duration) |
SetRequestIdLayer | tako::middleware::request_id::RequestId |
tower-sessions | tako::middleware::session::SessionMiddleware |
tower_governor | tako::plugins::rate_limiter::RateLimiterBuilder (plugins) |
Plugins register with router.plugin(...), and middleware structs convert with
.into_middleware():
use tako::middleware::{request_id::RequestId, IntoMiddleware};
use tako::plugins::{compression::CompressionBuilder, cors::CorsBuilder};
router.plugin(CorsBuilder::new().build());
router.plugin(CompressionBuilder::new().build());
router.middleware(RequestId::new().into_middleware());Authentication (BasicAuth, BearerAuth, ApiKeyAuth, JwtAuth), CSRF, and
security headers are covered in Middleware.
Nesting and fallbacks
let mut api = Router::new();
api.get("/users/{id}", show_user);
let mut app = Router::new();
app.nest("/api", api);
app.fallback(|_req: Request| async { (StatusCode::NOT_FOUND, "nothing here") });Serving and shutdown
Axum hands the router to axum::serve:
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await?;
axum::serve(listener, app)
.with_graceful_shutdown(shutdown_signal())
.await?;Tako spawns the server and returns a handle:
use tako::Server;
use tokio::net::TcpListener;
let listener = TcpListener::bind("0.0.0.0:8080").await?;
let handle = Server::builder().build().try_spawn_http(listener, app)?;
handle.shutdown_on_signal().await?;shutdown_on_signal waits for Ctrl+C or SIGTERM, then drains in-flight requests
within the drain timeout. Use handle.result().await? to wait without handling
signals. The same builder serves TLS, h2c, HTTP/3, and Unix sockets; see
Transports. Axum needs axum-server or a rustls acceptor for
TLS and has no HTTP/3 server.
WebSocket and SSE
A Tako WebSocket handler receives the whole request instead of a
WebSocketUpgrade extractor, and the socket is a tokio-tungstenite
WebSocketStream:
use futures_util::{SinkExt, StreamExt};
use tako::ws::TakoWs;
async fn echo(req: Request) -> impl Responder {
TakoWs::new(req, |mut ws| async move {
while let Some(Ok(msg)) = ws.next().await {
if (msg.is_text() || msg.is_binary()) && ws.send(msg).await.is_err() {
break;
}
}
})
}Axum's Sse takes a stream of Result<Event, E>; Tako's Sse::events takes a
stream of SseEvent:
use futures_util::{stream, StreamExt};
use tako::sse::{Sse, SseEvent};
async fn ticks(_: Request) -> impl Responder {
Sse::events(stream::iter(0..3).map(|i| SseEvent::data(format!("tick {i}"))))
}See WebSocket and Server-Sent Events for keep-alive, limits, and Compio.
Testing
Axum tests drive the router through tower::ServiceExt::oneshot. A Tako router
dispatches a request directly, with no server or socket:
use tako::body::TakoBody;
let mut request = Request::new(TakoBody::empty());
*request.uri_mut() = "/api/users/7".parse()?;
let response = app.dispatch(request).await;
assert_eq!(response.status(), StatusCode::OK);No direct equivalent
- Tower compatibility. Tako has its own middleware and plugin traits.
#[debug_handler]. Handler errors surface as ordinary trait-bound errors.- Typed router state. State lives in a per-router, type-keyed store instead
of
Router<S>.
Tako also has pieces Axum does not ship: typed route macros such as
#[tako::get("/users/{id: u64}")] (see Routing), background
queues, in-process signals, and the
thread-per-core server.
Last updated on
Quickstart
Add the tako-rs dependency, write a handler, register a GET route on Router, and start it with Server::builder and a TcpListener.
Using Tako with AI assistants
Point coding agents at Tako's llms.txt, Markdown docs, and Context7 index, and give them project rules so they write current Tako 2.x code.