December 5, 2024
How to set up tRPC with Next.js App Router and TanStack Query for end-to-end type safety without code generation.

REST and GraphQL both require you to define a schema, generate types, and keep them in sync with your server. tRPC skips that entirely — your TypeScript types are the contract. If the server changes a procedure, the client breaks at compile time, not at runtime.
Define your procedures in a router file. Each procedure is a function that validates its input with Zod and returns typed data.
// services/trpc/routers/posts.ts
import { z } from "zod";
import { publicProcedure, router } from "../init";
export const postsRouter = router({
list: publicProcedure.query(async () => {
return db.query.posts.findMany({ orderBy: desc(posts.createdAt) });
}),
byId: publicProcedure
.input(z.object({ id: z.string() }))
.query(async ({ input }) => {
return db.query.posts.findFirst({ where: eq(posts.id, input.id) });
}),
create: publicProcedure
.input(z.object({ title: z.string().min(1), body: z.string() }))
.mutation(async ({ input }) => {
return db.insert(posts).values(input).returning();
}),
});With the useTRPC hook and TanStack Query, data fetching is declarative and fully typed.
"use client";
const PostList = () => {
const trpc = useTRPC();
const { data, isPending } = useQuery(trpc.posts.list.queryOptions());
if (isPending) return <Skeleton className="h-40 w-full" />;
return (
<ul>
{data?.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
};"use client";
const CreatePost = () => {
const trpc = useTRPC();
const createPost = useMutation(trpc.posts.create.mutationOptions());
return (
<Button
onClick={() => createPost.mutate({ title: "Hello", body: "World" })}
disabled={createPost.isPending}
>
{createPost.isPending ? <Spinner /> : "Create post"}
</Button>
);
};Your laboratory instruments should serve you, not the other way around. We're happy to help you.