All topics
Frontend · Learning hub

Trpc notes for developers

Master Trpc with a curated set of 1 developer notes — core concepts, patterns, and interview prep. Maintained by the DevRecall team.

Save this stack to your DevRecallMore Frontend notes
Trpc

tRPC Setup & Usage

tRPC Setup & Usage Server Setup (Next.js App Router) // server/trpc.ts — core tRPC instance import { initTRPC, TRPCError } from '@trpc/server'; import { auth }

tRPC Setup & Usage

Server Setup (Next.js App Router)

// server/trpc.ts — core tRPC instance
import { initTRPC, TRPCError } from '@trpc/server';
import { auth } from '@clerk/nextjs/server';
import superjson from 'superjson';
import { z } from 'zod';

export const createTRPCContext = async () => {
  const { userId } = auth();
  return { userId };
};

const t = initTRPC.context<typeof createTRPCContext>().create({
  transformer: superjson,
});

export const router = t.router;
export const publicProcedure = t.procedure;
export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
  if (!ctx.userId) throw new TRPCError({ code: 'UNAUTHORIZED' });
  return next({ ctx: { ...ctx, userId: ctx.userId } });
});

// server/routers/post.ts
export const postRouter = router({
  list: publicProcedure
    .input(z.object({ limit: z.number().default(20), cursor: z.string().optional() }))
    .query(async ({ input }) => {
      const posts = await db.post.findMany({
        take: input.limit + 1,
        cursor: input.cursor ? { id: input.cursor } : undefined,
        orderBy: { createdAt: 'desc' },
      });
      const nextCursor = posts.length > input.limit ? posts.pop()!.id : undefined;
      return { posts, nextCursor };
    }),

  byId: publicProcedure
    .input(z.string())
    .query(async ({ input: id }) => {
      const post = await db.post.findUniqueOrThrow({ where: { id } });
      return post;
    }),

  create: protectedProcedure
    .input(z.object({ title: z.string().min(1), body: z.string() }))
    .mutation(async ({ input, ctx }) => {
      return db.post.create({ data: { ...input, authorId: ctx.userId } });
    }),

  delete: protectedProcedure
    .input(z.string())
    .mutation(async ({ input: id, ctx }) => {
      const post = await db.post.findUniqueOrThrow({ where: { id } });
      if (post.authorId !== ctx.userId) throw new TRPCError({ code: 'FORBIDDEN' });
      return db.post.delete({ where: { id } });
    }),
});

// server/routers/_app.ts
import { userRouter } from './user';
import { postRouter } from './post';

export const appRouter = router({ user: userRouter, post: postRouter });
export type AppRouter = typeof appRouter;

API Route & Client

// app/api/trpc/[trpc]/route.ts
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { appRouter } from '@/server/routers/_app';
import { createTRPCContext } from '@/server/trpc';

const handler = (req: Request) =>
  fetchRequestHandler({
    endpoint: '/api/trpc',
    req,
    router: appRouter,
    createContext: createTRPCContext,
  });

export { handler as GET, handler as POST };

// lib/trpc/client.ts — React Query client
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '@/server/routers/_app';
export const trpc = createTRPCReact<AppRouter>();

// app/providers.tsx
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { trpc } from '@/lib/trpc/client';
import { httpBatchLink } from '@trpc/client';
import superjson from 'superjson';

export function TRPCProvider({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(() => new QueryClient());
  const [trpcClient] = useState(() =>
    trpc.createClient({
      links: [httpBatchLink({ url: '/api/trpc', transformer: superjson })],
    })
  );
  return (
    <trpc.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
    </trpc.Provider>
  );
}

Client Usage

'use client';
import { trpc } from '@/lib/trpc/client';

// Query
function PostList() {
  const { data, isLoading, error } = trpc.post.list.useQuery({ limit: 20 });
  if (isLoading) return <Spinner />;
  if (error) return <Error message={error.message} />;
  return <>{data.posts.map(p => <PostCard key={p.id} post={p} />)}</>;
}

// Infinite query
const { data, fetchNextPage, hasNextPage } = trpc.post.list.useInfiniteQuery(
  { limit: 20 },
  { getNextPageParam: (last) => last.nextCursor }
);

// Mutation with optimistic update
function DeletePost({ id }: { id: string }) {
  const utils = trpc.useUtils();
  const { mutate, isPending } = trpc.post.delete.useMutation({
    onSuccess: () => {
      utils.post.list.invalidate();   // refetch list
    },
    onMutate: async (id) => {
      await utils.post.list.cancel();
      const prev = utils.post.list.getData();
      utils.post.list.setData(undefined, old =>
        old ? { ...old, posts: old.posts.filter(p => p.id !== id) } : old
      );
      return { prev };
    },
    onError: (_err, _id, ctx) => {
      if (ctx?.prev) utils.post.list.setData(undefined, ctx.prev);
    },
  });
  return <button onClick={() => mutate(id)} disabled={isPending}>Delete</button>;
}

Keep your Trpc knowledge sharp.

Save this stack to your personal DevRecall — add your own notes, track what you're learning, and share what you know with the community.

Get started — free forever