Tako v0.5.0 → v0.7.1-2: from "nice router" to "mini platform"
by Daniel Boros
Jan 14
4 min read
6 views
If you last touched Tako around v0.5.0, the jump to v0.7.1-2 feels like a proper “we shipped a bunch of foundational stuff” cycle: signals, a real plugin system with batteries-included plugins, OpenAPI generation + hosted docs UIs, a new runtime option, and a big internal refactor to make the core types cleaner and less coupled.
Also: this wasn’t a tiny bump. The compare range is 73 commits and 111 files changed. Github: v.0.5...v.0.7
1) Signals: your app gets a nervous system
The big new idea is: events are first-class.
You get:
- built-in lifecycle events (server started, connections opened/closed, request start/completed)
- route-level request signals
- custom signals you emit yourself
- multiple ways to consume them: callbacks, streams, prefix subscriptions
- even a typed “RPC-ish” mechanism when you want request/response inside-process
Example: listen to request completion (app-level)
use tako::signals::{app_events, ids, Signal};
fn init_signals() {
let arbiter = app_events();
// Callback-style handler
arbiter.on(ids::SERVER_STARTED, |signal: Signal| async move {
println!("server.started meta = {:?}", signal.metadata);
});
// Stream-style listener
let mut rx = arbiter.subscribe(ids::REQUEST_COMPLETED);
tokio::spawn(async move {
while let Ok(signal) = rx.recv().await {
let method = signal.metadata.get("method").cloned().unwrap_or_default();
let path = signal.metadata.get("path").cloned().unwrap_or_default();
let status = signal.metadata.get("status").cloned().unwrap_or_default();
println!("request.completed: {} {} -> {}", method, path, status);
}
});
}
Example: route-level custom signal (“routes.hit”)
use tako::extractors::state::State;
use tako::signals::{Signal, SignalArbiter};
use std::collections::HashMap;
async fn route_handler(State(bus): State<SignalArbiter>) -> impl tako::responder::Responder {
let mut meta: HashMap<String, String, tako::types::BuildHasher> =
HashMap::with_hasher(tako::types::BuildHasher::default());
meta.insert("path".to_string(), "/route".to_string());
bus.emit(Signal::with_metadata("routes.hit", meta)).await;
"ok".into_response()
}
fn init_route_signals(router: &mut tako::router::Router) {
let arbiter = router.signal_arbiter();
router.state(arbiter.clone());
router.on_signal("routes.hit", |signal: Signal| async move {
if let Some(path) = signal.metadata.get("path") {
println!("routes.hit for {}", path);
}
});
}
Example: typed RPC on the arbiter
use std::sync::Arc;
use tako::signals::SignalArbiter;
struct AddRequest { a: i32, b: i32 }
#[derive(Clone)]
struct AddResponse { sum: i32 }
async fn demo() {
let arbiter = SignalArbiter::new();
arbiter.register_rpc::<AddRequest, AddResponse, _, _>("rpc.add", |req: Arc<AddRequest>| async move {
AddResponse { sum: req.a + req.b }
});
let res = arbiter.call_rpc::<AddRequest, AddResponse>("rpc.add", AddRequest { a: 2, b: 40 }).await;
println!("result = {:?}", res.map(|r| r.sum));
}
2) Plugins: “ship features, not glue code”
There’s now a clean router-level vs route-level plugin story via a TakoPlugin trait. That unlocks reusable “modules” that can register middleware, hook signals, expose endpoints, etc.
And the project didn’t stop at infrastructure — it also shipped a solid starter pack:
- Compression (gzip / brotli / deflate, optional zstd, optional streaming)
- Rate limiting (token bucket style)
- Idempotency (header-keyed dedupe with TTL + replay protection)
- Metrics (via signals, with Prometheus and OpenTelemetry options)
- CORS improvements
- plus a handy Bearer auth middleware for simple token checks
Example: Prometheus metrics in one call (and it mounts /metrics)
use tako::plugins::metrics::PrometheusMetricsConfig;
let mut router = tako::router::Router::new();
// installs the backend + registers GET /metrics + stores registry in state
PrometheusMetricsConfig::default().install(&mut router);
Example: Idempotency for POSTs (safe retries without duplicates)
use tako::plugins::idempotency::{IdempotencyPlugin, Scope};
use tako::Method;
let plugin = IdempotencyPlugin::builder()
.methods(&[Method::POST])
.ttl_secs(60)
.scope(Scope::MethodAndPath)
.build();
router.plugin(plugin);
This one is especially “production-y”: it coalesces in-flight requests, caches responses, and can reject same-key/different-payload with a conflict.
Example: Rate limiting (simple “N per second”)
use tako::plugins::rate_limiter::RateLimiterBuilder;
router.plugin(
RateLimiterBuilder::new()
.requests_per_second(100)
.build()
);
3) OpenAPI: your docs ship with the API
Two “flavors” show up in this range:
- route discovery + spec generation via the
vesperaintegration - derive-driven spec generation via the
utoipaintegration
And then it gets nice: there are helpers to host docs UIs:
- Swagger UI
- Scalar
- RapiDoc
Example: generate /openapi.json and host /docs + /scalar
use tako::openapi::ui::{Scalar, SwaggerUi};
use tako::openapi::vespera::{generate_openapi_from_routes, Info, VesperaOpenApiJson};
let mut router = tako::router::Router::new();
setup_routes(&mut router);
let info = Info {
title: "Tako Example API".to_string(),
version: "1.0.0".to_string(),
description: Some("Sample API demonstrating OpenAPI integration".to_string()),
terms_of_service: None,
contact: None,
license: None,
summary: None,
};
let spec = generate_openapi_from_routes(&router, info);
router.route(tako::Method::GET, "/openapi.json", move |_: tako::types::Request| {
let spec = spec.clone();
async move { VesperaOpenApiJson(spec) }
});
router.route(tako::Method::GET, "/docs", |_: tako::types::Request| async {
SwaggerUi::new("/openapi.json").title("API - Swagger UI")
});
router.route(tako::Method::GET, "/scalar", |_: tako::types::Request| async {
Scalar::new("/openapi.json").title("API - Scalar")
});
4) Performance + ergonomics upgrades (the “less pain” bucket)
A few highlights that matter day-to-day:
- Core HTTP types got refactored so the request/response aliases are cleaner and less tied to a specific underlying stack.
- Faster defaults / options like:
- a feature-gated faster hasher (
ahash) - newer concurrent map usage (
scc) and an upgrade/migration around it
- a feature-gated faster hasher (
- Faster JSON options and zero-copy-ish extraction patterns:
- a borrowed JSON extractor that caches raw body bytes so the parsed value can borrow from it
- SIMD-accelerated JSON extractors (plus a new parser option added in this range)
Example: borrowed JSON extraction
use tako::zero_copy_extractors::json::JsonBorrowed;
async fn handler(JsonBorrowed(payload, _): JsonBorrowed<'_, MyType>) -> impl tako::responder::Responder {
// payload can borrow from the cached request body bytes
tako::responder::Json(payload)
}
5) Small but lovable DX wins
These are the kind of things you don’t brag about, but you feel them:
- Interactive port fallback helper for local dev (bind to a port, and if it’s taken, it offers the next one instead of just dying).
- Lots of tightened middleware + plugin initialization behavior (global and route-level).
- Extra examples (signals, OpenAPI, runtime variants) so people can copy/paste their way to success.
let listener = tako::bind_with_port_fallback("127.0.0.1:8080").await?;
tako::serve(listener, router).await;
- Github: https://github.com/rust-dd/tako
- Crates.io: https://crates.io/crates/tako-rs
- Docs.rs: https://docs.rs/tako-rs/0.5.1/tako/
If you like it drop a ⭐ on Github or just open an issue if something you dont like.
Made by love! 🦀🦀