WARNING: These docs are a work in progress! Some sections are incomplete or incorrect.

Router

A router is responsible for collecting up 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.

TCtx

The type of the request context. This is usually a struct containing state coming from the webserver such as a database connection and user session.

For example:

pub struct MyCtx {
    // The database connection. Prisma Client Rust shown here.
    db: Arc<PrismaClient>,
 
    // The session_id which could be extracted from HTTP cookies.
    session_id: Option<String>,
 
    // 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<Mutex<String>>,
}

Constructing an rspc router with a specific context type is done as following.

// Create a router with the default `TCtx` of `()`
let router = <Router>::new();
 
// Create a router with a custom `TCtx` type
let router = Router::<MyCtx>::new();

TCtx is super powerful in rspc because 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.

pub struct AuthenticatedCtx {
    db: Arc<PrismaClient>, // Your database connection
    user: User, // Your user model in Rust.
}
 
// We define a router with the `TCtx` type set to `MyCtx`
let router = Router::<MyCtx>::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

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.

let router = <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?

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

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)

// This could be defined in another file or even another crate
let users_router = <Router>::new()
        .query("list", |t| t(|ctx, input: ()| vec![] as Vec<()>));
 
let router = <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

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.

let router = <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, Prettier or other similar tools from processing the generated file.

let router = <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

  • 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?
Edit on GitHub

Last updated on

On this page