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>;
}