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
npm install @trpc/server @trpc/client @trpc/react-query @trpc/next @tanstack/react-query zodCrear el router (servidor)
// 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;// 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 });
}),
});// 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
// 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
// lib/trpc.ts
import { createTRPCReact } from "@trpc/react-query";
import type { AppRouter } from "@/server/routers";
export const trpc = createTRPCReact<AppRouter>();// 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:
"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>
);
}"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
| tRPC | REST API | Server Actions | |
|---|---|---|---|
| Type safety | automático end-to-end | Manual (tienes que tipar el fetch) | automático |
| Validación | Zod integrado | Manual | Zod manual |
| Caching | React Query built-in | Manual o SWR | Next.js cache |
| Clientes externos | No ideal | Si | No |
| Setup | Medio | Bajo | Bajo |
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.
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.
Articulos relacionados
Zod Avanzado: Discriminated Unions, Transforms y Pipes
Patrones avanzados de Zod: discriminated unions, transforms, pipes, preprocess, y como validar datos complejos en TypeScript con schemas reutilizables.
Webhooks en Next.js: Recibe y Procesa Eventos
Implementa webhooks en Next.js para recibir eventos de Stripe, GitHub, Clerk y otros servicios. Verificación de firmas, tipado y manejo de errores.
Formularios dinámicos con React Hook Form y Zod
Crea formularios dinámicos con campos condicionales, arrays de campos y validación type-safe usando React Hook Form y Zod en Next.js.