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 automaticamente.

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

Aqui es donde brilla. Los tipos se infieren automaticamente:

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 automaticamente
  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 valida 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 safetyAutomatico end-to-endManual (tienes que tipar el fetch)Automatico
ValidacionZod 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 validacion de tRPC, profundiza en la guia de Zod para patrones avanzados. Y si tu API necesita conectarse a PostgreSQL, la guia de PostgreSQL para devs TypeScript cubre el setup completo.

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

Preguntas frecuentes

Que 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 codigo.

tRPC o REST para Next.js?

Si tu frontend y backend son el mismo repo TypeScript (monorepo o Next.js), tRPC es mas 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 practica recomendada. Zod valida los inputs en runtime y tRPC infiere los tipos automaticamente del schema de Zod. Sin Zod, pierdes la validacion en runtime.