tRPC: API Type-Safe End-to-End con TypeScript
Il Problema che tRPC Risolve
Hai mai cambiato la signature di un endpoint REST e dimenticato di aggiornare il client? O passato ore a debuggare un bug causato da un tipo JSON non allineato tra frontend e backend? Questo è il problema che tRPC elimina alla radice: API type-safe end-to-end, senza schema di mezzo, senza codegen.
Con tRPC, TypeScript è il contratto tra client e server. Se cambi il tipo di un parametro nel backend, il frontend smette di compilare — immediatamente, prima ancora di fare una chiamata di rete. In questa guida vediamo come configurare tRPC da zero con Next.js e come sfruttarne le funzionalità più potenti.
Setup: tRPC con Next.js App Router
Installa le dipendenze necessarie:
npm install @trpc/server @trpc/client @trpc/react-query @trpc/next
npm install @tanstack/react-query zod
npm install --save-dev typescript @types/node
Struttura delle cartelle consigliata:
src/
server/
routers/
user.ts # router per gli utenti
post.ts # router per i post
trpc.ts # inizializzazione tRPC
root.ts # router radice (app router)
utils/
trpc.ts # client tRPC per il frontend
app/
api/
trpc/
[trpc]/
route.ts # API route Next.js
Configurazione del Server tRPC
// src/server/trpc.ts
import { initTRPC, TRPCError } from "@trpc/server";
import { ZodError } from "zod";
// Context: dati disponibili in ogni procedura
export const createTRPCContext = async () => {
// Qui puoi aggiungere session, db, etc.
return {
userId: "user_123", // in produzione: da JWT/session
};
};
type Context = Awaited<ReturnType<typeof createTRPCContext>>;
const t = initTRPC.context<Context>().create({
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError:
error.cause instanceof ZodError ? error.cause.flatten() : null,
},
};
},
});
// Middleware di autenticazione
const isAuthed = t.middleware(({ ctx, next }) => {
if (!ctx.userId) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return next({ ctx: { ...ctx, userId: ctx.userId } });
});
export const router = t.router;
export const publicProcedure = t.procedure;
export const protectedProcedure = t.procedure.use(isAuthed);
Creare Router e Procedure
// src/server/routers/post.ts
import { z } from "zod";
import { router, publicProcedure, protectedProcedure } from "../trpc";
export const postRouter = router({
// Query: GET /api/trpc/post.getAll
getAll: publicProcedure
.input(
z.object({
limit: z.number().min(1).max(100).default(10),
cursor: z.string().optional(), // per paginazione
})
)
.query(async ({ input }) => {
// Qui va la logica database (Prisma, Drizzle, etc.)
const posts = await db.post.findMany({
take: input.limit,
cursor: input.cursor ? { id: input.cursor } : undefined,
orderBy: { createdAt: "desc" },
});
return { posts, nextCursor: posts[posts.length - 1]?.id };
}),
// Mutation: POST /api/trpc/post.create
create: protectedProcedure
.input(
z.object({
title: z.string().min(1).max(200),
content: z.string().min(10),
published: z.boolean().default(false),
})
)
.mutation(async ({ input, ctx }) => {
const post = await db.post.create({
data: { ...input, authorId: ctx.userId },
});
return post;
}),
// Subscription: WebSocket real-time
onCreated: publicProcedure.subscription(async function* () {
// Emette un evento ogni volta che un post viene creato
for await (const post of postCreatedEmitter) {
yield post;
}
}),
});
Root Router e API Route
// src/server/root.ts
import { router } from "./trpc";
import { postRouter } from "./routers/post";
import { userRouter } from "./routers/user";
export const appRouter = router({
post: postRouter,
user: userRouter,
});
// Esporta il tipo per il client
export type AppRouter = typeof appRouter;
// src/app/api/trpc/[trpc]/route.ts
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import { appRouter } from "@/server/root";
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 };
Client: Usare tRPC nel Frontend
// src/utils/trpc.ts
import { createTRPCReact } from "@trpc/react-query";
import type { AppRouter } from "@/server/root";
export const trpc = createTRPCReact<AppRouter>();
// src/app/providers.tsx
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { httpBatchLink } from "@trpc/client";
import { trpc } from "@/utils/trpc";
import { useState } from "react";
export function TRPCProvider({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(() => new QueryClient());
const [trpcClient] = useState(() =>
trpc.createClient({
links: [
httpBatchLink({
url: "/api/trpc",
// Batching automatico: più query → 1 richiesta HTTP
}),
],
})
);
return (
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
</trpc.Provider>
);
}
// Uso in un componente React
"use client";
import { trpc } from "@/utils/trpc";
export function PostList() {
// TypeScript conosce ESATTAMENTE il tipo di data.posts
const { data, isLoading } = trpc.post.getAll.useQuery({ limit: 20 });
const createPost = trpc.post.create.useMutation({
onSuccess: () => trpc.useUtils().post.getAll.invalidate(),
});
if (isLoading) return <div>Loading...</div>;
return (
<div>
{data?.posts.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2> {/* TypeScript sa che title è una string */}
</article>
))}
</div>
);
}
tRPC vs REST vs GraphQL: Quando Scegliere Cosa
- tRPC: monorepo TypeScript, team piccolo/medio, Next.js full-stack. Massima DX, zero overhead di schema
- REST: API pubblica (consumata da client non TypeScript), team con skill diverse, integrazione con tool esistenti (Swagger, Postman)
- GraphQL: API complessa con molte relazioni, client mobile con requisiti diversi, team grandi con necessità di schema esplicito
FAQ e Domande Frequenti
tRPC funziona con framework diversi da Next.js?
Sì. tRPC ha adapter per Express, Fastify, SvelteKit, Nuxt, e qualsiasi ambiente con un handler HTTP standard (fetch API). Il core è framework-agnostic; solo gli adapter cambiano. Puoi usarlo anche con un backend Node.js standalone separato dal frontend.
Come gestisco l’autenticazione con tRPC?
Attraverso i middleware nel context. Il pattern standard è: leggi il JWT/session cookie nel createTRPCContext, aggiungi l’utente al context, poi usa un middleware isAuthed che lancia TRPCError({ code: "UNAUTHORIZED" }) se il contesto non ha utente. Le protectedProcedure ereditano questo middleware automaticamente.
tRPC scala bene in produzione?
Sì. Il batching HTTP riduce il numero di richieste (più query in parallelo vengono aggregate in 1 richiesta). Le subscription usano WebSocket o Server-Sent Events. In produzione, tRPC è usato da aziende come Vercel e Planetscale. Non è adatto solo se hai bisogno di un’API pubblica REST standard.
Posso usare tRPC con Zod per la validazione?
tRPC è progettato per funzionare con Zod: ogni .input() accetta uno schema Zod. Il tipo inferito da Zod diventa automaticamente il tipo TypeScript della procedura — input e output sono validati sia a runtime che a compile time. Puoi leggere la guida completa su Zod per approfondire la validazione.
Conclusione
tRPC è la scelta naturale per ogni progetto TypeScript full-stack dove frontend e backend vivono nello stesso monorepo. Elimina un’intera categoria di bug, riduce il boilerplate e migliora la developer experience in modo misurabile. Se stai costruendo con Next.js e TypeScript, non c’è motivo per non usarlo.
Per completare il tuo stack type-safe, abbinalo a Zod per la validazione e a Next.js 16 con Turbopack per il build più veloce.
💡 Pro tip: Abilita il
httpBatchLinkin produzione e ilsplitLinkper separare query normali da subscription WebSocket — ottieni il meglio di entrambi i trasporti senza configurazione extra.🔧 Tool: Usa tRPC Devtools per visualizzare le procedure in tempo reale durante lo sviluppo — simile alle React Query devtools.
🎯 Nota: Se stai migrando da REST esistente a tRPC, usa il
createTRPCProxyClientper chiamate server-side (Next.js Server Components) senza React Query.

