🐙 tako
Getting started

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.8Tako 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, StateSame names in tako::extractors
http::HeaderMapheader_map::HeaderMap wrapper
IntoResponseResponder
Router::with_staterouter.with_state(value) per type
middleware::from_fnrouter.middleware(f)
route_layerroute.middleware(f)
tower-http layersBundled middleware and plugins
nest, fallbackrouter.nest, router.fallback
axum::serveServer::builder()
with_graceful_shutdownhandle.shutdown_on_signal()
WebSocketUpgradeTakoWs::new(req, handler)
sse::Sse, EventSse::events, SseEvent
ServiceExt::oneshotrouter.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 an Arc<T>, and state is keyed by type. Call with_state once per type and extract any subset in each handler; there is no FromRef.
  • HeaderMap is a tuple wrapper around http::HeaderMap, so destructure it.
  • Buffered body extractors reject bodies over 2 MiB by default, much like Axum's DefaultBodyLimit. Change it with router.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 ecosystemTako
CorsLayertako::plugins::cors::CorsBuilder (plugins)
CompressionLayertako::plugins::compression::CompressionBuilder (plugins)
DefaultBodyLimit, RequestBodyLimitLayerrouter.body_limit(bytes)
TimeoutLayerrouter.timeout(duration) or route.timeout(duration)
SetRequestIdLayertako::middleware::request_id::RequestId
tower-sessionstako::middleware::session::SessionMiddleware
tower_governortako::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

On this page