Using With tRPC
tRPC is a fantastic library that magically turns server-side procedures into client-callable functions without requiring you to provide any formal contract. The popular T3 stack promotes the combo of Prisma + tRPC for achieving type safety from your frontend all the way down to the database.
ZenStack makes things even easier by automatically generating tRPC routers from the ZModel schema. You can use the generated routers together with an enhanced Prisma client; since the Prisma client has the ability to enforce access policies, there is no need to implement authorization code anymore.
Details
1. Initializing the project
If you haven't initialized your tRPC project with ZenStack, run the following command to do it:
npx zenstack@latest init
The "init" command does the following things for you:
- Install Prisma if it's not already installed.
- Install the
zenstack
CLI package as a dev dependency. - Install the
@zenstackhq/runtime
package - used for enhancingPrismaClient
at the runtime. - Copy the
prisma/schema.prisma
file toschema.zmodel
if it exists; otherwise, create a new templateschema.zmodel
file.
You can always manually complete the steps above if you have a special project setup that the "init" command doesn't work with.
After the initialization, please remember that you should edit the schema.zmodel
moving forward. The prisma/schema.prisma
file will be automatically regenerated when you run zenstack generate
.
2. Installing the tRPC plugin
You can enable tRPC router generation with the @zenstackhq/trpc
plugin.
First install the trpc package:
npm install -D @zenstackhq/trpc@latest
Then, add the following plugin configuration to your ZModel schema. Note that the configuration is different for tRPC v10 and v11:
- tRPC v10
- tRPC v11 (preview)
plugin trpc {
provider = '@zenstackhq/trpc'
output = 'src/server/routers/generated'
}
To use with tRPC v11, you need to explicitly set the "version" option to "v11" and add two extra options to let ZenStack know where to import the tRPC's router factory (named createTRPCRouter
) and procedure factory (named procedure
) from. For example, with the following settings:
plugin trpc {
provider = '@zenstackhq/trpc'
output = 'src/server/routers/generated'
version = 'v11'
importCreateRouter = "../../trpc"
importProcedure = "../../trpc"
}
The generated tRPC routers will contain code like the following:
import { createTRPCRouter } from "../../trpc";
import { procedure } from "../../trpc";
export default function createRouter() {
return createTRPCRouter({
findMany: procedure.input(...).query(...) => ...
...
});
}
tRPC v11 removed several public TypeScript types that our previous way of code generation relied on. To get around this, we have to resort to letting the user tell us where to import the factories from so we can use them without explicitly typing them.
3. Setting up the tRPC context
Usually in your tRPC project, you have a function to create a tRPC context. You need to make sure the context contains a prisma
field that is an instance of Prisma client. The generated tRPC routers use that to talk to the database.
For most of the cases you should use a Prisma client that's "enhanced" by ZenStack so that the CRUD operations are guarded by the access policies defined in the ZModel schema. Here's a quick example with Next.js:
import { enhance } from '@zenstackhq/runtime';
import { prisma } from './db';
import { getSessionUser } from './auth';
export const createContext = async ({ req, res }: CreateNextContextOptions) => {
return {
...,
// use access-control-enabled Prisma client
prisma: await enhance(prisma, { user: getSessionUser(req, res) }),
};
};
4. Using the generated routers
Now run the zenstack
CLI to generate artifacts:
npx zenstack generate
You should find a bunch of tRPC routers generated in the output folder, one per each data model. A createRouter
helper function is also generated, which returns a router instance for all models. You can use it as your top-level tRPC router, or merge it with other routers to form a more complex setup.
- tRPC v10
- tRPC v11
import { createRouter } from './generated/routers';
const t = initTRPC.context<Context>().create();
export const appRouter = createRouter(t.router, t.procedure);
export type AppRouter = typeof appRouter;
import { createRouter } from './generated/routers';
const t = initTRPC.context<Context>().create();
export const procedure = t.publicProcedure;
export const createTRPCRouter = t.createRouter;
...
export const appRouter = createRouter();
export type AppRouter = typeof appRouter;
NOTE: The ZenStack trpc plugin is based on the awesome work by Omar Dulaimi.
Please refer to the @zenstackhq/trpc plugin documentation for more details. You can also use the sample-todo-trpc project as a reference.