# taut-rpc
[](https://crates.io/crates/taut-rpc)
[](https://www.npmjs.com/package/taut-rpc)
[](https://docs.rs/taut-rpc)
[](#license)
[](https://github.com/nktkt/taut-rpc/actions/workflows/ci.yml)
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`](./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](https://github.com/tokio-rs/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](https://valibot.dev) or [Zod](https://zod.dev) schemas, also generated.
## Install
```sh
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
| 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) | partial (via `specta`; less ergonomic) | n/a (Tauri-only IPC) | n/a (manual) |
## 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`](https://github.com/hyperium/tonic).
- **Schema-first workflows.** Rust types are the source of truth.
## Quick taste
```rust
// 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 { /* ... */ }
```
```ts
// 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](https://modelcontextprotocol.io/) `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.
```sh
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/`](./docs/concepts/) — IR model, transport, validation bridge, error semantics
- **Guides:** [`docs/guides/`](./docs/guides/) — getting started, axum integration, custom schema dialects, subscriptions
- **SPEC:** [`SPEC.md`](./SPEC.md) — wire format and codegen contract
- **Examples:** [`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
- Apache License, Version 2.0 ([LICENSE-APACHE](./LICENSE-APACHE) or <https://www.apache.org/licenses/LICENSE-2.0>)
- MIT license ([LICENSE-MIT](./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.