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 vespera integration
  • derive-driven spec generation via the utoipa integration

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
  • 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;

If you like it drop a ⭐ on Github or just open an issue if something you dont like.

Made by love! 🦀🦀