Type safety from database to UI eliminates an entire class of bugs. Next.js 15 combined with tRPC gives you end-to-end type safety with zero code generation. Here is how to build a modern full-stack application.
Why tRPC?
tRPC lets you call server functions directly from the client with full TypeScript autocompletion. No REST endpoints, no GraphQL schemas, no code generation.
Project Setup
npx create-next-app@latest my-app --typescript
cd my-app
npm install @trpc/server @trpc/client @trpc/next @trpc/react-query
npm install @tanstack/react-query zod superjsonDefine Your Router
// server/trpc.ts
import { initTRPC } from "@trpc/server";
import superjson from "superjson";
import { z } from "zod";
const t = initTRPC.create({ transformer: superjson });
export const router = t.router;
export const publicProcedure = t.procedure;// server/routers/user.ts
import { router, publicProcedure } from "../trpc";
import { z } from "zod";
export const userRouter = router({
getAll: publicProcedure.query(async () => {
return await db.user.findMany();
}),
getById: publicProcedure
.input(z.object({ id: z.string() }))
.query(async ({ input }) => {
return await db.user.findUnique({ where: { id: input.id } });
}),
create: publicProcedure
.input(z.object({
name: z.string().min(2),
email: z.string().email(),
}))
.mutation(async ({ input }) => {
return await db.user.create({ data: input });
}),
});Client Usage
// app/users/page.tsx
"use client";
import { trpc } from "@/utils/trpc";
export default function UsersPage() {
const { data: users, isLoading } = trpc.user.getAll.useQuery();
const createUser = trpc.user.create.useMutation({
onSuccess: () => utils.user.getAll.invalidate(),
});
if (isLoading) return <div>Loading...</div>;
return (
<div>
{users?.map(user => (
<div key={user.id}>{user.name} - {user.email}</div>
))}
<button onClick={() => createUser.mutate({
name: "New User",
email: "new@example.com"
})}>
Add User
</button>
</div>
);
}Server-Side Calls
// app/users/[id]/page.tsx (Server Component)
import { createCaller } from "@/server/routers";
export default async function UserPage({ params }: { params: { id: string } }) {
const caller = createCaller({});
const user = await caller.user.getById({ id: params.id });
return <div>{user?.name}</div>;
}Authentication Middleware
const protectedProcedure = t.procedure.use(async ({ ctx, next }) => {
if (!ctx.session?.user) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return next({ ctx: { user: ctx.session.user } });
});Benefits
- Change a server function signature and TypeScript catches all client errors instantly
- Full autocompletion for inputs and outputs
- No API documentation needed — the types ARE the documentation
- Smaller bundle than GraphQL clients
- Works perfectly with React Query for caching
Conclusion
tRPC with Next.js 15 is the most productive way to build full-stack TypeScript applications. The developer experience of end-to-end type safety is transformative.

Leave a Reply