Auto-generate type-safe tRPC routers from ZenStack V3 schemas
npm install zenstack-trpcAuto-generate fully type-safe tRPC routers from ZenStack V3 schemas.
- Zero codegen - Router generated at runtime from schema metadata
- Full type inference - Input AND output types from your ZenStack schema
- Dynamic result typing - include/select options reflected in return types
- Zod validation - Runtime input validation built-in
- All CRUD operations - findMany, findUnique, create, update, delete, and more
- Standard tRPC - Works with all tRPC adapters and clients
``bash`
npm install zenstack-trpc @trpc/server @zenstackhq/orm zod
`prisma
// schema.zmodel
model User {
id String @id @default(cuid())
email String @unique
name String?
posts Post[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Post {
id String @id @default(cuid())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
`
`bash`
npx zenstack generate
`typescript
// server/trpc.ts
import { initTRPC } from "@trpc/server";
import { ZenStackClient } from "@zenstackhq/orm";
import { schema } from "./zenstack/schema.js";
import { createZenStackRouter } from "zenstack-trpc";
// Create your database client
const db = new ZenStackClient(schema, {
dialect: yourDialect, // Kysely dialect (SQLite, PostgreSQL, MySQL, etc.)
});
// Create your tRPC instance
const t = initTRPC.context<{ db: typeof db }>().create();
// Generate the router
export const appRouter = createZenStackRouter(schema, t);
export type AppRouter = typeof appRouter;
`
`typescript
// client.ts
import { createTRPCClient, httpBatchLink } from "@trpc/client";
import type { AppRouter } from "./server/trpc.js";
const client = createTRPCClient
links: [httpBatchLink({ url: "http://localhost:3000/trpc" })],
});
// All operations are fully typed!
const users = await client.user.findMany.query();
// Include relations
const usersWithPosts = await client.user.findMany.query({
include: { posts: true },
});
// Create with validation
const user = await client.user.create.mutate({
data: { email: "alice@example.com", name: "Alice" },
});
// Update
await client.user.update.mutate({
where: { id: user.id },
data: { name: "Alice Smith" },
});
`
For each model in your schema, the following procedures are generated:
| Queries | Mutations |
|---------|-----------|
| findMany | create |
| findUnique | createMany |
| findFirst | update |
| count | updateMany |
| aggregate | upsert |
| groupBy | delete |
| | deleteMany |
Generates a tRPC router from a ZenStack schema.
`typescript
import { initTRPC } from "@trpc/server";
import { createZenStackRouter } from "zenstack-trpc";
const t = initTRPC.context<{ db: any }>().create();
const appRouter = createZenStackRouter(schema, t);
`
Pass a custom base procedure to apply middleware (e.g., auth) to all generated routes:
`typescript
import { TRPCError } from "@trpc/server";
const protectedProcedure = t.procedure.use(({ ctx, next }) => {
if (!ctx.user) throw new TRPCError({ code: "UNAUTHORIZED" });
return next({ ctx });
});
const appRouter = createZenStackRouter(schema, t, {
procedure: protectedProcedure,
});
`
Type helper for server-side caller with full type inference, including dynamic include/select result typing.
`typescript
import type { TypedRouterCaller } from "zenstack-trpc";
import type { SchemaType } from "./zenstack/schema.js";
const caller = appRouter.createCaller({ db }) as TypedRouterCaller
// Return type dynamically includes the posts relation!
const usersWithPosts = await caller.user.findMany({ include: { posts: true } });
// Type: (User & { posts: Post[] })[]
`
Type helpers for the generated router structure:
- ZenStackRouter and createCallerZenStackRouterRecord
- - Just the procedure map (useful for type composition)
`typescript
import type { ZenStackRouter, ZenStackRouterRecord } from "zenstack-trpc";
import type { SchemaType } from "./zenstack/schema.js";
// The full router type
type MyRouter = ZenStackRouter
// Just the procedures (for advanced type manipulation)
type Procedures = ZenStackRouterRecord
// { user: { findMany: ..., create: ..., ... }, post: { ... }, ... }
`
The library provides a composable type system for adding full include/select type inference to tRPC clients. This solves tRPC's limitation where generic type information is lost during type inference.
The system uses three composable parts:
1. WithZenStack
2. WithReact<...> / WithClient<...> - Adapter that transforms to React hooks or vanilla client types
3. typedClient
`typescript
import { createTRPCReact } from "@trpc/react-query";
import { typedClient, type WithZenStack, type WithReact } from "zenstack-trpc";
import type { AppRouter } from "./server/trpc.js";
import type { SchemaType } from "./zenstack/schema.js";
// Compose your types
type Typed = WithReact
// Apply to client
const _trpc = createTRPCReact
export const trpc = typedClient
// Now includes are fully typed!
const { data } = trpc.user.findMany.useQuery({
include: { posts: true }
});
// data is typed as (User & { posts: Post[] })[] | undefined
`
For vanilla tRPC clients, use WithClient:
`typescript
import { createTRPCClient, httpBatchLink } from "@trpc/client";
import { typedClient, type WithZenStack, type WithClient } from "zenstack-trpc";
type Typed = WithClient
const _client = createTRPCClient
links: [httpBatchLink({ url: "http://localhost:3000/trpc" })],
});
export const client = typedClient
const usersWithPosts = await client.user.findMany.query({
include: { posts: true }
});
// Type: (User & { posts: Post[] })[]
`
When merging the ZenStack router with other routers, you need to cast it to AnyRouter for tRPC compatibility:
`typescript
import { initTRPC } from "@trpc/server";
import type { AnyRouter } from "@trpc/server";
import { createZenStackRouter } from "zenstack-trpc";
import { schema } from "./zenstack/schema.js";
const t = initTRPC.context<{ db: any }>().create();
// Create the ZenStack router and cast for tRPC compatibility
const generatedRouter = createZenStackRouter(schema, t) as unknown as AnyRouter;
// Merge with other routers
export const appRouter = t.router({
admin: adminRouter,
auth: authRouter,
generated: generatedRouter,
});
`
On the client side, include the path in WithZenStack to get full type inference:
`typescript
import { createTRPCReact } from "@trpc/react-query";
import { typedClient, type WithZenStack, type WithReact } from "zenstack-trpc";
import type { AppRouter } from "./server/trpc.js";
import type { SchemaType } from "./zenstack/schema.js";
// Single level nesting:
type Typed = WithReact
const _trpc = createTRPCReact
export const trpc = typedClient
// Multi-level nesting (dot notation):
type Typed = WithReact
export const trpc = typedClient
// Now you can use:
// trpc.generated.user.findMany.useQuery({ include: { posts: true } }) // fully typed
// trpc.auth.login.useMutation() // other routers unaffected
// trpc.useUtils().generated.user.findMany.invalidate() // typed query utils
`
The composable architecture allows third parties to create custom adapters. An adapter transforms WithZenStack into framework-specific types:
`typescript
import type { WithZenStack, TypedTRPCReact } from "zenstack-trpc";
// Example: Custom adapter for a hypothetical framework
type WithMyFramework
T extends WithZenStack
? { readonly __types: MyFrameworkTypes; readonly __path: P }
: never;
// Usage:
type Typed = WithMyFramework
const client = typedClient
`
For advanced use cases, you can access the underlying types directly:
`typescript
import type { TypedTRPCClient, TypedTRPCReact } from "zenstack-trpc";
// Manual casting
const client = _client as unknown as TypedTRPCClient
const trpc = _trpc as unknown as TypedTRPCReact
`
Access the generated Zod schemas for custom validation:
`typescript
import { createModelSchemas, createWhereSchema } from "zenstack-trpc";
const userSchemas = createModelSchemas(schema, "User");
`
- Node.js >= 18
- ZenStack V3 (@zenstackhq/orm >= 3.0.0)
- tRPC >= 11.0.0
- Zod >= 3.0.0
- @trpc/react-query >= 11.0.0@tanstack/react-query` >= 5.0.0
-
MIT