taut-rpc
End-to-end type-safe RPC between Rust servers and TypeScript clients.
Status: v0.1.0 release candidate (Phases 0–5 landed: workspace scaffold, end-to-end pipeline, error model, subscriptions, validation bridge, release polish). See
ROADMAP.mdfor what comes next.
Why
If you write a Rust backend and a TypeScript frontend, you currently glue them together by hand: define a Rust handler, write an OpenAPI schema (or ts-rs-generated types), then wire the client. The types drift, the runtime validation is yours to write, and refactors break silently.
taut-rpc aims to make the wire as taut as the function call: change a Rust signature, get a TypeScript compile error.
Approach
- Server side: an attribute macro (
#[rpc]) on a plain Rust function or trait registers it into a router that lives on top of axum. - Client side: a
cargosubcommand emits a single.tsfile containing a fully typed client — no runtime reflection, no schema fetch. - Wire format: JSON over HTTP for queries/mutations, SSE for subscriptions. WebSocket is opt-in.
- Validation: types implement a
Validatetrait (auto-derived); the client mirrors them via Valibot or Zod schemas, also generated.
Install
The Rust crates power the server and derive macros; the CLI emits the TypeScript client; the npm package supplies the runtime helpers the generated client imports.
Comparison
taut-rpc |
rspc |
taurpc |
ts-rs + axum |
|
|---|---|---|---|---|
| Transport | axum (HTTP/SSE/WS) | router-agnostic | Tauri IPC only | manual |
| Codegen | cargo taut gen |
runtime-driven | macro-time | manual |
| Status | v0.1.0 | stalled | active (Tauri-only) | low-level |
| Subscriptions | first-class | yes | yes | n/a |
| Validation bridge | yes (Valibot default, Zod opt-in, custom)[^vbridge] | partial (via specta; less ergonomic) |
n/a (Tauri-only IPC) | n/a (manual) |
[^vbridge]: Constraints flow Rust → IR → TS schemas; server-side enforcement is automatic (the #[derive(Validate)] macro wires input validation into every #[rpc] handler).
Non-goals
- Cross-language servers. This is Rust↔TS. Adding Go, Python, etc. would force a lowest-common-denominator type system and that defeats the point.
- gRPC compatibility. If you need gRPC, use
tonic. - Schema-first workflows. Rust types are the source of truth.
Quick taste
// server: src/api.rs
use ;
async
async + Send + 'static
// client: generated by `cargo taut gen`
import { createApi, procedureSchemas } from "./api.gen";
const client = createApi({ url: "/rpc", schemas: procedureSchemas });
try {
const u = await client.create_user({ username: "alice", email: "a@b.c" });
for await (const e of client.user_events.subscribe()) console.log(e.id);
} catch (err) {
if (err.kind === "Invalid") console.warn(err.fieldErrors);
}
Validation runs both ways: the client checks before sending, the server checks before dispatch, and both sides share the same constraint source.
Agent tooling
cargo taut mcp emits a Model Context Protocol tools/list manifest from the same IR that drives the TypeScript client. Each query/mutation procedure becomes an MCP tool whose inputSchema is JSON Schema (Draft 2020-12), with reachable named types inlined as $defs and rustdoc surfaced as description. Drop the resulting mcp.json into any MCP-aware agent harness to expose your taut-rpc service as a callable toolset — no hand-written schemas.
# or, dump straight from a built binary:
Documentation
- Concepts:
docs/concepts/— IR model, transport, validation bridge, error semantics - Guides:
docs/guides/— getting started, axum integration, custom schema dialects, subscriptions - SPEC:
SPEC.md— wire format and codegen contract - Examples:
examples/— runnable phase-by-phase demos
The repo ships these examples:
examples/phase1/— basic queriesexamples/phase2-auth/— middleware + bearer authexamples/phase2-tracing/— tower-http TraceLayerexamples/phase3-counter/— SSE subscriptionsexamples/phase4-validate/— input validationexamples/smoke/— Phase 0 hand-written reference
License
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or https://opensource.org/licenses/MIT)
at your option.
Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.