Critical Patterns
Project Structure (REQUIRED)
.
├── src
│ ├── pages
│ │ ├── _app.tsx # add createTRPCNext setup here
│ │ ├── api
│ │ │ └── trpc
│ │ │ └── [trpc].ts # tRPC HTTP handler
│ │ ├── server
│ │ │ ├── routers
│ │ │ │ ├── _app.ts # main app router
│ │ │ │ ├── [feature].ts # feature-specific routers
│ │ │ │ └── [...]
│ │ │ ├── context.ts # create app context
│ │ │ └── trpc.ts # procedure helpers
│ │ └── utils
│ │ └── trpc.ts # typesafe tRPC hooks
Server-Side Setup (REQUIRED)
// server/trpc.ts - Initialize backend (once per backend) import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
export const router = t.router; export const publicProcedure = t.procedure;
Router Definition (REQUIRED)
// server/routers/_app.ts import { z } from 'zod'; import { router, publicProcedure } from '../trpc';
export const appRouter = router({
greeting: publicProcedure
.input(z.object({ name: z.string() }))
.query(({ input }) => {
return Hello ${input.name};
}),
});
// Export type definition, NOT the router itself! export type AppRouter = typeof appRouter;
Client-Side Setup (REQUIRED)
// utils/trpc.ts import { httpBatchLink } from '@trpc/client'; import { createTRPCNext } from '@trpc/next'; import type { AppRouter } from '../server/routers/_app';
function getBaseUrl() {
if (typeof window !== 'undefined') return '';
if (process.env.VERCEL_URL) return https://${process.env.VERCEL_URL};
return http://localhost:${process.env.PORT ?? 3000};
}
export const trpc = createTRPCNext<AppRouter>({
config() {
return {
links: [
httpBatchLink({
url: ${getBaseUrl()}/api/trpc,
}),
],
};
},
ssr: false,
});
Decision Tree
Need public endpoint? → Use publicProcedure Need auth? → Use protectedProcedure with middleware Need validation? → Use Zod in .input() Need caching? → Use React Query options Need complex types? → Use SuperJSON transformer
Code Examples
Organize Routers by Feature
// server/routers/user.ts export const userRouter = router({ list: publicProcedure.query(() => { /* ... / }), byId: publicProcedure.input(z.string()).query(({ input }) => { / ... / }), create: publicProcedure.input(/ ... /).mutation(({ input }) => { / ... */ }), });
// server/routers/_app.ts import { userRouter } from './user'; import { postRouter } from './post';
export const appRouter = router({ user: userRouter, post: postRouter, });
Middleware for Auth
const isAuthed = t.middleware(({ next, ctx }) => { if (!ctx.user) { throw new TRPCError({ code: 'UNAUTHORIZED' }); } return next({ ctx: { user: ctx.user } }); });
const protectedProcedure = t.procedure.use(isAuthed);
Error Handling
import { TRPCError } from '@trpc/server';
publicProcedure
.input(z.string())
.query(({ input }) => {
const user = getUserById(input);
if (!user) {
throw new TRPCError({
code: 'NOT_FOUND',
message: User with id ${input} not found,
});
}
return user;
});
Data Transformers (SuperJSON)
import { initTRPC } from '@trpc/server'; import superjson from 'superjson';
const t = initTRPC.create({ transformer: superjson, });
React Query Integration
function UserProfile({ userId }: { userId: string }) { const { data, isLoading, error } = trpc.user.byId.useQuery(userId);
if (isLoading) return <div>Loading...</div>; if (error) return <div>Error: {error.message}</div>;
return <div>{data.name}</div>; }
Context Creation
// server/context.ts import { inferAsyncReturnType } from '@trpc/server'; import * as trpcNext from '@trpc/server/adapters/next';
export async function createContext({ req, res, }: trpcNext.CreateNextContextOptions) { const user = await getUser(req); return { req, res, prisma, user }; }
export type Context = inferAsyncReturnType<typeof createContext>;
Procedure Types
export const publicProcedure = t.procedure; export const protectedProcedure = t.procedure.use(isAuthed); export const adminProcedure = t.procedure.use(isAdmin);
Performance: Batching & Prefetching
// Client batching
httpBatchLink({
url: ${getBaseUrl()}/api/trpc,
maxURLLength: 2083,
})
// Prefetching in Next.js export async function getStaticProps() { const ssg = createServerSideHelpers({ router: appRouter, ctx: {}, });
await ssg.post.byId.prefetch('1');
return { props: { trpcState: ssg.dehydrate() }, revalidate: 1, }; }
Version Compatibility
-
tRPC v11
-
TypeScript >= 5.7.2
-
Strict mode required ("strict": true )