tutoriales·7 min de lectura

tRPC + Next.js: APIs Type-Safe sin REST

Implementa tRPC en Next.js para APIs 100% type-safe. Sin schemas de API, sin fetch manual, sin types duplicados. End-to-end type safety con TypeScript.

tRPC + Next.js: APIs Type-Safe sin REST

tRPC elimina la capa de API tradicional. En vez de definir endpoints REST, escribir fetch, y mantener tipos duplicados entre cliente y servidor, llamas funciones del servidor directamente desde el cliente. TypeScript infiere todos los tipos automáticamente.

Si tu app es un monorepo o un proyecto Next.js donde frontend y backend viven juntos, tRPC te ahorra horas de boilerplate.

Setup

bash
npm install @trpc/server @trpc/client @trpc/react-query @trpc/next @tanstack/react-query zod

Crear el router (servidor)

typescript
// server/trpc.ts
import { initTRPC } from "@trpc/server";
import { z } from "zod";
 
const t = initTRPC.create();
 
export const router = t.router;
export const publicProcedure = t.procedure;
typescript
// server/routers/user.ts
import { z } from "zod";
import { router, publicProcedure } from "../trpc";
 
export const userRouter = router({
  // Query: leer datos
  getById: publicProcedure
    .input(z.object({ id: z.string() }))
    .query(async ({ input }) => {
      const user = await db.user.findUnique({ where: { id: input.id } });
      return user;
    }),
 
  // Mutation: escribir datos
  create: publicProcedure
    .input(z.object({
      name: z.string().min(2),
      email: z.string().email(),
    }))
    .mutation(async ({ input }) => {
      return await db.user.create({ data: input });
    }),
});
typescript
// server/routers/index.ts
import { router } from "../trpc";
import { userRouter } from "./user";
 
export const appRouter = router({
  user: userRouter,
});
 
export type AppRouter = typeof appRouter;

API route handler

typescript
// app/api/trpc/[trpc]/route.ts
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
import { appRouter } from "@/server/routers";
 
const handler = (req: Request) =>
  fetchRequestHandler({
    endpoint: "/api/trpc",
    req,
    router: appRouter,
  });
 
export { handler as GET, handler as POST };

Configurar el cliente

typescript
// lib/trpc.ts
import { createTRPCReact } from "@trpc/react-query";
import type { AppRouter } from "@/server/routers";
 
export const trpc = createTRPCReact<AppRouter>();
typescript
// app/providers.tsx
"use client";
 
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { httpBatchLink } from "@trpc/client";
import { trpc } from "@/lib/trpc";
import { useState } from "react";
 
export function Providers({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(() => new QueryClient());
  const [trpcClient] = useState(() =>
    trpc.createClient({
      links: [httpBatchLink({ url: "/api/trpc" })],
    })
  );
 
  return (
    <trpc.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>
        {children}
      </QueryClientProvider>
    </trpc.Provider>
  );
}

Usar en componentes

aquí es donde brilla. Los tipos se infieren automáticamente:

typescript
"use client";
 
import { trpc } from "@/lib/trpc";
 
export function UserProfile({ userId }: { userId: string }) {
  // TypeScript sabe que data es User | null
  const { data: user, isLoading } = trpc.user.getById.useQuery({ id: userId });
 
  if (isLoading) return <p>Cargando...</p>;
  if (!user) return <p>Usuario no encontrado</p>;
 
  // user.name, user.email -- todo tipado automáticamente
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}
typescript
"use client";
 
import { trpc } from "@/lib/trpc";
 
export function CreateUserForm() {
  const mutation = trpc.user.create.useMutation();
 
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);
 
    // TypeScript válida que pases name y email
    mutation.mutate({
      name: formData.get("name") as string,
      email: formData.get("email") as string,
    });
  };
 
  return (
    <form onSubmit={handleSubmit}>
      <input name="name" placeholder="Nombre" />
      <input name="email" placeholder="Email" />
      <button disabled={mutation.isPending}>
        {mutation.isPending ? "Creando..." : "Crear"}
      </button>
      {mutation.error && <p>{mutation.error.message}</p>}
    </form>
  );
}

Si cambias el schema en el servidor (por ejemplo, agregas un campo age), TypeScript te marca en rojo todos los lugares del cliente que necesitan actualizarse. Zero sorpresas en runtime.

tRPC vs REST vs Server Actions

tRPCREST APIServer Actions
Type safetyautomático end-to-endManual (tienes que tipar el fetch)automático
ValidaciónZod integradoManualZod manual
CachingReact Query built-inManual o SWRNext.js cache
Clientes externosNo idealSiNo
SetupMedioBajoBajo

Usa tRPC cuando tu app es un monorepo TypeScript y necesitas queries complejas con caching. Usa Server Actions para mutaciones simples (formularios, updates). Usa REST cuando tu API la consumen clientes no-TypeScript.

Siguiente paso

Si usas Zod para la validación de tRPC, profundiza en la guía de Zod para patrones avanzados. Y si tu API necesita conectarse a PostgreSQL, la guía de PostgreSQL para devs TypeScript cubre el setup completo.

#trpc#nextjs#typescript#api#type-safe

Preguntas frecuentes

¿Qué es tRPC?

tRPC te permite llamar funciones del servidor desde el cliente como si fueran funciones locales, con tipos TypeScript automaticos. No necesitas definir schemas de API, no necesitas hacer fetch manual, y los tipos se comparten entre servidor y cliente sin generar código.

¿TRPC o REST para Next.js?

Si tu frontend y backend son el mismo repo TypeScript (monorepo o Next.js), tRPC es más productivo. Si tu API la consumen clientes que no son TypeScript (apps moviles, terceros), REST o GraphQL es mejor porque necesitas un contrato explicito.

¿TRPC funciona con App Router?

Si. tRPC 11+ soporta Next.js App Router con Server Components y React Query. Puedes hacer prefetch en el servidor y pasar los datos hidratados al cliente.

¿Necesito Zod con tRPC?

No es obligatorio pero es la práctica recomendada. Zod válida los inputs en runtime y tRPC infiere los tipos automáticamente del schema de Zod. Sin Zod, pierdes la validación en runtime.