Specta LogoSpecta
1.0.0-rc.x (alpha)

Quickstart

Run the experimental procedure API with Axum and a TypeScript client.

rspc is no longer maintained. This guide describes the procedure-based rewrite in the source snapshot, not the older v1.0.0-rc tags. Use a fixed revision for this API. Do not install the latest Specta alongside it.

Create a project

cargo new rspc-example
cd rspc-example
cargo add tokio --features macros,rt-multi-thread,net
cargo add axum@0.8
cargo add serde --features derive
cargo add specta@=2.0.0-rc.22 --features derive

Add the core and adapter from the same revision:

[dependencies.rspc]
git = "https://github.com/specta-rs/rspc"
rev = "30402a42a6a523e0a76d23f3b7ce80903386a98d"
features = ["typescript"]

[dependencies.rspc-axum]
git = "https://github.com/specta-rs/rspc"
rev = "30402a42a6a523e0a76d23f3b7ce80903386a98d"

Keep the default legacy feature enabled. It exports the ProceduresLegacy type used by the tuple-based JavaScript client below.

Define and expose a router

use rspc::{Procedure, Router};
use serde::Serialize;
use specta::Type;

#[derive(Debug, Serialize, Type)]
enum ApiError {
    InvalidName,
}

impl rspc::Error for ApiError {
    fn into_procedure_error(self) -> rspc::ProcedureError {
        rspc::ResolverError::new(self, None::<std::io::Error>).into()
    }
}

fn router() -> Router<()> {
    Router::new()
        .procedure("version", {
            Procedure::builder::<ApiError>()
                .query(|_: (), _: ()| async { Ok(env!("CARGO_PKG_VERSION")) })
        })
        .procedure("greet", {
            Procedure::builder::<ApiError>()
                .mutation(|_: (), name: String| async move {
                    if name.trim().is_empty() {
                        return Err(ApiError::InvalidName);
                    }
                    Ok(format!("Hello, {name}!"))
                })
        })
}

#[tokio::main]
async fn main() {
    let (procedures, types) = router().build().expect("invalid router");

    rspc::Typescript::default()
        .export_to("./bindings.ts", &types)
        .expect("failed to export bindings");

    let app = axum::Router::new()
        .nest("/rspc", rspc_axum::endpoint(procedures, || ()));

    let listener = tokio::net::TcpListener::bind("127.0.0.1:4000")
        .await
        .expect("failed to bind server");
    axum::serve(listener, app).await.expect("server failed");
}

#[cfg(test)]
mod tests {
    #[test]
    fn router_builds() {
        super::router().build().expect("invalid router");
    }
}

Run cargo run. The server writes bindings.ts and listens on port 4000. A query reads data. A mutation performs an action that may change data.

Call it from TypeScript

npm install @rspc/client@0.3.1

Copy or export bindings.ts into your frontend source directory:

import { createClient, FetchTransport } from "@rspc/client";
import type { ProceduresLegacy } from "./bindings";

export const client = createClient<ProceduresLegacy>({
  transport: new FetchTransport("/rspc"),
});

console.log(await client.query(["version", null]));
console.log(await client.mutation(["greet", "Ada"]));

This example uses a same-origin /rspc endpoint. Configure your frontend development server to proxy that path to http://127.0.0.1:4000. For cross-origin requests, configure CORS on the server and use its full URL instead.

This rewrite exports Rust's () input as null. Pass null as the second tuple item for those procedures. The released 0.3.0 exporter uses never instead, so its no-input calls have a different tuple shape.

Add a custom payload

Inputs need Deserialize and Type. Outputs need Serialize and Type:

#[derive(serde::Deserialize, Type)]
struct GreetingInput {
    name: String,
}

#[derive(Serialize, Type)]
struct Greeting {
    message: String,
}

Use these types in a resolver, attach it with .procedure(...), and regenerate bindings. See Procedures for errors, context, streams, and shared middleware.

Other integrations

  • Axum covers request context and WebSockets.
  • Tauri explains the IPC adapter and client compatibility.
  • TanStack Query covers frontend query caching.
  • The Rust client is a prototype, not a supported alternative to the TypeScript client.
Edit on GitHub

Last updated on

On this page