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
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
Aqui es donde brilla. Los tipos se infieren automaticamente:
"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>
);
}"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
| tRPC | REST API | Server Actions | |
|---|---|---|---|
| Type safety | Automatico end-to-end | Manual (tienes que tipar el fetch) | Automatico |
| Validacion | 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 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.
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.
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. Verificacion de firmas, tipado y manejo de errores.
Formularios Dinamicos con React Hook Form y Zod
Crea formularios dinamicos con campos condicionales, arrays de campos y validacion type-safe usando React Hook Form y Zod en Next.js.