Extra
rspc Integration

rspc Integration

Prisma Client Rust has first-class support for rspc (opens in a new tab), a server-agnostic router that can automatically generate TypeScript bindings.

The examples use the following schema:

model Post {
    id        String   @id @default(cuid())
    title     String
 
    comments Comment[]
}
 
model Comment {
    id        String   @id @default(cuid())
    content   String
 
    post   Post   @relation(fields: [postID], references: [id])
    postID String
}

Setup

When the rspc feature is enabled for both prisma-client-rust and prisma-client-rust-cli, all data types generated by PCR will implement specta::Type, and a From<queries::Error> implementation is generated for rspc::Error.

Queries

The following is possible when combining PCR and rspc:

use rspc::Router;
 
fn main() -> Router {
    Router::<prisma::PrismaClient>::new()
        .query("posts", |db, _: ()| async move {
            let posts = db
                .post()
                .find_many(vec![])
                .exec()
                .await?;
 
            Ok(posts)
        })
        .build()
}

When TypeScript is generated it will look something like this:

export type Operations = {
    queries: { key: ["posts"], result: Array<Post> }
};
 
export interface Post { id: string, title: string }

This also works for select & include:

use rspc::Router;
 
fn main() -> Router {
    Router::<prisma::PrismaClient>::new()
        .query("posts", |db, _: ()| async move {
            let posts = db
                .post()
                .find_many(vec![])
                .select(post::select!({
                    id
                    title
                }))
                .exec()
                .await?;
 
            Ok(posts)
        })
        .build()
}

The generated TypeScript will look something like this:

export type Operations = {
    queries: { key: ["posts"], result: Array<{ id: string, title: string}> }
};

As of 0.6.4, dedicated types will be generated for named include/selects. These are only provided for extenuating cirsumstances though, and it is instead recommended that you use helpers from rspc such as rspc.inferProcedureResult to access procedure results instead of relying on the named types.

Relation types

It is important to note that the types generated for models do not have fields for relations. This is done to provide a better experience when using include, at the expense of a worse experience using with/fetch for fetching relations.

This tradeoff was made as a result of our experience building Spacedrive (opens in a new tab). Instead of all our TypeScript functions having to verify at runtime whether a relation had been loaded or not, as is the case when using with/fetch, each function has to explicitly specify the relations it wants loaded:

import { User, Post } from "./exported-types"
 
// Only recieves scalar fields of User
function needsOneUser(user: User) {}
 
// Receives scalar fields + posts relation of User
function needsOneUserWithPosts(user: User & { posts: Post[] }) {}

There are a few downsides to this approach:

  • It is necessary to know the name and type of the relation you are including in TypeScript (though this is being worked on)
  • with/fetch can be modified dynamically
  • with/fetch provides better editor autocomplete

However, we've found that these downsides are outweighed by the benefits of having full type-safety across your backend and frontend.

Error Handling

Since rspc::Error can implement From<queries::Error>, all of Rust's regular error handling conventions are available.

The ? operator can be used to extract success values and return early if an error is encountered, automatically converting the Prisma error to an rspc error. This has the problem of needing to wrap the extracted value in Ok before return from a resolver.

Another approach is adding .map_err(Into::into) before returning from a resolver. This causes the Prisma error to be converted to an rspc error while keeping the Result intact, as demonstrated below:

use rspc::Router;
 
fn main() -> Router {
    Router::<prisma::PrismaClient>::new()
        .query("posts", |db, _: ()| async move {
            db
                .post()
                .find_many(vec![])
                .exec()
                .await
                .map_err(Into::into)
        })
        .build()
}

For specifics on error handling in rpsc, see the documentation (opens in a new tab).