trpc

SKILL.md

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)
Weekly Installs
6
First Seen
Jan 26, 2026
Installed on
github-copilot5
gemini-cli4
codex4
cursor4
opencode4
cline3