Skip to content

Repository files navigation

fnrpc

Type-safe RPC for Rust + TypeScript, with Axum and Tauri support.

Define your RPC functions once in Rust. TypeScript types are auto-generated. Call them from the frontend with full type safety.

Features

  • One source of truth — Rust functions drive TypeScript types via specta codegen
  • Dual runtime — Axum (HTTP) and Tauri (IPC) backends from the same router
  • Subscriptions — Server-sent events (Axum) or Channel<string> (Tauri), typed as AsyncIterable
  • Middleware — HookLayer before/after, TracingLayer, custom FnLayer
  • BigInt safe — Automatic BigInt envelope across all transports
  • TanStack Query@fnrpc/tanstack-query for query/mutation/stream/live utilities

Quick start

1. Define RPCs

use fnrpc::{rpc_query, rpc_mutate, rpc_subscribe, RpcErr};

// Single param, no context
#[rpc_query]
async fn health_check() -> String {
    "ok".into()
}

// Multi-param query with context
#[rpc_query]
async fn get_user(ctx: &Ctx, id: i64) -> Result<User, RpcErr> {
    // ctx.db.query(...)
    todo!()
}

// Mutation — structured input
#[derive(specta::Type, serde::Deserialize)]
struct CreateUserInput {
    name: String,
    email: String,
}

#[rpc_mutate]
async fn create_user(ctx: &Ctx, input: CreateUserInput) -> Result<User, RpcErr> {
    // INSERT INTO users (name, email) ...
    todo!()
}

// Subscription — sync fn returning a Stream
#[rpc_subscribe]
fn watch_user(ctx: &Ctx, id: i64) -> Pin<Box<dyn Stream<Item = Result<UserUpdate, RpcErr>> + Send + '_>> {
    // ...
}

// Subscription with POST — input sent in body instead of URL query params
#[rpc_subscribe("post")]
fn large_stream(ctx: &Ctx, input: LargeInput) -> Pin<Box<dyn Stream<Item = Result<Output, RpcErr>> + Send + '_>> {
    // ...
}

2. Build router

use fnrpc::router::RpcRouterBuilder;

let router = RpcRouterBuilder::<Ctx>::new()
    .query(health_check)
    .query(get_user)
    .mutate(create_user)
    .subscribe(watch_user)
    .layer(HookLayer::new()
        .before(|ctx, path, input| tracing::info!("{path} invoked")))
    .layer(TracingLayer)
    .build();

3. Serve with Axum

use std::sync::Arc;
use axum::Router;
use fnrpc_axum::{FnrpcState, handle};

Router::new()
    .route("/fnrpc/{*path}", axum::routing::get(handle::<Ctx>).post(handle::<Ctx>))
    .with_state(Arc::new(FnrpcState {
        router: Arc::new(router),
        ctx_from_headers: Arc::new(|headers| Ctx {
            db: db_pool.clone(),
            user_id: extract_user_id(&headers),
        }),
    }))
    .layer(cors);

4. Generate TypeScript types

// scripts/gen_fnrpc.rs
fn main() {
    let router = build_router();
    fnrpc::gen_ts_client::write_ts_client(
        &router,
        "http://localhost:3000/fnrpc",
        Path::new("../src/bindings.ts"),
    )
    .expect("codegen failed");
}

5. Call from TypeScript

import { createClient, fetchTransport, tauriTransport } from "@fnrpc/client";
import type { Procedures } from "./bindings";
import { __procedureMeta } from "./bindings";
import { isTauri } from "@tauri-apps/api/core";

const transport = (() => {
  try {
    if (isTauri()) {
      return tauriTransport(() => import("@tauri-apps/api/core"));
    }
  } catch {}
  return fetchTransport({ url: "http://localhost:19110/fnrpc" });
})();

export const fnrpc = createClient<Procedures>(transport, __procedureMeta);

Client API

Single param

const user = await fnrpc.get_user(42);
//        ^? User

Multi-param (tuple input)

Rust functions with multiple params accept a tuple in TypeScript:

const add = await fnrpc.add([1, 2]); // fn add(a: i32, b: i32)

For structured input, pass an object:

const created = await fnrpc.create_user({ name: "Alice", email: "alice@example.com" });

Subscription (AsyncIterable)

const stream = await fnrpc.watch_user(42);
for await (const update of stream) {
    console.log("user updated:", update);
}

For POST subscriptions (large inputs), the transport automatically sends the input in the request body.

Abort a subscription via AbortSignal:

const controller = new AbortController();
const stream = await fnrpc.watch_user(42, controller.signal);

setTimeout(() => controller.abort(), 5000);

Error handling

try {
    await fnrpc.get_user(999);
} catch (err) {
    if (isRpcError(err)) {
        // { code: "NOT_FOUND", message: "...", data: unknown }
    }
}

TanStack Query integration

Use @fnrpc/tanstack-query to create typed query/mutation/stream utilities for your RPCs.

Setup

import { createTanstackQueryUtils } from "@fnrpc/tanstack-query";

export const client = createTanstackQueryUtils(fnrpc);

React

import { useQuery, useMutation } from "@tanstack/react-query";

// Query
const { data: user } = useQuery(client.get_user.queryOptions(42));

// Mutation
const mutation = useMutation(client.create_user.mutationOptions());
mutation.mutate({ name: "Alice", email: "alice@example.com" });

// Streamed — accumulates chunks into an array
const { data: updates } = useQuery(client.watch_user.streamedOptions(42));

// Live — each chunk updates the cache in real time
const { data: lastUpdate } = useQuery(client.watch_user.liveOptions(42));

Solid

import { createQuery, createMutation } from "@tanstack/solid-query";

// Query
const query = createQuery(() => client.get_user.queryOptions(42));

// Mutation
const mutation = createMutation(() => client.create_user.mutationOptions());
mutation.mutate({ name: "Alice", email: "alice@example.com" });

// Streamed
const streamed = createQuery(() => client.watch_user.streamedOptions(42));

// Live
const live = createQuery(() => client.watch_user.liveOptions(42));

Packages

Package Description
fnrpc Rust core: macros, router, codegen, middleware, handler traits
fnrpc-axum Axum integration: FnrpcState, handle
fnrpc-macros Proc macros: #[rpc_query], #[rpc_mutate], #[rpc_subscribe]
@fnrpc/client TypeScript typed client, Proxy-based, fetch & Tauri transports
@fnrpc/tanstack-query TanStack Query utilities: queryOptions, mutationOptions, streamedQuery, liveQuery

Examples

Architecture

See SUMMARY.md for detailed architecture, proc macro reference, middleware docs, handler traits, and test index.

Roadmap

See Roadmap.md.

About

Rust functions power your API — TypeScript types generated automatically via specta. A single router serves both Axum (HTTP SSE) and Tauri (IPC). Frontend calls `client.my_rpc(input)` directly; subscriptions return AsyncIterable. TanStack Query integration built in.

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages