TechLead
Intermedio
25 min
Lección 10 de 10
API

tRPC - Seguridad de Tipos de Extremo a Extremo

Construye APIs completamente type-safe sin esquemas ni generación de código usando tRPC

¿Qué es tRPC?

tRPC (TypeScript Remote Procedure Call) te permite construir APIs completamente type-safe sin esquemas ni generación de código. Aprovecha la inferencia de tipos de TypeScript para compartir automáticamente tipos entre tu servidor y cliente.

Si estás construyendo una aplicación full-stack de TypeScript (como Next.js), tRPC elimina la complejidad tradicional de la capa API mientras proporciona seguridad de tipos completa.

✨ ¿Por Qué tRPC?

🔒
Seguridad de Tipos de Extremo a Extremo

Los tipos fluyen del servidor al cliente automáticamente

Sin Generación de Código

Sin paso de compilación, actualizaciones de tipos instantáneas

🎯
Autocompletado en Todas Partes

Soporte completo del IDE para llamadas API

🚀
Bundle Pequeño

~2KB cliente, sin sobrecarga en tiempo de ejecución

Configurando tRPC

// Instalación
// npm install @trpc/server @trpc/client @trpc/react-query @tanstack/react-query zod

// 1. Define tu router (server/trpc.ts)
import { initTRPC, TRPCError } from '@trpc/server';
import { z } from 'zod';

// Inicializar tRPC
const t = initTRPC.context().create();

// Exportar ayudantes reutilizables de router y procedimiento
export const router = t.router;
export const publicProcedure = t.procedure;
export const protectedProcedure = t.procedure.use(isAuthed);

// 2. Crear tu app router (server/routers/_app.ts)
import { router } from '../trpc';
import { userRouter } from './user';
import { postRouter } from './post';

export const appRouter = router({
  user: userRouter,
  post: postRouter,
});

// Exportar definición de tipo de API
export type AppRouter = typeof appRouter;

Definiendo Procedimientos

// server/routers/user.ts
import { z } from 'zod';
import { router, publicProcedure, protectedProcedure } from '../trpc';
import { TRPCError } from '@trpc/server';

export const userRouter = router({
  // Query - para obtener datos (como GET)
  getById: publicProcedure
    .input(z.object({ id: z.string() }))
    .query(async ({ input, ctx }) => {
      const user = await ctx.db.user.findUnique({
        where: { id: input.id }
      });
      
      if (!user) {
        throw new TRPCError({
          code: 'NOT_FOUND',
          message: 'Usuario no encontrado'
        });
      }
      
      return user;
    }),

  // Query sin entrada
  list: publicProcedure
    .query(async ({ ctx }) => {
      return ctx.db.user.findMany({
        take: 10,
        orderBy: { createdAt: 'desc' }
      });
    }),

  // Mutation - para cambiar datos (como POST/PUT/DELETE)
  create: protectedProcedure
    .input(z.object({
      name: z.string().min(2).max(100),
      email: z.string().email(),
      role: z.enum(['admin', 'user']).optional().default('user')
    }))
    .mutation(async ({ input, ctx }) => {
      // ctx.user está disponible debido a protectedProcedure
      const user = await ctx.db.user.create({
        data: {
          ...input,
          createdBy: ctx.user.id
        }
      });
      
      return user;
    }),

  // Mutation de actualización
  update: protectedProcedure
    .input(z.object({
      id: z.string(),
      name: z.string().min(2).optional(),
      email: z.string().email().optional()
    }))
    .mutation(async ({ input, ctx }) => {
      const { id, ...data } = input;
      return ctx.db.user.update({
        where: { id },
        data
      });
    }),

  // Mutation de eliminación
  delete: protectedProcedure
    .input(z.object({ id: z.string() }))
    .mutation(async ({ input, ctx }) => {
      await ctx.db.user.delete({
        where: { id: input.id }
      });
      return { success: true };
    })
});

Context y Middleware

// server/context.ts
import { inferAsyncReturnType } from '@trpc/server';
import { getSession } from 'next-auth/react';

export async function createContext({ req, res }) {
  const session = await getSession({ req });
  
  return {
    req,
    res,
    user: session?.user ?? null,
    db: prisma // Tu cliente de base de datos
  };
}

export type Context = inferAsyncReturnType;

// Middleware para rutas protegidas
import { TRPCError } from '@trpc/server';

const isAuthed = t.middleware(({ ctx, next }) => {
  if (!ctx.user) {
    throw new TRPCError({
      code: 'UNAUTHORIZED',
      message: 'Debes iniciar sesión'
    });
  }
  
  return next({
    ctx: {
      // Infiere que el usuario no es nulo
      user: ctx.user
    }
  });
});

// Middleware de logging
const loggerMiddleware = t.middleware(async ({ path, type, next }) => {
  const start = Date.now();
  const result = await next();
  const duration = Date.now() - start;
  
  console.log(`${type} ${path} - ${duration}ms`);
  
  return result;
});

// Aplicar middleware
export const loggedProcedure = t.procedure.use(loggerMiddleware);

Cliente React con React Query

// utils/trpc.ts - Configuración del cliente
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '../server/routers/_app';

export const trpc = createTRPCReact();

// app/providers.tsx
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 }) {
  const [queryClient] = useState(() => new QueryClient());
  const [trpcClient] = useState(() =>
    trpc.createClient({
      links: [
        httpBatchLink({
          url: '/api/trpc',
          headers() {
            return {
              authorization: getAuthToken()
            };
          }
        })
      ]
    })
  );

  return (
    
      
        {children}
      
    
  );
}

Usando tRPC en Componentes

// components/UserList.tsx
import { trpc } from '../utils/trpc';

export function UserList() {
  // Queries - caché automático y refetch
  const { data: users, isLoading, error } = trpc.user.list.useQuery();

  // Query con entrada
  const { data: user } = trpc.user.getById.useQuery(
    { id: '123' },
    { enabled: !!userId } // Solo ejecutar si userId existe
  );

  // Mutations
  const utils = trpc.useUtils();
  
  const createUser = trpc.user.create.useMutation({
    onSuccess: () => {
      // Invalidar caché para refetch
      utils.user.list.invalidate();
    },
    onError: (error) => {
      alert(error.message);
    }
  });

  const handleSubmit = (data) => {
    createUser.mutate({
      name: data.name,
      email: data.email,
      // ¡TypeScript sabe exactamente qué campos son requeridos!
    });
  };

  if (isLoading) return 
Cargando...
; if (error) return
Error: {error.message}
; return (
{users?.map(user => (
{user.name} - {user.email}
))}
); } // Actualizaciones optimistas const updateUser = trpc.user.update.useMutation({ onMutate: async (newData) => { // Cancelar refetches salientes await utils.user.getById.cancel({ id: newData.id }); // Snapshot del valor actual const previousUser = utils.user.getById.getData({ id: newData.id }); // Actualizar optimistamente utils.user.getById.setData({ id: newData.id }, (old) => ({ ...old, ...newData })); return { previousUser }; }, onError: (err, newData, context) => { // Revertir en caso de error utils.user.getById.setData( { id: newData.id }, context?.previousUser ); } });

Integración con Next.js

// pages/api/trpc/[trpc].ts (Pages Router)
import { createNextApiHandler } from '@trpc/server/adapters/next';
import { appRouter } from '../../../server/routers/_app';
import { createContext } from '../../../server/context';

export default createNextApiHandler({
  router: appRouter,
  createContext,
  onError: ({ error, type, path }) => {
    console.error(`Error tRPC en ${path}:`, error);
  }
});

// app/api/trpc/[trpc]/route.ts (App Router)
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { appRouter } from '@/server/routers/_app';
import { createContext } from '@/server/context';

const handler = (req: Request) =>
  fetchRequestHandler({
    endpoint: '/api/trpc',
    req,
    router: appRouter,
    createContext
  });

export { handler as GET, handler as POST };

// Server Components (App Router) - ¡Llamadas directas!
import { appRouter } from '@/server/routers/_app';
import { createContext } from '@/server/context';

export default async function UsersPage() {
  // Llamar tRPC directamente en el servidor - ¡sin HTTP!
  const caller = appRouter.createCaller(await createContext());
  const users = await caller.user.list();

  return (
    
{users.map(user => (
{user.name}
))}
); }

Suscripciones (Tiempo Real)

// Servidor - Suscripciones WebSocket
import { observable } from '@trpc/server/observable';
import { EventEmitter } from 'events';

const ee = new EventEmitter();

export const postRouter = router({
  onNewPost: publicProcedure
    .subscription(() => {
      return observable((emit) => {
        const onAdd = (post: Post) => emit.next(post);
        
        ee.on('newPost', onAdd);
        
        return () => {
          ee.off('newPost', onAdd);
        };
      });
    }),

  create: protectedProcedure
    .input(z.object({ title: z.string(), content: z.string() }))
    .mutation(async ({ input, ctx }) => {
      const post = await ctx.db.post.create({ data: input });
      
      // Emitir a suscriptores
      ee.emit('newPost', post);
      
      return post;
    })
});

// Cliente - Suscribirse a actualizaciones
import { trpc } from '../utils/trpc';

function LiveFeed() {
  const [posts, setPosts] = useState([]);

  trpc.post.onNewPost.useSubscription(undefined, {
    onData: (post) => {
      setPosts((prev) => [post, ...prev]);
    },
    onError: (err) => {
      console.error('Error de suscripción:', err);
    }
  });

  return (
    
{posts.map(post => (
{post.title}
))}
); }

Manejo de Errores

// Errores del lado del servidor
import { TRPCError } from '@trpc/server';

// Códigos de error integrados
throw new TRPCError({
  code: 'BAD_REQUEST',        // 400
  code: 'UNAUTHORIZED',       // 401
  code: 'FORBIDDEN',          // 403
  code: 'NOT_FOUND',          // 404
  code: 'CONFLICT',           // 409
  code: 'UNPROCESSABLE_CONTENT', // 422
  code: 'TOO_MANY_REQUESTS',  // 429
  code: 'INTERNAL_SERVER_ERROR', // 500
  message: 'Mensaje de error personalizado',
  cause: originalError // Opcional
});

// Manejo del lado del cliente
const mutation = trpc.user.create.useMutation({
  onError: (error) => {
    // error.data contiene el código de error
    if (error.data?.code === 'CONFLICT') {
      toast.error('El usuario ya existe');
    } else if (error.data?.code === 'UNAUTHORIZED') {
      router.push('/login');
    } else {
      toast.error(error.message);
    }
  }
});

// ¡Los errores de validación Zod son automáticos!
// Si la entrada no coincide con el esquema, el cliente obtiene errores detallados

tRPC vs REST vs GraphQL

Característica REST GraphQL tRPC
Seguridad de Tipos Manual Codegen Automática ✓
Esquema OpenAPI SDL No necesario ✓
Paso de Compilación Opcional Requerido Ninguno ✓
Lenguaje Cualquiera Cualquiera Solo TypeScript
Curva de Aprendizaje Baja Media Baja ✓

💡 Mejores Prácticas de tRPC

  • Usa Zod para validación - Validación de entrada e inferencia de tipos
  • Divide routers por dominio - Mantén routers enfocados y organizados
  • Usa middleware - Auth, logging, limitación de tasa
  • Aprovecha React Query - Caché, actualizaciones optimistas, prefetching
  • Manejo de errores - Usa TRPCError con códigos apropiados
  • Server components - Llama tRPC directamente sin HTTP
Anterior
gRPC y Protocol Buffers
Ver
Todos los Temas

¡Has completado el curso de API!

Ahora tienes las habilidades para trabajar con APIs modernas. Continúa tu aprendizaje con temas relacionados: