# specta-rs With specta-rs we are setting out to build an ecosystem of crates which enable developers to build better web apps with Rust. We are doing this by building libraries with a focus on: **Typesafety**: types should always be inferred end-to-end, allowing you to move quickly and collaborate while maintaining confidence that your code is correct. **Developer experience**: obsessed with simplicity, while maintaining flexibility. We build tools that make doing the *"right thing"* the *"easy thing"*, freeing you up to focus on the things that matter. **Modern practices**: embrace the world of modern JavaScript tooling, enabling you to ship better user experiences than server-driven approaches. **Scale**: build codebases that scale sanely, both in performance and maintainability. Take advantage of Rust's compile-time checks and tooling to reduce risk when developing large-scale applications. ## Projects [#projects] Currently specta-rs contains the following projects:
## Why Choose Rust? [#why-choose-rust] We are focused on building tooling in Rust but it is not always the right choice for your project. Rust has a lot of advantages such as it's **strong type system**, **tooling**, **high-performance**, and **memory safety** which leads to fewer runtime bugs and makes refactoring and maintaining code easier. The right choice often depends on your situation: * Server-side rendered UI frameworks often necessitate part or most of your backend to be in Javascript. * Libraries like [Effect](https://effect.website) can bring Rust-like benefits without changing languages. * Go has a lower learning curve and great standard library but has weaker error handling and type-checking guarantees resulting in more unreliable codebases. Rust is the right choice if: * Your team has Rust expertise * Your building a microservice, your frontend is an SPA or your embedding your API into a native application (Eg. [Tauri](https://tauri.app)) * Your API needs to do heavy data processing, simulations or work where performance is critical. * Your API needs to do processing where reliability and security are critical (Eg. banking, healthcare) # Alpha (1.0.0-rc.x) The alpha releases are not stable and are not recommended for production use yet. I would also not rely on this documentation being up to date as things are still changing. ## Should you use the alpha? [#should-you-use-the-alpha] Probally not right now. The high-level API's are fairly stable but documentation is still lacking. # Quickstart {/* import { Interpolate, IfFramework } from "@/components/Switchers"; import vscodeIntegrationImg from "@/images/vscode-integration.png"; import Image from "next/image"; */} # Quickstart [#quickstart] ## CLI [#cli] Coming soon... {/* // TODO: Add create rspc-app support here */} ## Manual setup [#manual-setup] Get rspc up and running in your own project. {/* ### Create new project (optional) If you haven't got a Rust project already setup, create a new one using the following command. ```bash cargo new cd cargo add tokio --features full # rpsc requires an async runtime ``` ### Install rspc `rspc` is distributed through a Rust crate hosted on [crates.io](https://crates.io/crates/rspc). Add it to your project using the following command: ```bash cargo add rspc specta ``` This command will not exist if your running a Rust version earlier than `1.62.0`, please upgrade your Rust version if this is the case. ### Create a router Go into `src/main.rs` and add the following code: ```rs copy filename="src/main.rs" use rspc::{Rspc, Router}; const R: Rspc<()> = Rspc::new(); fn router() -> Router<()> { R.router() // TODO: Set ts export path using config .procedure("version", R.query(|ctx, _: ()| env!("CARGO_PKG_VERSION"))) .compat() } #[tokio::main] async fn main() { let router = router(); // TODO: Mount an integration to expose your API } #[cfg(test)] mod tests { // It is highly recommended to unit test your rspc router by creating it // This will ensure it doesn't have any issues and also export updated Typescript types. #[test] fn test_rspc_router() { super::router(); } } ``` ### Exposing your router Now that you have a router your probably wondering how you access it from your frontend. This is done through an rspc integration. I would recommend starting with [Axum](https://github.com/tokio-rs/axum), by following [this](/integrations/axum). ### Usage on the frontend Install the frontend package using the following command: ```bash pnpm install @rspc/client @rspc/react ``` ```tsx copy filename="src/MyComponent.tsx" // TODO: Finish example -> show imports function SomeComponent() { const version = rspc.useQuery(["version"]); return ( <>

{version.data}

); } ```
```tsx copy filename="src/MyComponent.tsx" // TODO: Finish example -> show imports function SomeComponent() { const version = rspc.useQuery(() => ["version"]); return ( <>

{version.data}

); } ```
### (Optional) Setup your editor If you are using [Visual Studio Code](https://code.visualstudio.com) you can optionally install the [rspc extension](https://marketplace.visualstudio.com/items?itemName=oscartbeaumont.rspc-vscode) for useful code shortcuts.
rspc VSCode integration
*/} # Advanced ### React Native support [#react-native-support] Docs coming soon! ### Implementing with a custom server [#implementing-with-a-custom-server] Docs coming soon! ### Implementing a link [#implementing-a-link] Docs coming soon! ### Invalidate query [#invalidate-query] 🚧 WIP - [Tracking issue #19](https://github.com/oscartbeaumont/rspc/issues/19) # Basics Getting started with rspc on the server. This guide will take you through all the parts of rspc and explain how they work. ### Router [#router] First start by creating a new router with the default context type (we will touch on this later). A router is a collection of procedures which is a similar to a REST endpoint Eg. `/api/users`. ```rust fn router() -> Router { let router = ::new(); } ``` Next you will want to export the router's bindings to Typescript so the frontend code can use them. rspc's typesafe works by converting your Rust code into a Typescript declaration file. ```rust use rspc::{Rspc, Router}; const R: Rspc<()> = Rspc::new(); fn router() -> Router { let router = R.router().build().unwrap(); #[cfg(debug_assertions)] // Only export in development builds router .export_ts(ExportConfig::new( PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("./bindings.ts"), )) .unwrap(); router } fn main() { let router = router(); } #[cfg(test)] mod test { // I recommend doing this as rspc's router can fail to build and this ensures it will be caught by the tests #[test] fn export_bindings() { super::router(); } } ``` ### Transports [#transports] Now that you have a basic router you will want to expose it to the outside world. rspc provides are multiple methods for doing this but I recommend using [Axum](https://github.com/tokio-rs/axum). ```rs ``` ### Context [#context] A router is cool and all but your application has state such as a database connection you will want to be able to access from your procedures. ```rust use my_database_library::DatabaseConn; #[derive(Clone)] // Clone is generally required struct MyCtx { db: DatabaseConn } fn main() { let router = Router::::new(); } ``` The context type must be an immutable reference (`&T`). If your type doesn't satisfy this property you will want to wrap your data (`T`) in an [`Arc`](https://doc.rust-lang.org/std/sync/struct.Arc.html) or an [`Arc>`](https://doc.rust-lang.org/std/sync/struct.Mutex.html) depending on if your require mutability (this pattern is known as [interior mutability](https://doc.rust-lang.org/book/ch15-05-interior-mutability.html)). #### Request context [#request-context] rspc deal with context differently than you might expect if your coming from other popular Rust libraries. A new context is created for every incoming request or websocket connection. This may seem weird at first but it allows your context to include both system data such as a database connection and user data such as the users session. ### Procedure [#procedure] A procedure represents a single operation on the server. Thing of this as a regular REST endpoint Eg. `/users`. It can take in an argument and return a result. #### Queries [#queries] A query is a request for data. It's important it has no [side-effects](https://en.wikipedia.org/wiki/Side_effect_\(computer_science\)) as it's possible for a query to be retried. When using the React or Solid integrations the data will always be refetched periodically. ```rs use serde::Deserialize; // This requires the 'derive' feature to be enabled. use specta::Type; #[derive(Deserialize, Type)] pub struct MyCustomType { } # TODO ``` #### Mutations [#mutations] TODO #### Subscriptions [#subscriptions] {/* TODO: Must be enabled the Rust features */} TODO ### Merging routers [#merging-routers] Ok now your starting to build your app but your finding that your file is getting a bit big. It might be time to split up your procedures across multiple routers. ```rs # TODO ``` ### Advanced Procedures [#advanced-procedures] #### Custom Types [#custom-types] ```rs # TODO ``` #### Error handling [#error-handling] Now we all think our code is perfect, ```rs # TODO ``` #### Custom error types [#custom-error-types] Look, rspc's error type is cool but what if i'm using my own. ```rs # TODO ``` ### Middleware [#middleware] This is all cool but i'm building a real application. I need to be able to do authentication, authorization, logging and more! This is where the rspc's powerful middleware system comes in. Docs coming soon as the syntax is undergoing breaking changes!
If your interesting in using them jump in the Discord!
### Footguns [#footguns] #### Capturing context [#capturing-context] You should NOT capture variable into your handler function and instead use the request context. Their are exceptions to this rule but you should ideally be able to build the router without a connection to any external resources such as your database. ```rs # TODO ``` # Plugins Coming soon... WIP Plugins: * OpenAPI - [issue #29](https://github.com/oscartbeaumont/rspc/issues/29) * Playground - [issue #23](https://github.com/oscartbeaumont/rspc/issues/23) * Authentication library # Request * How to use request from procedures * Cookie APIs * Setting response HTTP headers * How to access the HTTP headers on the frontend # Selection **If you are using [Prisma Client Rust](https://prisma.brendonovich.dev) with rspc generally use [select & include](https://prisma.brendonovich.dev/reading-data/select-include) instead of this.** It is very common when building an API to fetch some data from the database but you only want to expose a subset of the data to the client. With rspc you can use the `selection!` macro to easily return a subset of fields on a struct. For example say you have a `User` struct like the following: ```rs copy filename="src/main.rs" pub struct User { pub id: i32, pub name: String, pub email: String, pub age: i32, pub password: String, } ``` If your database returns a `User` struct you are unable to return it directly from your procedure as that would leak the value in the `password` field. Traditionally you would have to create a second struct without the `password` field, however this isn't optimal as it adds unnecessary boilerplate to your project. Instead you can use the `selection!` macro like below to select only certain fields from the struct. ```rs copy filename="src/main.rs" let router = ::new() .query("me", |t| { t(|_, _: ()| { // This struct would be returned from your database! let user = User { id: 1, name: "Monty Beaumont".into(), email: "monty@otbeaumont.me".into(), age: 7, password: "password123".into(), }; selection!(user, { name, age }) // We select only the name and age fields to return }) }) .query("users", |t| { t(|_, _: ()| { let user = User { name: "Monty Beaumont".into(), email: "monty@otbeaumont.me".into(), age: 7, password: "password123".into(), }; // We have a vector of data which contains information but we only want to return some of it the user. // Eg. We don't want to expose the password field. let users = vec![user.clone(), user.clone(), user]; // Here we are selecting the fields we want to expose on each item in the list. This is completely type safe! // The square brackets around the selection dictate that the selection should be applied to each item in the list. selection!(users, [{ name, age }]) }) }) .build(); ``` # Vanilla Client The vanilla client allows you to consume your API on the frontend. This client is the minimal core and it is recommended that you use the [React](/client/react) or [Solid](/client/solid) integration for building application. To get started first install the minimal runtime package. ```bash copy npm i @rspc/client ``` Next you need to export the Typescript bindings from your `rspc::Router` by using either [export\_ts\_bindings](/server/router#exporting-the-typescript-bindings) or [export\_ts](/server/router#exporting-the-typescript-bindings). ```rs /export_ts_bindings/ copy filename="src/main.rs" let router = ::new() // This will automatically export the bindings to the `./ts` directory when you run build() in a non-release Rust build .config(Config::new().export_ts_bindings("./bindings.rs")) .build(); ``` Then you can use the `@rspc/client` package to consume your API. ```ts copy filename="index.ts" import { createClient, FetchTransport } from "@rspc/client"; import type { Procedures } from "./ts/index"; // These were the bindings exported from your Rust code! // You must provide the generated types as a generic and create a transport (in this example we are using HTTP Fetch) so that the client knows how to communicate with your API. const client = createClient({ // Refer to the integration your using for the correct transport. transport: new FetchTransport("http://localhost:4000/rspc"), }); // Now use the client in your code! const version = await client.query(["version"]); // The types will be inferred from your backend. const userOne = await client.query(["getUser", 1]); const userTwo = await client.mutation(["addUser", { name: "Monty Beaumont" }]); ``` # Transports [#transports] rspc has multiple different transports which can be used. These dictate how the frontend is able to talk with the backend. ## Fetch Transport [#fetch-transport] Fetch transport does not support subscriptions! Transport build on the standard [Fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) API. This uses HTTP GET and POST requests under the hood. rspc will generally use a GET request for queries and POST request for mutations however this **isn't guaranteed**. ```ts /FetchTransport/ copy filename="index.ts" import { createClient, FetchTransport } from "@rspc/client"; import type { Procedures } from "./bindings.ts"; // The bindings exported from your Rust code! const client = createClient({ transport: new FetchTransport("http://localhost:4000/rspc"), }); ``` ### Custom Fetch implementation [#custom-fetch-implementation] ```ts copy filename="index.ts" import { createClient, FetchTransport } from "@rspc/client"; import type { Procedures } from "./bindings.ts"; // The bindings exported from your Rust code! const client = createClient({ transport: new FetchTransport( "http://localhost:4000/rspc", // Include Cookies for cross-origin requests (input, init) => fetch(input, { ...init, credentials: "include" }), ), }); ``` ### Fetch Authentication [#fetch-authentication] Guide coming soon... ## Websocket Transport [#websocket-transport] Transport build on the standard [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) API. This uses HTTP GET and POST requests under the hood. ```ts /WebsocketTransport/ copy filename="index.ts" import { createClient, WebsocketTransport } from "@rspc/client"; import type { Procedures } from "./bindings.ts"; // The bindings exported from your Rust code! const client = createClient({ transport: new WebsocketTransport("ws://localhost:8080/rspc/ws"), }); ``` ### Websocket Authentication [#websocket-authentication] Guide coming soon... # React Client rspc can be used on the frontend with [React](https://reactjs.org) via the powerful [React Query](https://tanstack.com/query/v4) library which provides caching, refetching and a lot more. To get started first install the required packages. ```bash copy pnpm i @rspc/client # The core client pnpm i @rspc/react # The React Query integration ``` Then you can do the following: ```tsx copy filename="index.ts" import { QueryClient } from "@tanstack/react-query"; import { FetchTransport, createClient } from "@rspc/client"; import { createReactQueryHooks } from "@rspc/react"; import type { Procedures } from "./ts/index"; // These were the bindings exported from your Rust code! // You must provide the generated types as a generic and create a transport (in this example we are using HTTP Fetch) so that the client knows how to communicate with your API. const client = createClient({ // Refer to the integration your using for the correct transport. transport: new FetchTransport("http://localhost:4000/rspc"), }); const queryClient = new QueryClient(); const rspc = createReactQueryHooks(); function SomeComponent() { const { data, isLoading, error } = rspc.useQuery(["version"]); const { mutate } = rspc.useMutation("updateVersion"); return ( <>

{data}

); } function App() { return ( ); } ``` # SolidJS Client rspc can be used on the frontend with [SolidJS](https://www.solidjs.com/) via [Tanstack Solid Query](https://tanstack.com/query/v4/docs/adapters/solid-query) which provides caching, refetching and a lot more. To get started first install the required packages. ```bash copy pnpm i @rspc/client # The core client pnpm i @rspc/solid # The SolidJS integration ``` Then you can do the following: ```tsx copy filename="index.ts" import { QueryClient } from "@tanstack/solid-query"; import { FetchTransport, createClient } from "@rspc/client"; import { createSolidQueryHooks } from "@rspc/solid"; import type { Procedures } from "./ts/index"; // These were the bindings exported from your Rust code! // You must provide the generated types as a generic and create a transport (in this example we are using HTTP Fetch) so that the client knows how to communicate with your API. const client = createClient({ // Refer to the integration your using for the correct transport. transport: new FetchTransport("http://localhost:4000/rspc"), }); const queryClient = new QueryClient(); const rspc = createSolidQueryHooks(); function SomeComponent() { const echo = rspc.createQuery(() => ({ queryKey: ["echo", "somevalue"], })); const sendMsg = rspc.createMutation(() => ({ mutationKey: "sendMsg", })); return ( <>

{echo.data}

); } function App() { return ( ); } ``` # Svelte Client rspc can be used on the frontend with [Svelte](https://svelte.dev) via [Tanstack Svelte Query](https://tanstack.com/query/latest/docs/framework/svelte/overview) which provides caching, refetching and a lot more. To get started first install the required packages. ```bash copy pnpm i @rspc/client # The core client pnpm i @rspc/svelte-query # The integration ``` Then you can do the following: ```svelte copy filename="index.svelte"

Using rspc version: {$version.data}

``` # Deployment ## Coming soon... [#coming-soon] We will document how to deploy an rspc API to: * [Vercel Functions](https://vercel.com/docs/concepts/functions) - Tracked in issue [#9](https://github.com/oscartbeaumont/rspc/issues/9) * [Netlify Functions](https://www.netlify.com/products/functions/) * [Fly.io](https://fly.io) * [Docker Container](https://www.docker.com/) # Overview # rspc [#rspc] rspc is a typesafe router allowing you to build end-to-end type safe APIs. It lets you define your backend logic in a Rust function and use the Typescript client to call it. Your rspc router is transport agnostic which means you can serve it from any HTTP server of your choice such as [Axum](https://github.com/tokio-rs/axum) or even from [Tauri](https://tauri.app). ## Features [#features] * ✅ **Typesafety** - allows your team to move faster and **eliminate** a whole class of common bugs * ✅ **Developer experience** - you **define a function** in Rust and can **call it from the frontend** with no extra effort * ✅ **Minimal runtime** - small runtime footprint so your can get the **full potential of Rust's speed** * ✅ **Middleware** - For easily extending your procedures with **auth, logging and more** ### Production users [#production-users] * [Spacedrive](https://spacedrive.com) * [CrabNebula Cloud](https://crabnebula.dev/cloud) * [Twidge](https://twidge.app) * [Reader](https://reader.place) # Axum Integration rspc has a built-in integration with [Axum](https://github.com/tokio-rs/axum) so that you can expose your API over HTTP. ### Enable feature [#enable-feature] You must install the [`rspc_axum`](https://docs.rs/rspc-axum/latest/rspc_axum/) crate to use Axum with rspc. ```toml /rspc_axum = { version = "0.0.0", features = ["ws"] }/ copy filename="Cargo.toml" [dependencies] rspc = "0.0.0" rspc_axum = { version = "0.0.0", features = ["ws"] } axum = "0.7.0" ``` ### Usage [#usage] ```rs copy filename="src/main.rs" let router = rspc::Router::<()>::new() .query("version", |_, _: ()| "1.0.0") .build() .arced(); let app = axum::Router::new() .route("/", get(|| async { "Hello 'rspc'!" })) .nest("/rspc", rspc_axum::endpoint(router, || ())) .layer(cors); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); axum::serve(listener, app).await.unwrap(); ``` {/* TODO: Bring this back */} {/* ### Extracting Context from Request **Warning: The Axum extractor API is probally going to be removed in a future release. If you are using this API, I would appreciate a message in the Discord about your usecase so I can ensure the replacement API can do everything you need.** **Warning: Current we only support a single extractor. This is a temporary limitation so open a GitHub Issue if you need more.** You may want to use Axum extractors to get data from the request such as cookies and put them on the request context. The `axum_handler` function takes a closure that can take up to 16 valid Axum extractors as arguments and then returns the [request context](/server/request-context) (of type `TCtx`). ```rs copy filename="src/main.rs" let router = rspc::Router::::new() .query("currentPath", |ctx, _: ()| ctx) .build() .arced(); let app = axum::Router::new() .route("/", get(|| async { "Hello 'rspc'!" })) // We use Axum `Path` extractor. The `rspc::Router` has `TCtx` set to `String` so we return the path string as the context. .nest("/rspc", rspc_axum::endpoint(|req: Request| req.uri().path())) .layer(cors); ``` */} ### Usage on frontend [#usage-on-frontend] ```ts copy filename="index.ts" import { FetchTransport, WebsocketTransport, createClient } from "@rspc/client"; import type { Procedures } from "./ts/bindings"; // These were the bindings exported from your Rust code! // For fetch transport const client = createClient({ transport: new FetchTransport("http://localhost:4000/rspc"), }); // For websocket transport - Required for subscriptions const client = createClient({ transport: new WebsocketTransport("ws://localhost:8080/rspc/ws"), }); client.query(["version"]).then((data) => console.log(data)); ``` # Tauri Integration rspc has a built-in integration with [Tauri](https://tauri.app/) so that you can expose your API to your frontend code using Tauri's IPC. ### Enable feature [#enable-feature] For the integration to work you must enable the `tauri` feature of rspc. Ensure the rspc line in your `Cargo.toml` file looks like the following: ```toml /rspc_tauri = "0.0.0"/ copy filename="Cargo.toml" [dependencies] rspc = "0.0.0" rspc-tauri = "0.0.0" ``` Read more about Rust features [here](https://doc.rust-lang.org/cargo/reference/features.html#dependency-features) ### Usage [#usage] Then expose your router using the Tauri plugin. ```rs copy filename="src/main.rs" let router = ::new().build(); tauri::Builder::default() .plugin(rspc_tauri::plugin(router.arced(), |app_handle| ())) ``` ### Usage on frontend [#usage-on-frontend] ```ts copy filename="index.ts" import { createClient } from "@rspc/client"; import { TauriTransport } from "@rspc/tauri"; import type { Procedures } from "./ts/bindings"; // These were the bindings exported from your Rust code! const client = createClient({ transport: new TauriTransport(), }); client.query(["version"]).then((data) => console.log(data)); ``` You can use the `client` by itself or integrate with the [Tanstack Query](/client/tanstack-query) hooks. # Quickstart {/* import { Interpolate, IfFramework } from "../components/Switchers"; import vscodeIntegrationImg from "../images/vscode-integration.png"; import Image from "next/image"; */} ## CLI [#cli] Coming soon... {/* // TODO: Add create rspc-app support here */} ## Manual setup [#manual-setup] Get rspc up and running in your own project. {/* ### Create new project (optional) If you haven't got a Rust project already setup, create a new one using the following command. ```bash cargo new cd cargo add tokio --features full # rpsc requires an async runtime ``` ### Install rspc `rspc` is distributed through a Rust crate hosted on [crates.io](https://crates.io/crates/rspc). Add it to your project using the following command: ```bash cargo add rspc specta ``` This command will not exist if your running a Rust version earlier than `1.62.0`, please upgrade your Rust version if this is the case. ### Create a router Go into `src/main.rs` and add the following code: ```rs copy filename="src/main.rs" use rspc::Router; fn router() -> Router<()> { ::new() .query("version", |t| t(|ctx, input: ()| env!("CARGO_PKG_VERSION"))) .build() } #[tokio::main] async fn main() { let router = router(); // TODO: Mount an integration to expose your API } #[cfg(test)] mod tests { // It is highly recommended to unit test your rspc router by creating it // This will ensure it doesn't have any issues and also export updated Typescript types. #[test] fn test_rspc_router() { super::router(); } } ``` ### Exposing your router Now that you have a router your probably wondering how you access it from your frontend. This is done through an rspc integration. I would recommend starting with [Axum](https://github.com/tokio-rs/axum), by following [this](/integrations/axum). ### Usage on the frontend Refer to the [Vanilla](/client), [React](/client/react) or [Solid](/client/solid) documentation for how to use the rspc client in your frontend. Install the frontend package using the following command: ```bash pnpm install @rspc/client @rspc/react ``` ```tsx copy filename="src/MyComponent.tsx" // TODO: Finish example -> show imports function SomeComponent() { const version = rspc.useQuery(["version"]); return ( <>

{version.data}

); } ```
```tsx copy filename="src/MyComponent.tsx" // TODO: Finish example -> show imports function SomeComponent() { const version = rspc.createQuery(() => ({ queryKey: ["version"], })); return ( <>

