tRPC Patterns
Server Setup (Next.js App Router)
// server/trpc.ts
import { initTRPC, TRPCError } from '@trpc/server'
import { getServerSession } from 'next-auth'
import superjson from 'superjson'
import { ZodError } from 'zod'
interface Context {
session: Awaited<ReturnType<typeof getServerSession>> | null
}
const t = initTRPC.context<Context>().create({
transformer: superjson,
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError: error.cause instanceof ZodError ? error.cause.flatten() : null,
},
}
},
})
export const router = t.router
export const publicProc = t.procedure
export const authedProc = t.procedure.use(({ ctx, next }) => {
if (!ctx.session) throw new TRPCError({ code: 'UNAUTHORIZED' })
return next({ ctx: { ...ctx, session: ctx.session } })
})
Router Definition
// server/routers/users.ts
import { z } from 'zod'
import { router, publicProc, authedProc } from '../trpc'
export const usersRouter = router({
list: publicProc
.input(z.object({ page: z.number().default(1), limit: z.number().max(100).default(20) }))
.query(async ({ input }) => {
const { page, limit } = input
const users = await db.user.findMany({ skip: (page - 1) * limit, take: limit })
return users
}),
byId: publicProc
.input(z.object({ id: z.string().cuid() }))
.query(async ({ input }) => {
const user = await db.user.findUnique({ where: { id: input.id } })
if (!user) throw new TRPCError({ code: 'NOT_FOUND' })
return user
}),
create: authedProc
.input(z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
}))
.mutation(async ({ input }) => {
return db.user.create({ data: input })
}),
delete: authedProc
.input(z.object({ id: z.string().cuid() }))
.mutation(async ({ input, ctx }) => {
const user = await db.user.findUnique({ where: { id: input.id } })
if (!user) throw new TRPCError({ code: 'NOT_FOUND' })
if (user.id !== ctx.session.user.id) throw new TRPCError({ code: 'FORBIDDEN' })
return db.user.delete({ where: { id: input.id } })
}),
})
Root Router
// server/routers/_app.ts
import { router } from '../trpc'
import { usersRouter } from './users'
import { postsRouter } from './posts'
export const appRouter = router({
users: usersRouter,
posts: postsRouter,
})
export type AppRouter = typeof appRouter
Next.js Route Handler
// app/api/trpc/[trpc]/route.ts
import { fetchRequestHandler } from '@trpc/server/adapters/fetch'
import { appRouter } from '@/server/routers/_app'
import { createContext } from '@/server/context'
const handler = (req: Request) =>
fetchRequestHandler({
endpoint: '/api/trpc',
req,
router: appRouter,
createContext,
})
export { handler as GET, handler as POST }
Client Setup
// lib/trpc.ts
import { createTRPCReact } from '@trpc/react-query'
import { httpBatchLink } from '@trpc/client'
import superjson from 'superjson'
import type { AppRouter } from '@/server/routers/_app'
export const trpc = createTRPCReact<AppRouter>()
export function TRPCProvider({ children }: { children: React.ReactNode }) {
const queryClient = new QueryClient()
const trpcClient = trpc.createClient({
transformer: superjson,
links: [
httpBatchLink({ url: '/api/trpc' }),
],
})
return (
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
</trpc.Provider>
)
}
Usage in Components
function UserList() {
const { data, isLoading } = trpc.users.list.useQuery({ page: 1 })
const createUser = trpc.users.create.useMutation({
onSuccess: () => utils.users.list.invalidate(),
})
const utils = trpc.useUtils()
if (isLoading) return <Spinner />
return (
<>
{data?.map(u => <UserRow key={u.id} user={u} />)}
<button => createUser.mutate({ name: 'Alice', email: 'a@b.com' })}>
Add
</button>
</>
)
}
Middleware Pattern
// Logging middleware
const loggedProc = t.procedure.use(async ({ path, type, next }) => {
const start = Date.now()
const result = await next()
const ms = Date.now() - start
console.log(`${type} ${path} - ${ms}ms`)
return result
})
// Rate limiting middleware
const rateLimited = t.procedure.use(async ({ ctx, next }) => {
const key = `ratelimit:${ctx.session?.user.id ?? ctx.ip}`
const ok = await rateLimiter.check(key)
if (!ok) throw new TRPCError({ code: 'TOO_MANY_REQUESTS' })
return next()
})
Subscriptions (WebSocket)
// Server
import { observable } from '@trpc/server/observable'
import { EventEmitter } from 'events'
const ee = new EventEmitter()
export const notificationsRouter = router({
onNew: authedProc.subscription(({ ctx }) => {
return observable<Notification>(emit => {
const handler = (n: Notification) => {
if (n.userId === ctx.session.user.id) emit.next(n)
}
ee.on('notification', handler)
return () => ee.off('notification', handler)
})
}),
})
// Client
const { data } = trpc.notifications.onNew.useSubscription(undefined, {
onData(notification) { toast(notification.message) },
})