tRPC: API Type-Safe End-to-End Senza Schema Manuale

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 httpBatchLink in produzione e il splitLink per 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 createTRPCProxyClient per chiamate server-side (Next.js Server Components) senza React Query.

Condividi

Articoli Recenti

Categorie popolari