{version.data}

); } ```
### (Optional) Setup your editor If you are using [Visual Studio Code](https://code.visualstudio.com) you can optionally install the [rspc extension](https://marketplace.visualstudio.com/items?itemName=oscartbeaumont.rspc-vscode) for useful code shortcuts.
rspc VSCode integration
*/} # Common Errors rspc uses traits to allow for any nearly any type to be returned from your procedures, however this can make the error messages hard to understand so some guidance is provided here. #### the trait `IntoLayerResult<_>` is not implemented for type [#the-trait-intolayerresult_-is-not-implemented-for-type] This error means the type which you reaturned from your procedure is not valid. This is probably because it doesn't implement the traits: * [`serde::Serialize`](https://docs.rs/serde/latest/serde/trait.Serialize.html) * [`specta::Type`](https://docs.rs/specta/latest/specta/trait.Type.html) To fix this error ensure the custom types which you return from your procedure have the derive macros as shown below or that the type is a [Rust primitive type](https://doc.rust-lang.org/book/ch03-02-data-types.html). ```rs copy filename="src/main.rs" use specta::Type; use serde::Serialize; // This requires the 'derive' feature to be enabled. #[derive(Type, Serialize)] struct MyStruct {} #[derive(Type, Serialize)] enum MyStruct { SomeVariant } // Type aliases do not require the derive macro. type AnotherName = MyStruct; ``` If you are unable to determine what is causing your type to be invalid you can use the following utility functions to get a better warning from the Rust compiler. Ensure you don't keep this utility function in production code. ```rs copy filename="src/main.rs" pub struct Demo {} rspc::test_result_type::(); rspc::test_result_value(Demo {}); ``` #### the trait `for<'de> serde::de::Deserialize<'de>` is not implemented for [#the-trait-forde-serdededeserializede-is-not-implemented-for] This error means that the type which you specified as your argument type is invalid. Ensure the type implements the trait [`serde::DeserializeOwned`](https://docs.rs/serde/latest/serde/de/trait.DeserializeOwned.html). This can be done using the `Deserialize` derive macro provided by [serde](https://serde.rs/derive.html): ```rs copy filename="src/main.rs" use serde::Deserialize; // This requires the 'derive' feature to be enabled. #[derive(Deserialize)] struct MyStruct {} #[derive(Deserialize)] enum MyStruct { SomeVariant } // Type aliases do not require the derive macro. type AnotherName = MyStruct; ``` #### the trait `Type` is not implemented for [#the-trait-type-is-not-implemented-for] This error means that the type which you specified as your argument type is invalid. Ensure the type implements the trait [`specta::Type`](https://docs.rs/specta/latest/specta/trait.Type.html). This can be done using the `Type` derive macro: ```rs copy filename="src/main.rs" use specta::Type; #[derive(Type)] struct MyStruct {} #[derive(Type)] enum MyStruct { SomeVariant } ``` #### type mismatch in closure arguments [#type-mismatch-in-closure-arguments] This is probably caused by you incorrectly hardcoding the type for the request context (first argument) of the procedure closure. ```rs copy filename="src/main.rs" // INVALID CODE Router::<()>::new() // Here we set the context to `()` but we set the closures argument type to `i32`. .query("debug", |t| t(|ctx: i32, _: ()| {})) // SOLUTION Router::<()>::new() // Here we don't set the type of the context on the closure and Rust infers it. .query("debug", |t| t(|ctx, _: ()| {})) ``` # Concepts ## Capturing variables [#capturing-variables] rspc allows for capturing variables in the closure of a procedure. This is generally fround upon as it put a requirement on that value when creating the router which could limit your ability to unit test the router. More of the logic behind this is explained in request context section below. This is a general rule and you will likely find exceptions. ```rs copy filename="src/main.rs" // NOT-RECOMMEND - Capturing variables // You should avoid providing having arguments to your mount function pub(crate) fn mount(db: DatabaseConn) -> Router { // The `move` on the next line is the best indication that you are capturing variables. ::new().query("getUsers", move |t| { t(move |_, _: ()| async move { db.users().find_all().exec().await }) }); } // RECOMMEND - Using Request Context struct MyCtx { db: DatabaseConn } pub(crate) fn mount() -> Router { Router::::new().query("getUsers", |t| { t(|ctx: MyCtx, _: ()| async move { ctx.db.users().find_all().exec().await }) }); } ``` ## Request Context [#request-context] When calling execute on a operation you must provide a request context. The type of the request context must match the `TCtx` generic parameter defined on the `rspc::Router`. Using request context is important because it means you can construct the router without a dependency on anything (such a database) which allows you to validate the router in a unit test. The routes are stringly typed so we can't just rely on Rust's compiler to validate the router. This tradeoff was made for the superior developer experience as we believe using request context and a unit test for validating the router is able to mitigate the risk. A request context is created on every request and can hold any data the user wants. The request context also abstracts the underlying transport layer such as HTTP, Websocket or Tauri so that the router can be agonistic to which one is being used. ```rs copy filename="src/main.rs" struct MyCtx { db: Arc, some_value: &'static str } // Axum shown here as an example. This could be any transport. fn main() { let db = Arc::new(Database::new()); // Setup your rspc router to take your custom context type let router = Router::::new() .query("myQuery", |t| t(|ctx, input: ()| { assert_eq!(ctx.some_value, "Hello World"); })) .build(); axum::Router::new() // Attach the rspc router to your axum router // The closure you provide is used to create a new request context for each request .route("/rspc/:id", router .endpoint(move || MyCtx { db: db.clone(), some_value: "Hello World", }) .axum() ) } ``` # Error Handling rspc procedures have to return the type `Result` where `T` can be any type which can be returned from a normal procedure. The fact that Rust as a language currently requires the error type to be concrete makes error handling slightly annoying. All of the error handling done by rspc relys on the [question mark operator (`?`)](https://doc.rust-lang.org/rust-by-example/std/result/question_mark.html) in Rust to make a reasonable developer experience. The question mark operator will expand into something along the lines of `return Err(From::from(err))` under the hood. This means for any type `T` if you implement `From for rspc::Error` you will be able to rely on the question mark operator to convert it into an `rspc::Error` type. ### An example using the `rspc::Error` type [#an-example-using-the-rspcerror-type] ```rs copy filename="src/main.rs" use rspc::{Error, Router}; let router = ::new() .query("ok", |t| { t(|_, args: ()| { // Rust infers the return type is `Result` Ok("Hello World".into()) }) }) .query("err", |t| { t(|_, args: ()| { // Rust is unable to infer the `Ok` variant of the result. // We use the `as` keyword to tell Rust the type of the result. // This situation is rare in real world code. Err(Error::new( ErrorCode::BadRequest, "This is a custom error!".into(), )) as Result }) }) .query("errWithCause", |t| { t(|_, args: ()| { some_function_returning_error().map_err(|err| { Error::with_cause( ErrorCode::BadRequest, "This is a custom error!".into(), // This error type implements `std::error::Error` err, ) }) }) }) .build(); ``` ### Custom error type [#custom-error-type] ```rs copy filename="src/main.rs" pub enum MyCustomError { ServerDidABad, } impl From for rspc::Error { fn from(_: MyCustomError) -> Self { rspc::Error::new(rspc::ErrorCode::InternalServerError, "Server did an oopsie".into()) } } let router = ::new() .query("returnCustomErrorUsingQuestionMark", |t| { t(|_, args: ()| Ok(Err(MyCustomError::ServerDidABad)?)) }) .query("customErrUsingInto", |t| { t(|_, _args: ()| { let res: Result = some_function(); res.map_err(Into::into) // This is an alternative to using the question mark operator }) }) .build(); ``` # Middleware These API's are still undergoing stabilisation. Feel free to use them but they will likely change in the future updates! rspc allows adding middleware to your router which can intercept the request and response for procedures defined after it on the router. Middleware can also modify the context type which is passed to future procedures which is super powerful. The middleware APIs are still fairly new. Better documentation will come in the future once they are more stable. ## Context switching [#context-switching] Middleware are allowed to modify the context. This includes being able to change it's type. All operations below the middleware in the router will receive the new context type. ```rs copy filename="src/main.rs" use rspc::Router; fn main() { let router = Router::<()>::new() .middleware(|mw| mw.middleware(|mw| async move { let old_ctx: () = mw.ctx; Ok(mw.with_ctx(42)) })) .query("version", |t| { t(|ctx: i32, _: ()| "1.0.0") }) .query("anotherQuery", |t| t(|ctx: i32, _: ()| "Hello World!")) .build(); } ``` ## Route metadata [#route-metadata] Feature coming soon. Tracking in issue [#21](https://github.com/oscartbeaumont/rspc/issues/21). ## Examples [#examples] ### Logger middleware [#logger-middleware] ```rs copy filename="src/main.rs" let router = ::new() // Logger middleware .middleware(|mw| { mw.middleware(|mw| async move { let state = (mw.req.clone(), mw.ctx.clone(), mw.input.clone()); Ok(mw.with_state(state)) }) .resp(|state, result| async move { println!( "[LOG] req='{:?}' ctx='{:?}' input='{:?}' result='{:?}'", state.0, state.1, state.2, result ); Ok(result) }) }); ``` ### Authentication middleware [#authentication-middleware] ```rs copy filename="src/main.rs" pub struct UnauthenticatedContext { pub session_id: Option, } let router = Router::::new() .query("unauthenticatedQuery", |t| { t(|ctx: UnauthenticatedContext, _: ()| { "Some Public Data!" }) }) .middleware(|mw| { mw.middleware(|mw| async move { match mw.ctx.session_id { Some(ref session_id) => { let user = db_get_user_from_session(session_id).await; // We use `.with_ctx` to switch the context type. Ok(mw.with_ctx(AuthenticatedCtx { user })) } None => Err(rspc::Error::new( ErrorCode::Unauthorized, "Unauthorized".into(), )), } }) }) .query("authenticatedQuery", |t| { // This query takes the context from the middleware. t(|ctx: AuthenticatedCtx, _: ()| { "Some Secure Data!" }) }); ``` ### Reject all middleware [#reject-all-middleware] ```rs copy filename="src/main.rs" let router = ::new() // Reject all middleware .middleware(|mw| { mw.middleware(|mw| async move { Err(rspc::Error::new( ErrorCode::Unauthorized, "Unauthorized".into(), )) as Result, _> }) }) // The middleware was stop this query from being called. .query("unreachableQuery", |t| { t(|ctx: (), _: ()| { "Some Unreachable Data!" }) }); ``` # Plugins Coming soon... WIP Plugins: * OpenAPI - [issue #29](https://github.com/oscartbeaumont/rspc/issues/29) * Playground - [issue #23](https://github.com/oscartbeaumont/rspc/issues/23) * Authentication library # Router A router contains a collection of procedures (queries, mutations or subscriptions) that can be called by a client. A router has many generic arguments which can be configured by the user to match the type of data that the router will be handling. A router is defined as `Router`. ## TCtx [#tctx] The type of the [request context](/server/concepts#request-context). This is usually a `struct` containing state coming from the webserver such as a database connection and user session. For example: ```rs copy filename="src/main.rs" pub struct MyCtx { // The database connection. Prisma Client Rust shown here. db: Arc, // The session_id which could be extracted from HTTP cookies. session_id: Option, // The HTTP cookie jar. The `Cookies` type is hypothetical. cookies: Cookies, // An `Arc` allows us to hold multiple immutable references to the message. // The `Mutex` allows interior mutability so we can safely modify the string from a immutable reference. my_cool_msg: Arc>, } ``` Constructing an rspc router with a specific context type is done as following. ```rs copy filename="src/main.rs" // Create a router with the default `TCtx` of `()` let router = ::new(); // Create a router with a custom `TCtx` type let router = Router::::new(); ``` `TCtx` is super powerful in rspc because [middleware](/server/middleware) are able to change it for procedures following them in the chain. A great example of this in action is an authentication middleware like shown below. ```rs copy filename="src/main.rs" pub struct AuthenticatedCtx { db: Arc, // Your database connection user: User, // Your user model in Rust. } // We define a router with the `TCtx` type set to `MyCtx` let router = Router::::new() // We then define the version query before the middleware so that it doesn't require authentication. .query("version", |t| { t(|ctx: MyCtx, _: ()| "1.0.0") }) // Then we define a middleware which is responsible for rejecting unauthorized requests. .middleware(|mw| mw.middleware(|mw| async move { let old_ctx = mw.ctx; match old_ctx.session_id { Some(ref session_id) => { // .with_ctx changes the type of `TCtx` for all preceding procedures. Ok(mw.with_ctx(AuthenticatedCtx { user: User::from_session(session_id).await? })) } None => Err(rspc::Error::new( ErrorCode::Unauthorized, "Unauthorized".into(), )), } })) // We then define a query to return the current user. This will only be called for authenticated users. .query("getMe", |t| { // See how we now take in `AuthenticatedCtx` with all the data from the middleware t(|ctx: AuthenticatedCtx, _: ()| ctx.user) }) .build(); ``` ## TMeta [#tmeta] For all intents and purposes keep this `()`. This generic argument does nothing in the current release and may be deprecated in the future. ## TMiddleware [#tmiddleware] This argument holds the instance of the last middleware builder which you mounted onto your router through a `.middleware(...)` call. You generally don't need to worry about this generic but is what allows the context switching to work. ## Attaching procedures [#attaching-procedures] Procedures represent a function you define in Rust which can be called from the frontend. You can define them on your routing like the following example. ```rs copy filename="src/main.rs" let router = ::new() // Define a query taking no arguments and returning "1.0.0" .query("version", |t| t(|ctx, input: ()| "1.0.0")) // Define a query taking a string and returning it .query("echo", |t| t(|ctx, input: String| input)) // Define a query which does an asynchronous operation. .query("getUsers", |t| t(|ctx, input: String| async move { await User::get_all() // returns `User` })) // The same syntax as above can be used for mutations. .mutation("createUser", |t| t(|ctx, new_user: User| async move { await new_user.create() // Returns `()` })) // Subscriptions can also be used for server -> client real time events // Subscriptions have a slightly different syntax. You can respond with any Rust `Stream` type. .subscription("pings", |t| t(|ctx, input: ()| async_stream::stream! { for i in 0..5 { yield "ping".to_string(); sleep(Duration::from_secs(1)).await; } })) .build(); // Ensure you build once you have added all your operations. ``` ## Should I use a query or a mutation? [#should-i-use-a-query-or-a-mutation] Does your operation have **side effects**? If so, use a mutation else, use a query. A query should not change any data on the server, it should just be responsible for fetching data. A mutation should be responsible for changing data on the server. ## Merging routers [#merging-routers] When building an API server, you will often want to split up your endpoints into multiple files to make the code easier to work on. You can combine routers using the `.merge` method. `router.merge(prefix: &'static str, router: Router)` ```rs copy filename="src/main.rs" // This could be defined in another file or even another crate let users_router = ::new() .query("list", |t| t(|ctx, input: ()| vec![] as Vec<()>)); let router = ::new() .query("version", |t| t(|_ctx, _: ()| "1.0.0")) // The first parameter is a prefix to add to all routes in the merged router. .merge("users.", users_router) // You can now call `users.list` from your frontend. .build(); ``` ## Invalidate query [#invalidate-query] 🚧 WIP - [Tracking issue #19](https://github.com/oscartbeaumont/rspc/issues/19) ## Method chaining [#method-chaining] When combining multiple operations, you must ensure you chain the method calls or shadow the router variable. This is required due to the way the generics work on the Router. ```rs copy filename="src/main.rs" // Chaining method calls let router = ::new() .query("version", |t| t(|ctx, input: ()| todo!())) .mutation("createUser", |t| t(|ctx, input: ()| todo!())) .build(); // Shadowing variable let router = ::new() .query("version", |t| t(|ctx, input: ()| todo!())) .mutation("createUser", |t| t(|ctx, input: ()| todo!())); let router = router .mutation("deleteUser", |t| t(|ctx, input: ()| todo!())); let router = router.build(); ``` ## Exporting the Typescript bindings [#exporting-the-typescript-bindings] There are two methods to export the Typescript bindings. You can either use the `export_ts_bindings` configuration option or call the `export_ts` function directly on the build router. ```rs copy filename="src/main.rs" let router = ::new() .config( Config::new() // Doing this will automatically export the bindings when the `build` function is called. .export_ts_bindings(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("./bindings.ts")) ) .query("version", |t| t(|_, _: ()| env!("CARGO_PKG_VERSION"))) .build(); // Doing it this way you have the flexibility to export it at any time and to wheerever you want. router.export_ts(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("./bindings.ts")).unwrap(); ``` You can also use the `set_ts_bindings_header` option on the `Config` if you want to add a custom header to the top of the generated file. This is useful to disable [ESLint](https://eslint.org), [Prettier](https://prettier.io) or other similar tools from processing the generated file. ```rs copy filename="src/main.rs" let router = ::new() .config( Config::new() // This text is added to the start of the exported Typescript file. .set_ts_bindings_header("/* eslint-disable */") )) .query("version", |t| t(|_, _: ()| env!("CARGO_PKG_VERSION"))) .build(); ``` # Selection **If you are using [Prisma Client Rust](https://prisma.brendonovich.dev) with rspc generally use [select & include](https://prisma.brendonovich.dev/reading-data/select-include) instead of this.** It is very common when building an API to fetch some data from the database but you only want to expose a subset of the data to the client. With rspc you can use the `selection!` macro to easily return a subset of fields on a struct. For example say you have a `User` struct like the following: ```rs copy filename="src/main.rs" pub struct User { pub id: i32, pub name: String, pub email: String, pub age: i32, pub password: String, } ``` If your database returns a `User` struct you are unable to return it directly from your procedure as that would leak the value in the `password` field. Traditionally you would have to create a second struct without the `password` field, however this isn't optimal as it adds unnecessary boilerplate to your project. Instead you can use the `selection!` macro like below to select only certain fields from the struct. ```rs copy filename="src/main.rs" let router = ::new() .query("me", |t| { t(|_, _: ()| { // This struct would be returned from your database! let user = User { id: 1, name: "Monty Beaumont".into(), email: "monty@otbeaumont.me".into(), age: 7, password: "password123".into(), }; selection!(user, { name, age }) // We select only the name and age fields to return }) }) .query("users", |t| { t(|_, _: ()| { let user = User { name: "Monty Beaumont".into(), email: "monty@otbeaumont.me".into(), age: 7, password: "password123".into(), }; // We have a vector of data which contains information but we only want to return some of it the user. // Eg. We don't want to expose the password field. let users = vec![user.clone(), user.clone(), user]; // Here we are selecting the fields we want to expose on each item in the list. This is completely type safe! // The square brackets around the selection dictate that the selection should be applied to each item in the list. selection!(users, [{ name, age }]) }) }) .build(); ``` # Specta For rspc to be able to convert your types into Typescript they must implement the `specta::Type` trait. [Specta](https://github.com/oscartbeaumont/rspc/tree/main/specta) is a crate that was created so that rspc can introspect Rust types. The `Type` trait allows the Typescript exporter to understand the fields, generics and dependant types of a Rust type. The easiest way to implement the `specta::Type` trait is by using the `specta::Type` derive macro. We have already implemented most in-built types if you can find a missing one open a [GitHub Issue](https://github.com/oscartbeaumont/rspc). ```rs copy filename="src/main.rs" use specta::Type; #[derive(Type)] pub struct MyStruct { pub name: String, pub age: i32, } #[derive(Type)] pub enum MyEnum { SomeVariant, // It is import MyStruct also implements `Type` or this will not work AnotherVariant(MyStruct), } ``` ### Limitations [#limitations] You should be careful when using generics with [type aliases](https://doc.rust-lang.org/reference/items/type-aliases.html) as you may run into situations where the types are not exported correctly. As far as we are aware this is a known limitation with Rust and is not something we can fix. If you run into this edge case you should change to using a `struct` instead of a type alias to workaround the problem. ### Other languages [#other-languages] Specta stores information about your type which means an exporter for languages other than [Typescript](https://www.typescriptlang.org) could be made. If you are interested in supporting other languages, you can do so directly using the `specta::Type` trait in your own project, however a pull request to Specta would be appreciated. ### Specta without rspc [#specta-without-rspc] Specta is an independent crate and can be used without rspc. Refer to it's [documentation](https://docs.rs/specta) for support using it. # React rspc can be used on the frontend with [React](https://reactjs.org) via the powerful [React Query](https://tanstack.com/query/v4) library which provides caching, refetching and a lot more. To get started first install the required packages. ```bash copy pnpm i @rspc/client # The core client pnpm i @rspc/react # The React Query integration ``` Then you can do the following: ```tsx copy filename="index.ts" import { QueryClient } from "@tanstack/react-query"; import { FetchTransport, createClient } from "@rspc/client"; import { createReactQueryHooks } from "@rspc/react"; import type { Procedures } from "./ts/index"; // These were the bindings exported from your Rust code! // You must provide the generated types as a generic and create a transport (in this example we are using HTTP Fetch) so that the client knows how to communicate with your API. const client = createClient({ // Refer to the integration your using for the correct transport. transport: new FetchTransport("http://localhost:4000/rspc"), }); const queryClient = new QueryClient(); const rspc = createReactQueryHooks(); function SomeComponent() { const { data, isLoading, error } = rspc.useQuery(["version"]); const { mutate } = rspc.useMutation("updateVersion"); return ( <>

{data}

); } function App() { return ( ); } ``` # Rust The Rust client is currently still very experimental and will likely change before it's stable release. Coming soon... # SolidJS rspc can be used on the frontend with [SolidJS](https://www.solidjs.com/) via [Tanstack Solid Query](https://tanstack.com/query/v4/docs/adapters/solid-query) which provides caching, refetching and a lot more. To get started first install the required packages. ```bash copy pnpm i @rspc/client # The core client pnpm i @rspc/solid # The SolidJS integration ``` Then you can do the following: ```tsx copy filename="index.ts" import { QueryClient } from "@tanstack/solid-query"; import { FetchTransport, createClient } from "@rspc/client"; import { createSolidQueryHooks } from "@rspc/solid"; import type { Procedures } from "./ts/index"; // These were the bindings exported from your Rust code! // You must provide the generated types as a generic and create a transport (in this example we are using HTTP Fetch) so that the client knows how to communicate with your API. const client = createClient({ // Refer to the integration your using for the correct transport. transport: new FetchTransport("http://localhost:4000/rspc"), }); const queryClient = new QueryClient(); const rspc = createSolidQueryHooks(); function SomeComponent() { const echo = rspc.createQuery(() => ({ queryKey: ["echo", "somevalue"], })); const sendMsg = rspc.createMutation(() => ({ mutationKey: "sendMsg", })); return ( <>

{echo.data}

); } function App() { return ( ); } ``` # Svelte rspc can be used on the frontend with [Svelte](https://svelte.dev) via [Tanstack Svelte Query](https://tanstack.com/query/latest/docs/framework/svelte/overview) which provides caching, refetching and a lot more. To get started first install the required packages. ```bash copy pnpm i @rspc/client # The core client pnpm i @rspc/svelte-query # The integration ``` Then you can do the following: ```svelte copy filename="index.svelte"

Using rspc version: {$version.data}

``` # Tanstack Query Coming soon... For now you can checkout the old integrations: * [React](./react) * [Solid](./solid) * [Svelte](./svelte) # Vanilla The vanilla client allows you to consume your API on the frontend. This client is the minimal core and it is recommended that you use the [React](/client/react) or [Solid](/client/solid) integration for building application. To get started first install the minimal runtime package. ```bash copy npm i @rspc/client ``` Next you need to export the Typescript bindings from your `rspc::Router` by using either [export\_ts\_bindings](/server/router#exporting-the-typescript-bindings) or [export\_ts](/server/router#exporting-the-typescript-bindings). ```rs /export_ts_bindings/ copy filename="src/main.rs" let router = ::new() // This will automatically export the bindings to the `./ts` directory when you run build() in a non-release Rust build .config(Config::new().export_ts_bindings("./bindings.rs")) .build(); ``` Then you can use the `@rspc/client` package to consume your API. ```ts copy filename="index.ts" import { createClient, FetchTransport } from "@rspc/client"; import type { Procedures } from "./ts/index"; // These were the bindings exported from your Rust code! // You must provide the generated types as a generic and create a transport (in this example we are using HTTP Fetch) so that the client knows how to communicate with your API. const client = createClient({ // Refer to the integration your using for the correct transport. transport: new FetchTransport("http://localhost:4000/rspc"), }); // Now use the client in your code! const version = await client.query(["version"]); // The types will be inferred from your backend. const userOne = await client.query(["getUser", 1]); const userTwo = await client.mutation(["addUser", { name: "Monty Beaumont" }]); ``` # Transports [#transports] rspc has multiple different transports which can be used. These dictate how the frontend is able to talk with the backend. ## Fetch Transport [#fetch-transport] Fetch transport does not support subscriptions! Transport is built on the standard [Fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) API.
`GET` or `POST` ? rspc uses: * `GET` requests for queries * `POST` requests for mutations **Important:** While rspc primarily uses `GET` requests for queries, it may also use `POST` requests for queries in certain scenarios (similar to how GraphQL operates). **Mutations will always use `POST` requests and will never use `GET`.** This behavior is an implementation detail and **isn't guaranteed** to remain the same in the future.
```ts /FetchTransport/ copy filename="index.ts" import { createClient, FetchTransport } from "@rspc/client"; import type { Procedures } from "./bindings.ts"; // The bindings exported from your Rust code! const client = createClient({ transport: new FetchTransport("http://localhost:4000/rspc"), }); ``` ### Custom Fetch implementation [#custom-fetch-implementation] ```ts copy filename="index.ts" import { createClient, FetchTransport } from "@rspc/client"; import type { Procedures } from "./bindings.ts"; // The bindings exported from your Rust code! const client = createClient({ transport: new FetchTransport( "http://localhost:4000/rspc", // Include Cookies for cross-origin requests (input, init) => fetch(input, { ...init, credentials: "include" }), ), }); ``` ### Fetch Authentication [#fetch-authentication] Guide coming soon... ## Websocket Transport [#websocket-transport] Transport build on the standard [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) API. This uses HTTP GET and POST requests under the hood. ```ts /WebsocketTransport/ copy filename="index.ts" import { createClient, WebsocketTransport } from "@rspc/client"; import type { Procedures } from "./bindings.ts"; // The bindings exported from your Rust code! const client = createClient({ transport: new WebsocketTransport("ws://localhost:8080/rspc/ws"), }); ``` ### Websocket Authentication [#websocket-authentication] Guide coming soon... \-- TODO: Discuss Single Flight Mutations # Overview rspc is a typesafe router for Rust which allows you to build end-to-end type safe APIs by generating a typesafe client to call the server from another language. ### Features [#features] * ✅ **Typesafety** - move fast and **eliminate** a whole class of common bugs * ✅ **Transport Agnostic** - expose your router via [Axum](https://github.com/tokio-rs/axum), [Tauri](https://tauri.app) or any other official or custom transport. * ✅ **Performance** - small runtime footprint so your can get the **full potential of Rust's speed** * ✅ **Middleware Ecosystem** - easily extend your procedures with **auth, logging and more** ### Frequently Asked Questions [#frequently-asked-questions] Due to rspc's opinionated nature we are able to push the boundaries of developer experience and optimisations which a regular web framework is not able to. Although many of rspc's features can be achieved with a regular web framework your going to end up with compromises or end up reinventing rspc instead of building your application. Big features: * End to end type safety as a first-class feature. * Client libraries to make calling your server from another language easy. * Ecosystem of middleware for abstracting common patterns. * Better compiler errors * Potentially better performance due to batching and single flight mutations. It is currently being used by the following projects: * [Spacedrive](https://spacedrive.com) * [CrabNebula Cloud](https://crabnebula.dev/cloud) * [Macrograph](https://macrograph.app) * [Twidge](https://github.com/twidgeapp/twidge) * [Chessbook](https://chessbook.com) Although programming language choice is generally informed by the requirements of the project and the team's experience a lot of common reasons come down to the way Rust's strong type system, performance and low-level control allow you to build reliable and fast software that is able to stay maintainable for a long time. rspc is great as your primary API, a microservice or even embedded into your desktop application making working with your Rust backend a breeze when you need it. # Actix Web TODO # Axum rspc has a built-in integration with [Axum](https://github.com/tokio-rs/axum) so that you can expose your API over HTTP. ### Enable feature [#enable-feature] You must install the [`rspc_axum`](https://docs.rs/rspc-axum/latest/rspc_axum/) crate to use Axum with rspc. ```toml /rspc_axum = { version = "0.0.0", features = ["ws"] }/ copy filename="Cargo.toml" [dependencies] rspc = "0.0.0" rspc_axum = { version = "0.0.0", features = ["ws"] } axum = "0.7.0" ``` ### Usage [#usage] ```rs copy filename="src/main.rs" let router = rspc::Router::<()>::new() .query("version", |_, _: ()| "1.0.0") .build() .arced(); let app = axum::Router::new() .route("/", get(|| async { "Hello 'rspc'!" })) .nest("/rspc", rspc_axum::endpoint(router, || ())) .layer(cors); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); axum::serve(listener, app).await.unwrap(); ``` {/* TODO: Bring this back */} {/* ### Extracting Context from Request **Warning: The Axum extractor API is probally going to be removed in a future release. If you are using this API, I would appreciate a message in the Discord about your usecase so I can ensure the replacement API can do everything you need.** **Warning: Current we only support a single extractor. This is a temporary limitation so open a GitHub Issue if you need more.** You may want to use Axum extractors to get data from the request such as cookies and put them on the request context. The `axum_handler` function takes a closure that can take up to 16 valid Axum extractors as arguments and then returns the [request context](/server/request-context) (of type `TCtx`). ```rs copy filename="src/main.rs" let router = rspc::Router::::new() .query("currentPath", |ctx, _: ()| ctx) .build() .arced(); let app = axum::Router::new() .route("/", get(|| async { "Hello 'rspc'!" })) // We use Axum `Path` extractor. The `rspc::Router` has `TCtx` set to `String` so we return the path string as the context. .nest("/rspc", rspc_axum::endpoint(|req: Request| req.uri().path())) .layer(cors); ``` */} ### Usage on frontend [#usage-on-frontend] ```ts copy filename="index.ts" import { FetchTransport, WebsocketTransport, createClient } from "@rspc/client"; import type { Procedures } from "./ts/bindings"; // These were the bindings exported from your Rust code! // For fetch transport const client = createClient({ transport: new FetchTransport("http://localhost:4000/rspc"), }); // For websocket transport - Required for subscriptions const client = createClient({ transport: new WebsocketTransport("ws://localhost:8080/rspc/ws"), }); client.query(["version"]).then((data) => console.log(data)); ``` # HTTP TODO ## TODO: [#todo] * Explain wire protocol w/ reference implementation * Explain extractors and content types * Using Workers, Lambda, Vercel/Netlify functions via Axum adapater * Explain supporting 3rd party web frameworks & contributing them back into rspc. * CDN caching * WebSocket * Allow `form` to submit to a specific query. Maybe a custom endpoint for it specifically. # Tauri rspc has a built-in integration with [Tauri](https://tauri.app/) so that you can expose your API to your frontend code using Tauri's IPC. ### Enable feature [#enable-feature] For the integration to work you must enable the `tauri` feature of rspc. Ensure the rspc line in your `Cargo.toml` file looks like the following: ```toml /rspc_tauri = "0.0.0"/ copy filename="Cargo.toml" [dependencies] rspc = "0.0.0" rspc-tauri = "0.0.0" ``` Read more about Rust features [here](https://doc.rust-lang.org/cargo/reference/features.html#dependency-features) ### Usage [#usage] Then expose your router using the Tauri plugin. ```rs copy filename="src/main.rs" let router = ::new().build(); tauri::Builder::default() .plugin(rspc_tauri::plugin(router.arced(), |app_handle| ())) ``` ### Usage on frontend [#usage-on-frontend] ```ts copy filename="index.ts" import { createClient } from "@rspc/client"; import { TauriTransport } from "@rspc/tauri"; import type { Procedures } from "./ts/bindings"; // These were the bindings exported from your Rust code! const client = createClient({ transport: new TauriTransport(), }); client.query(["version"]).then((data) => console.log(data)); ``` You can use the `client` by itself or integrate with the [Tanstack Query](/client/tanstack-query) hooks. # Advanced \-- TOOD: Zero-copy deserialization \-- TODO: Custom integration # Internal \-- TOOD: Crate structure # Cache # TODO [#todo] # Invalidation The invalidation middleware is not yet available but we are actively working on it. Stay tuned! # OpenAPI The OpenAPI middleware is not yet available but we are actively working on it. Stay tuned! # Overview These API's are still undergoing stabilisation. Feel free to use them but they will likely change in the future updates! rspc allows adding middleware to your router which can intercept the request and response for procedures defined after it on the router. Middleware can also modify the context type which is passed to future procedures which is super powerful. The middleware APIs are still fairly new. Better documentation will come in the future once they are more stable. ## Context switching [#context-switching] Middleware are allowed to modify the context. This includes being able to change it's type. All operations below the middleware in the router will receive the new context type. ```rs copy filename="src/main.rs" use rspc::Router; fn main() { let router = Router::<()>::new() .middleware(|mw| mw.middleware(|mw| async move { let old_ctx: () = mw.ctx; Ok(mw.with_ctx(42)) })) .query("version", |t| { t(|ctx: i32, _: ()| "1.0.0") }) .query("anotherQuery", |t| t(|ctx: i32, _: ()| "Hello World!")) .build(); } ``` ## Examples [#examples] ### Logger middleware [#logger-middleware] ```rs copy filename="src/main.rs" let router = ::new() // Logger middleware .middleware(|mw| { mw.middleware(|mw| async move { let state = (mw.req.clone(), mw.ctx.clone(), mw.input.clone()); Ok(mw.with_state(state)) }) .resp(|state, result| async move { println!( "[LOG] req='{:?}' ctx='{:?}' input='{:?}' result='{:?}'", state.0, state.1, state.2, result ); Ok(result) }) }); ``` ### Authentication middleware [#authentication-middleware] ```rs copy filename="src/main.rs" pub struct UnauthenticatedContext { pub session_id: Option, } let router = Router::::new() .query("unauthenticatedQuery", |t| { t(|ctx: UnauthenticatedContext, _: ()| { "Some Public Data!" }) }) .middleware(|mw| { mw.middleware(|mw| async move { match mw.ctx.session_id { Some(ref session_id) => { let user = db_get_user_from_session(session_id).await; // We use `.with_ctx` to switch the context type. Ok(mw.with_ctx(AuthenticatedCtx { user })) } None => Err(rspc::Error::new( ErrorCode::Unauthorized, "Unauthorized".into(), )), } }) }) .query("authenticatedQuery", |t| { // This query takes the context from the middleware. t(|ctx: AuthenticatedCtx, _: ()| { "Some Secure Data!" }) }); ``` ### Reject all middleware [#reject-all-middleware] ```rs copy filename="src/main.rs" let router = ::new() // Reject all middleware .middleware(|mw| { mw.middleware(|mw| async move { Err(rspc::Error::new( ErrorCode::Unauthorized, "Unauthorized".into(), )) as Result, _> }) }) // The middleware was stop this query from being called. .query("unreachableQuery", |t| { t(|ctx: (), _: ()| { "Some Unreachable Data!" }) }); ``` # Tracing # TODO [#todo] # Zer # TODO [#todo] Focus: * Session management - Refresh token type thing * OAuth * Email auth??? # Migrations You should upgrade each version of rspc in order and ensure you don't run into any issues at each step. # 0.4.0 [#040] TODO: Write this out # 0.5.0 [#050] TODO: Plan this out # Quickstart To get started with rspc you can use the quickstart guide below. ### Initialise Rust project [#initialise-rust-project] If you haven't got a Rust project already setup, create a new one using the following command. {/* TODO: Add `&&` when copying bash commands */} ```bash cargo new cd cargo add tokio --features full # rpsc requires an async runtime ``` ### Install rspc [#install-rspc] `rspc` is distributed through a Rust crate hosted on [crates.io](https://crates.io/crates/rspc). Add it to your project using the following command: ```bash cargo add rspc specta ``` This command will not exist if your running a Rust version earlier than `1.62.0`, please upgrade your Rust version if this is the case. ### Create a router [#create-a-router] {/* TODO: Boilerplate for typesafe errors and middleware */} Go into `src/main.rs` and add the following code: ```rs copy filename="src/main.rs" use rspc::Router; fn router() -> Router<()> { ::new() .query("version", |t| t(|ctx, input: ()| env!("CARGO_PKG_VERSION"))) .build() } #[tokio::main] async fn main() { let router = router(); // TODO: Mount an integration to expose your API } #[cfg(test)] mod tests { // It is highly recommended to unit test your rspc router by creating it // This will ensure it doesn't have any issues and also export updated Typescript types. #[test] fn test_rspc_router() { super::router(); } } ``` ### Exposing your router [#exposing-your-router] Now that you have a router your probably wondering how you access it from your frontend. This is done through an rspc integration. I would recommend starting with [Axum](https://github.com/tokio-rs/axum), by following [this](/integrations/axum). {/* TODO: Include install commands for this too */} TODO TODO ### Usage on the frontend [#usage-on-the-frontend] Refer to the [Vanilla](/client), [React](/client/react) or [Solid](/client/solid) documentation for how to use the rspc client in your frontend. TODO TODO TODO TODO ### Adding a procedure [#adding-a-procedure] TODO: Explaining adding procedure and using `derive(Type)` ## TODO [#todo] * Using `specta::selection`/`specta::json` # Integrations TODO: Show all of them and how to set them up. # Procedure A procedure represents a single operation which can be executed on your server. You define procedures which are collected up into a [router](./router). ## Procedure setup [#procedure-setup] The following code can be copied as the base setup for defining procedures. Although this may look like a lot of boilerplate, any non-trivial application will end up with all of these components. ```rust use serde::Serialize; use specta::Type; use thiserror::Error; use rspc::{Error2, ResolverError}; #[derive(Clone)] pub struct Ctx {} #[derive(Debug, Error, Serialize, Type)] pub enum Error {} impl Error2 for Error { fn into_resolver_error(self) -> rspc::ResolverError { ResolverError::new(500, self.to_string(), None::) // TODO: Shuffle error into last param? } } pub struct BaseProcedure(PhantomData); impl BaseProcedure { pub fn builder() -> ProcedureBuilder where TErr: Error2, TInput: ResolverInput, TResult: ResolverOutput, { Procedure2::builder() // You add default middleware here } } ``` ## Defining a procedure [#defining-a-procedure] The following code shows how to define a procedure and attach it to a router. rspc allows many procedures to be attached to a single router which allows logical grouping of procedures such as by feature or domain model. ```rust use rspc::Router2; pub fn mount() -> Router2 { Router2::new() .procedure("version", { ::builder().query(|_, _: ()| async move { Ok(env!("CARGO_PKG_VERSION")) }) }) } ``` ## Using custom types [#using-custom-types] For rspc to be able to convert your types into Typescript they must implement the `specta::Type` trait. [Specta](https://github.com/oscartbeaumont/rspc/tree/main/specta) is a crate that was created so that rspc can introspect Rust types. The `Type` trait allows the Typescript exporter to understand the fields, generics and dependant types of a Rust type. The easiest way to implement the `specta::Type` trait is by using the `specta::Type` derive macro. We have already implemented most in-built types if you can find a missing one open a [GitHub Issue](https://github.com/oscartbeaumont/rspc). ```rs use specta::Type; #[derive(Type)] pub struct MyStruct { pub name: String, pub age: i32, } #[derive(Type)] pub enum MyEnum { SomeVariant, // It is import MyStruct also implements `Type` or this will not work AnotherVariant(MyStruct), } ``` ## Request Context [#request-context] When calling execute on a operation you must provide a request context. The type of the request context must match the `TCtx` generic parameter defined on the `rspc::Router`. Using request context is important because it means you can construct the router without a dependency on anything (such a database) which allows you to validate the router in a unit test. The routes are stringly typed so we can't just rely on Rust's compiler to validate the router. This tradeoff was made for the superior developer experience as we believe using request context and a unit test for validating the router is able to mitigate the risk. A request context is created on every request and can hold any data the user wants. The request context also abstracts the underlying transport layer such as HTTP, Websocket or Tauri so that the router can be agonistic to which one is being used. ```rs copy filename="src/main.rs" struct MyCtx { db: Arc, some_value: &'static str } // Axum shown here as an example. This could be any transport. fn main() { let db = Arc::new(Database::new()); // Setup your rspc router to take your custom context type let router = Router::::new() .query("myQuery", |t| t(|ctx, input: ()| { assert_eq!(ctx.some_value, "Hello World"); })) .build(); axum::Router::new() // Attach the rspc router to your axum router // The closure you provide is used to create a new request context for each request .route("/rspc/:id", router .endpoint(move || MyCtx { db: db.clone(), some_value: "Hello World", }) .axum() ) } ``` ### Capturing variables [#capturing-variables] rspc allows for capturing variables in the closure of a procedure. This is generally fround upon as it put a requirement on that value when creating the router which could limit your ability to unit test the router. More of the logic behind this is explained in request context section below. This is a general rule and you will likely find exceptions. ```rs copy filename="src/main.rs" // NOT-RECOMMEND - Capturing variables // You should avoid providing having arguments to your mount function pub(crate) fn mount(db: DatabaseConn) -> Router { // The `move` on the next line is the best indication that you are capturing variables. ::new().query("getUsers", move |t| { t(move |_, _: ()| async move { db.users().find_all().exec().await }) }); } // RECOMMEND - Using Request Context struct MyCtx { db: DatabaseConn } pub(crate) fn mount() -> Router { Router::::new().query("getUsers", |t| { t(|ctx: MyCtx, _: ()| async move { ctx.db.users().find_all().exec().await }) }); } ``` ## Error handling [#error-handling] rspc procedures have to return the type `Result` where `T` can be any type which can be returned from a normal procedure. The fact that Rust as a language currently requires the error type to be concrete makes error handling slightly annoying. All of the error handling done by rspc relys on the [question mark operator (`?`)](https://doc.rust-lang.org/rust-by-example/std/result/question_mark.html) in Rust to make a reasonable developer experience. The question mark operator will expand into something along the lines of `return Err(From::from(err))` under the hood. This means for any type `T` if you implement `From for rspc::Error` you will be able to rely on the question mark operator to convert it into an `rspc::Error` type. ```rs copy filename="src/main.rs" use rspc::{Error, Router}; let router = ::new() .query("ok", |t| { t(|_, args: ()| { // Rust infers the return type is `Result` Ok("Hello World".into()) }) }) .query("err", |t| { t(|_, args: ()| { // Rust is unable to infer the `Ok` variant of the result. // We use the `as` keyword to tell Rust the type of the result. // This situation is rare in real world code. Err(Error::new( ErrorCode::BadRequest, "This is a custom error!".into(), )) as Result }) }) .query("errWithCause", |t| { t(|_, args: ()| { some_function_returning_error().map_err(|err| { Error::with_cause( ErrorCode::BadRequest, "This is a custom error!".into(), // This error type implements `std::error::Error` err, ) }) }) }) .build(); ``` ## TODO [#todo] * Put recommended file names on the code snippets??? * Go through and breakdown the generics/parts. What traits the input/return types need, etc. * Example using `anyhow` * Example exposing strings to the frontend * Show examples extending `BaseProcedure` * Why not middleware on router? * Error handling # Router A router is responsible for collecting up [procedures](./procedures). Once a router is built you are provided with the types for generating the client bindings and the procedure handlers which can be exposed via an [integration](../integrations/axum). ## TCtx [#tctx] The type of the [request context](/server/concepts#request-context). This is usually a `struct` containing state coming from the webserver such as a database connection and user session. For example: ```rs copy filename="src/main.rs" pub struct MyCtx { // The database connection. Prisma Client Rust shown here. db: Arc, // The session_id which could be extracted from HTTP cookies. session_id: Option, // The HTTP cookie jar. The `Cookies` type is hypothetical. cookies: Cookies, // An `Arc` allows us to hold multiple immutable references to the message. // The `Mutex` allows interior mutability so we can safely modify the string from a immutable reference. my_cool_msg: Arc>, } ``` Constructing an rspc router with a specific context type is done as following. ```rs copy filename="src/main.rs" // Create a router with the default `TCtx` of `()` let router = ::new(); // Create a router with a custom `TCtx` type let router = Router::::new(); ``` `TCtx` is super powerful in rspc because [middleware](/server/middleware) are able to change it for procedures following them in the chain. A great example of this in action is an authentication middleware like shown below. ```rs copy filename="src/main.rs" pub struct AuthenticatedCtx { db: Arc, // Your database connection user: User, // Your user model in Rust. } // We define a router with the `TCtx` type set to `MyCtx` let router = Router::::new() // We then define the version query before the middleware so that it doesn't require authentication. .query("version", |t| { t(|ctx: MyCtx, _: ()| "1.0.0") }) // Then we define a middleware which is responsible for rejecting unauthorized requests. .middleware(|mw| mw.middleware(|mw| async move { let old_ctx = mw.ctx; match old_ctx.session_id { Some(ref session_id) => { // .with_ctx changes the type of `TCtx` for all preceding procedures. Ok(mw.with_ctx(AuthenticatedCtx { user: User::from_session(session_id).await? })) } None => Err(rspc::Error::new( ErrorCode::Unauthorized, "Unauthorized".into(), )), } })) // We then define a query to return the current user. This will only be called for authenticated users. .query("getMe", |t| { // See how we now take in `AuthenticatedCtx` with all the data from the middleware t(|ctx: AuthenticatedCtx, _: ()| ctx.user) }) .build(); ``` ## Attaching procedures [#attaching-procedures] Procedures represent a function you define in Rust which can be called from the frontend. You can define them on your routing like the following example. ```rs copy filename="src/main.rs" let router = ::new() // Define a query taking no arguments and returning "1.0.0" .query("version", |t| t(|ctx, input: ()| "1.0.0")) // Define a query taking a string and returning it .query("echo", |t| t(|ctx, input: String| input)) // Define a query which does an asynchronous operation. .query("getUsers", |t| t(|ctx, input: String| async move { await User::get_all() // returns `User` })) // The same syntax as above can be used for mutations. .mutation("createUser", |t| t(|ctx, new_user: User| async move { await new_user.create() // Returns `()` })) // Subscriptions can also be used for server -> client real time events // Subscriptions have a slightly different syntax. You can respond with any Rust `Stream` type. .subscription("pings", |t| t(|ctx, input: ()| async_stream::stream! { for i in 0..5 { yield "ping".to_string(); sleep(Duration::from_secs(1)).await; } })) .build(); // Ensure you build once you have added all your operations. ``` ## Should I use a query or a mutation? [#should-i-use-a-query-or-a-mutation] Does your operation have **side effects**? If so, use a mutation else, use a query. A query should not change any data on the server, it should just be responsible for fetching data. A mutation should be responsible for changing data on the server. ## Merging routers [#merging-routers] When building an API server, you will often want to split up your endpoints into multiple files to make the code easier to work on. You can combine routers using the `.merge` method. `router.merge(prefix: &'static str, router: Router)` ```rs copy filename="src/main.rs" // This could be defined in another file or even another crate let users_router = ::new() .query("list", |t| t(|ctx, input: ()| vec![] as Vec<()>)); let router = ::new() .query("version", |t| t(|_ctx, _: ()| "1.0.0")) // The first parameter is a prefix to add to all routes in the merged router. .merge("users.", users_router) // You can now call `users.list` from your frontend. .build(); ``` ## Exporting the Typescript bindings [#exporting-the-typescript-bindings] There are two methods to export the Typescript bindings. You can either use the `export_ts_bindings` configuration option or call the `export_ts` function directly on the build router. ```rs copy filename="src/main.rs" let router = ::new() .config( Config::new() // Doing this will automatically export the bindings when the `build` function is called. .export_ts_bindings(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("./bindings.ts")) ) .query("version", |t| t(|_, _: ()| env!("CARGO_PKG_VERSION"))) .build(); // Doing it this way you have the flexibility to export it at any time and to wheerever you want. router.export_ts(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("./bindings.ts")).unwrap(); ``` You can also use the `set_ts_bindings_header` option on the `Config` if you want to add a custom header to the top of the generated file. This is useful to disable [ESLint](https://eslint.org), [Prettier](https://prettier.io) or other similar tools from processing the generated file. ```rs copy filename="src/main.rs" let router = ::new() .config( Config::new() // This text is added to the start of the exported Typescript file. .set_ts_bindings_header("/* eslint-disable */") )) .query("version", |t| t(|_, _: ()| env!("CARGO_PKG_VERSION"))) .build(); ``` ## TODO [#todo] * Request context (show putting data into it) * Show how to achieve websocket-scoped context via middleware (we `Clone` the context) * Type exporting * Explain `merge` and `nest` * "Should I use a query or a mutation?" as tip or even Accordion? # rspc TODO # Specta Core TODO # Build a Framework TODO # Specta TODO # Getting started [#getting-started] TODO TODO: Somewhere discuss runtime exporting vs unit test vs build script vs etc. # Languages TODO # Tauri Specta TODO