Skip to main content
Version: 2.x

@zenstackhq/trpc

The @zenstackhq/trpc plugin generates a tRPC router to make database CRUD calls. You can use it directly as your main router or merge it with other routers to create a more complex setup. The router syntactically mirrors the APIs of a standard Prisma client, including the function names and shapes of parameters (hooks directly use types generated by Prisma).

info

The @zenstackhq/trpc plugin depends on the @core/zod plugin to generate Zod schemas for the routers' input, and will enable that plugin automatically if it's not already enabled.

This plugin is based on prisma-trpc-generator. Thanks to Omar Dulaimi for making this happen!

Installation

npm install --save-dev @zenstackhq/trpc

Options

NameTypeDescriptionRequiredDefault
outputStringOutput directory (relative to the path of ZModel)Yes
versionStringtRPC version to target - "v10" or "v11". "v11" support is still in preview.Nov10
importCreateRouterStringOnly needed when "version" is set to "v11". This option tells the code generator where to import the createTRPCRouter tRPC router factory object from.Yes (v11)
importProcedureStringOnly needed when "version" is set to "v11". This option tells the code generator where to import the procedure tRPC procedure factory object from.Yes (v11)
generateModelActionsString, String[]Array or comma separated string for actions to generate for each model: create, findUnique, update, etc.NoAll supported Prisma actions
generateClientHelpersString, String[]Array or comma separated string for the types of client helpers to generate. Supported values: "react", "next", or "nuxt". See here for more details.No
zodSchemasImportStringImport path for the generated zod schemas. The trpc plugin relies on the @core/zod plugin to generate zod schemas for input validation. If you set a custom output location for the zod schemas, you can use this option to override the import path.No@zenstackhq/runtime/zod
info

When @core/zod plugin is automatically enabled by the @zenstackhq/trpc plugin, if the @zenstackhq/trpc plugin has a generateModels option specified, it'll be carried over to the @core/zod plugin as well.

Dependencies

Details

Preparing tRPC Context

The generated tRPC routers require a prisma field in the context and you need to make sure to include it when creating the context. You can use any PrismaClient instance for it, but most likely you want to use one created with enhance so that the client enforces access control policies.

export const createContext = async ({ req, res }: CreateNextContextOptions) => {
const session = await getServerAuthSession({ req, res });
return {
session,
// use access-control-enabled db client
prisma: enhance(prisma, { user: session?.user }),
};
};

Client Helpers

TRPC relies on TypeScript's type inference to provide the client-side API call signatures. This is very neat and powerful, but it has some limitations regarding generic APIs. For example, one of the best features of Prisma's API is its output type automatically adapts to the input's shape. e.g.:

const post = await prisma.post.findFirst({ include: { author: true } });

For the code above, the post variable is smartly inferred to be of type Post & { author: User }. However, if you wrap it with tRPC, such typing adaptivity is lost:

// trpc router
const router = t.router({
findFirst: t.procedure
.input(PostInputSchema)
.query(({ ctx, input }) => ctx.prisma.findFirst(input))
})

// client side
const { data } = trpc.post.findFirst({ include: { author: true } });

Now the data field has a fixed Post type, even though at runtime it carries the extra author field.

To mitigate this limitation, ZenStack's tRPC plugin can generate a "type-fixing" helper that re-enables the inference power from the client side. To use it, you need to specify the generateClientHelpers option:

plugin trpc {
provider = '@zenstackhq/trpc'
output = 'server/routers/generated'
generateClientHelpers = 'next'
}

Use value "next" if you're using tRPC with Next.js, or "react" if using it with react-query, or "nuxt" for trpc-nuxt. With that option on, the plugin generates an extra "client" folder containing helpers that do the type fixing. The only code change you need to make is instead of calling the createTRPCNext/createTRPCReact/createTRPCNuxtClient API, you call the one generated instead.

Here's an example of how to use it with Next.js:

import { createTRPCNext } from 'server/routers/generated/client/next';
export const trpc = createTRPCNext<AppRouter>({
...
});
info

If your generated trpc procedures are installed at a non-top-level path, you can specify the path with the second type parameter of the createTRPCNext, createTRPCReact, or createTRPCNuxtClient function.

// if the generated procedures are installed at `/crud` under the router tree
export const trpc = createTRPCNext<AppRouter, 'crud'>({
...
});

Example

Here's an example with a blogging app:

/schema.zmodel
plugin trpc {
provider = '@zenstackhq/trpc'
output = 'server/routers/generated'
generateModelActions = 'create,update,findUnique,findMany'
}

model User {
id String @id @default(cuid())
email String
posts Post[]

// everyone can signup, and user profile is also publicly readable
@@allow('create,read', true)
}

model Post {
id String @id @default(cuid())
title String
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId String

// author has full access
@@allow('all', auth() == author)

// logged-in users can view published posts
@@allow('read', auth() != null && published)
}
/server/routers/_app.ts
import { createRouter as createCRUDRouter } from './generated/routers';
import { initTRPC } from '@trpc/server';
import { type Context } from '../context';

const t = initTRPC.context<Context>().create();

export const appRouter = createCRUDRouter(t.router, t.procedure);
export type AppRouter = typeof appRouter;

Check out the Using With tRPC guide for more details about using ZenStack in a tRPC project.

Comments
Feel free to ask questions, give feedback, or report issues.

Don't Spam


You can edit/delete your comments by going directly to the discussion, clicking on the 'comments' link below