Codapult includes a full tRPC v11 setup with TanStack React Query and superjson serialization. tRPC provides end-to-end type safety — your client code knows the exact shape of every request and response without code generation.
File Structure
| Path | Description |
|---|---|
src/lib/trpc/init.ts | Router factory, procedure levels (publicProcedure, protectedProcedure, adminProcedure) |
src/lib/trpc/routers/ | Domain routers — user.ts, billing.ts, notifications.ts |
src/lib/trpc/server.ts | Server-side caller and prefetch() helper |
src/lib/trpc/client.ts | Client-side hooks (useTRPC, TRPCProvider, HydrateClient) |
src/app/api/trpc/[trpc]/route.ts | HTTP endpoint at /api/trpc |
Procedure Levels
| Level | Auth Required | Use Case |
|---|---|---|
publicProcedure | No | Public data (plans, features) |
protectedProcedure | Yes | User-scoped data (profile, settings) |
adminProcedure | Yes + admin role | Admin operations (user management) |
Creating a Router
import { z } from 'zod';
import { router, protectedProcedure } from '../init';
import { db } from '@/lib/db';
import { user } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';
export const userRouter = router({
me: protectedProcedure.query(async ({ ctx }) => {
const rows = await db
.select({ id: user.id, name: user.name, email: user.email })
.from(user)
.where(eq(user.id, ctx.session.user.id))
.limit(1);
return rows[0] ?? null;
}),
updateProfile: protectedProcedure
.input(z.object({ name: z.string().min(1).max(100) }))
.mutation(async ({ ctx, input }) => {
await db
.update(user)
.set({ name: input.name, updatedAt: new Date() })
.where(eq(user.id, ctx.session.user.id));
return { success: true };
}),
});
To register a new router, add it to the appRouter in src/lib/trpc/router.ts.
Server-Side Prefetch
Use prefetch() in server components to load data before the page renders, then hydrate the cache on the client:
import { prefetch } from '@/lib/trpc/server';
import { HydrateClient } from '@/lib/trpc/client';
export default async function ProfilePage() {
await prefetch((t) => t.user.me.prefetch());
return (
<HydrateClient>
<ProfileClient />
</HydrateClient>
);
}
Client-Side Usage
'use client';
import { useTRPC } from '@/lib/trpc/client';
import { useQuery, useMutation } from '@tanstack/react-query';
export function ProfileClient() {
const trpc = useTRPC();
const { data: user } = useQuery(trpc.user.me.queryOptions());
const updateMutation = useMutation(trpc.user.updateProfile.mutationOptions());
return (
<div>
<h1>Hello, {user?.name}</h1>
<button onClick={() => updateMutation.mutate({ name: 'New Name' })}>Update</button>
</div>
);
}
Adding a New Router
- Create a file in
src/lib/trpc/routers/(e.g.projects.ts) - Define queries and mutations using the appropriate procedure level
- Register the router in
src/lib/trpc/router.ts - Use it on the client via
trpc.projects.yourProcedure
The types flow automatically — no code generation or manual type imports needed.