| name | cross-package-types |
| description | Type flow patterns for tRPC + Prisma monorepos. Covers AppRouter type inference across shared, server, and client packages. Use when setting up types, fixing type errors across packages, or understanding the type chain. |
Skill: Cross-Package Types (tRPC + Prisma)
Defines how TypeScript types flow through a monorepo with tRPC and Prisma.
Type Flow
Prisma Schema → Prisma Client Types → Zod Schemas → tRPC Routers → Client Type Inference
Package Responsibilities
| Package | Owns | Exports |
|---|
| shared | Zod schemas, enums, constants | Schemas for both server and client |
| server | tRPC routers, Prisma client | AppRouter type |
| client | React components, tRPC hooks | Consumes AppRouter type |
Key Principles
- Prisma is the single source of truth — all types derive from the Prisma schema
- Zod schemas derive from Prisma types — use
.pick(), .omit(), .extend() on base schemas
- AppRouter types flow from server to client — never import server code in client
- No circular dependencies — shared → server → client (one direction only)
Shared Package
The shared package has two entry points:
{
"exports": {
"./server": "./src/server/index.ts",
".": "./src/index.ts"
}
}
Schema Derivation
import { z } from 'zod';
export const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
name: z.string().min(1),
role: z.enum(['ADMIN', 'USER', 'VIEWER']),
createdAt: z.date(),
updatedAt: z.date(),
});
const PRISMA_AUTO_FIELDS = ['id', 'createdAt', 'updatedAt'] as const;
export const CreateUserSchema = UserSchema.omit(
Object.fromEntries(PRISMA_AUTO_FIELDS.map(f => [f, true])) as Record<string, >
);
= .();
= .({
: ,
: ,
: ,
});
= z.< >;
= z.< >;
= z.< >;
Server Package
tRPC Router Pattern
import { router, protectedProcedure } from '../trpc';
import { CreateUserSchema, UpdateUserSchema } from '@myapp/shared';
import { prisma } from '@myapp/shared/server';
export const userRouter = router({
list: protectedProcedure.query(async () => {
return prisma.user.findMany();
}),
create: protectedProcedure
.input(CreateUserSchema)
.mutation(async ({ input }) => {
return prisma.user.create({ data: input });
}),
update: protectedProcedure
.input(z.object({ id: z.string().uuid(), data: UpdateUserSchema }))
.mutation(async ({ input }) => {
return prisma.user.update({ where: { id: input.id }, data: input.data });
}),
});
AppRouter Export
import { router } from '../trpc';
import { userRouter } from './user';
export const appRouter = router({
user: userRouter,
});
export type AppRouter = typeof appRouter;
Client Package
Type-Safe tRPC Client
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '@myapp/server';
export const trpc = createTRPCReact<AppRouter>();
Usage in Components
import { trpc } from '../utils/trpc';
export function UserList() {
const { data: users } = trpc.user.list.useQuery();
}
Common Mistakes
- ❌ Importing server code in client (breaks browser bundle)
- ❌ Defining types manually instead of deriving from Prisma/Zod
- ❌ Circular imports between packages
- ❌ Using
any to silence cross-package type errors
- ✅ Use
type imports: import type { AppRouter } from '@myapp/server'
- ✅ Derive all schemas from Prisma base types
- ✅ Use shared package for browser-safe code