# Trpc Patterns

> When to activate: tRPC, end-to-end type safety, RPC, routers, procedures, middleware, subscriptions, React Query integration

- Skill: `mattakushi432/trpc-patterns` (Agent Skill)
- Install (CLI): `npx skillmds@latest add mattakushi432/trpc-patterns`
- Raw SKILL.md: https://api.skillmd.com/api/skills/mattakushi432/trpc-patterns/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: Mattakushi432 (https://skillmd.com/u/mattakushi432)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/mattakushi432/trpc-patterns

---


# tRPC Patterns

## Server Setup (Next.js App Router)
```ts
// 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
```ts
// 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
```ts
// 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
```ts
// 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
```ts
// 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
```tsx
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 onClick={() => createUser.mutate({ name: 'Alice', email: 'a@b.com' })}>
        Add
      </button>
    </>
  )
}
```

## Middleware Pattern
```ts
// 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)
```ts
// 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) },
})
```

