taut-rpc 0.1.0

End-to-end type-safe RPC between Rust (axum) and TypeScript clients.
Documentation

taut-rpc

crates.io npm docs.rs License: MIT OR Apache-2.0 CI

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.md for 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 cargo subcommand emits a single .ts file 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 Validate trait (auto-derived); the client mirrors them via Valibot or Zod schemas, also generated.

Install

cargo add taut-rpc taut-rpc-macros
cargo install taut-rpc-cli
npm i taut-rpc

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 taut_rpc::{rpc, Router, Validate};

#[derive(serde::Deserialize, taut_rpc::Type, taut_rpc::Validate)]
pub struct CreateUser {
    #[taut(length(min = 3, max = 32))] pub username: String,
    #[taut(email)]                     pub email: String,
}

#[derive(serde::Serialize, taut_rpc::Type)]
pub struct User { pub id: u64, pub username: String }

#[derive(serde::Serialize, taut_rpc::Type, thiserror::Error, Debug)]
pub enum ApiError {
    #[error("conflict")]   Conflict,
    #[error("validation")] Invalid(taut_rpc::ValidationError),
}

#[rpc(mutation)]
async fn create_user(input: CreateUser) -> Result<User, ApiError> { /* ... */ }

#[rpc(stream)]
async fn user_events() -> impl futures::Stream<Item = User> + 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.

cargo taut mcp --out target/taut/mcp.json
# or, dump straight from a built binary:
cargo taut mcp --from-binary target/debug/my-server --out -

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 queries
  • examples/phase2-auth/ — middleware + bearer auth
  • examples/phase2-tracing/ — tower-http TraceLayer
  • examples/phase3-counter/ — SSE subscriptions
  • examples/phase4-validate/ — input validation
  • examples/smoke/ — Phase 0 hand-written reference

License

Licensed under either of

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.