Route Handlers Type-Safe en Next.js 16: El Error que el 90% Commite en Producción

Route Handlers Type-Safe en Next.js 16: El Error que el 90% Commite en Producción

Programming· 5 min read

El 90% de los Desarrolladores Construye Route Handlers Sin Validación — y Les Explota en Producción

Cada vez que desplegáis un Route Handler de Next.js, TypeScript os susurra una mentira tranquilizadora: "Todo tipo correctamente."

Mentira.

TypeScript verifica tipos en compilación. Los payloads llegan en runtime. Cuando un cliente envía {"age": "treinta"} en lugar de {"age": 30}, TypeScript no puede hacer nada. Vuestro "endpoint type-safe" acaba de aceptar datos inválidos y devolver un error críptico que nadie sabe cómo parsear.

El problema real no es que falten tipos. Es que la validación de runtime es la pieza que todos omiten.

Este artículo os muestra cómo construir Route Handlers que realmente garantizan type safety: validación con Zod, envoltorios genéricos, middleware de autenticación en edge, y contratos de respuesta tipados. Todo con Next.js 16 App Router.

La Trampa del Caching en GET Route Handlers

Hay un detalle que la documentación de Next.js menciona de pasada y que destruye APIs en producción:

GET Route Handlers que retornan Response.json() se cachean por defecto en producción.

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop
[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

La diferencia entre Response.json() y NextResponse.json() no es obvia. Mientras Response sigue el estándar web y se cachea, NextResponse mantiene comportamiento dinámico por defecto. Pero la confusión aparece cuando accedéis a request.nextUrl.searchParams — eso también desactiva el cacheo implícitamente.

Teams descubren esto cuando sus endpoints "/api/products?filter=new" devuelven el mismo resultado durante horas. La solución trivial (export const dynamic = 'force-dynamic') requiere saber que existe el problema.

El Patrón de Validación Jerárquica en 3 Capas

La mayoría de developers añaden Zod directamente en el handler. Esto funciona, pero no escala. Cuando tienes 20 endpoints, terminas copiando código de validación en cada archivo.

El patrón que realmente escala: validación jerárquica en 3 capas.

Capa 1 — Schemas compartidos en /lib/schemas

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop
[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

Capa 2 — Middleware de validación en el borde

El middleware de Next.js ejecuta antes que cualquier Route Handler. Aquí es donde validáis tokens JWT, rate limiting, y headers de autenticación.

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

Capa 3 — Handler con validación runtime

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

El Envoltorio Genérico que Elimina Boilerplate

Repetir try/catch, validación, y envelopes de respuesta en cada endpoint es un desastre para mantenibilidad. Aquí entra el apiHandler.

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

Uso del envoltorio:

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

Contrato de Respuesta Tipado

El 80% de los devs devuelven { data } en éxito y { error: string } en fallo. Pero cuando un endpoint devuelve { message: 'Not found' } y otro { error: '404' }, el front-end necesita switch statements para cada caso.

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

El cliente consume un único contrato:

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

Pages Router vs App Router: La Diferencia Real

Si migráis desde Pages Router, la diferencia no es solo sintaxis. Es comportamiento.

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

La nueva API de streaming y Server-Sent Events (SSE) en App Router permite respuestas que fluyen gradualmente. Especialmente útil para integración con modelos de lenguaje:

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

Nota importante: los endpoints de streaming no pueden usar el mismo middleware pipeline que endpoints JSON tradicionales. Timeout limits también varían entre plataformas serverless — Vercel tiene límites diferentes a Railway o AWS Lambda.

Framework: El Patrón de Validación Jerárquica para Route Handlers

Aquí está el sistema completo que os recomiendo implementar:

Paso 1: Crear /lib/schemas/ con Zod

Centralizad vuestras definiciones de schema. Cada dominio (users, products, orders) tiene su archivo. Los types se inferen automáticamente con z.infer<typeof schema>.

Paso 2: Configurar middleware.ts para auth y headers

Validación de JWT, rate limiting por IP, stripping de headers inesperados. Todo antes de que el handler se ejecute. Fail fast en el edge, no en serverless.

Paso 3: Crear createApiHandler en /lib/

El envoltorio genérico que ejecuta: validación → handler → error boundary → response envelope. Cada Route Handler usa este envoltorio.

Paso 4: Exportar dynamic = 'force-dynamic' en GET

Cada GET handler que sirva datos dinámicos necesita esta línea. Olvidarla significa servir datos obsoletos en producción.

Paso 5: Compartir schemas con el front-end

Importad los mismos schemas en vuestras funciones de fetching. Si el front-end envía un campo como string cuando el backend espera number, Zod lo captura antes del viaje de red.

[@portabletext/react] Unknown block type "code", specify a component for it in the `components.types` prop

Objections Respondidas

"Uso tRPC — no necesito validación manual."

tRPC maneja la API principal. Pero seguís necesitando Route Handlers para webhooks de Stripe, callbacks de NextAuth, y endpoints externos. Esos paths bypassean tRPC y necesitan la misma disciplina de validación.

"Zod añade demasiado boilerplate para CRUD simple."

Un schema de 5 líneas (email, name, id) cuesta 60 segundos escribirlo. Un campo no validado causando un constraint violation en la base de datos cuesta 30 minutos de debugging. El ROI aparece en el tercer endpoint.

"Con HTTP status codes es suficiente."

Status codes categorizan errores (4xx vs 5xx). No dicen nada sobre la forma del cuerpo de error. Sin envelope consistente, cada handler de error en el front-end necesita lógica personalizada.

Resumen y Próximos Pasos

Route Handlers en Next.js App Router no son mágicamente type-safe porque TypeScript esté configurado. La seguridad real requiere validación runtime con Zod, middleware en el edge para auth, envoltorios genéricos que eliminen boilerplate, y contratos de respuesta consistentes.

El Patrón de Validación Jerárquica en 3 Capas os da la estructura para escalar de 5 a 50 endpoints sin que el código se degeneré.

Próximos pasos concretos:

  1. Cread /lib/schemas/ hoy — moved los primeros schemas de vuestras rutas más críticas
  2. Implementad createApiHandler — el boilerplate que quitáis os saveará horas
  3. Configurad middleware.ts — auth en el edge es más rápido y más seguro
  4. Compartid schemas con el front-end — la misma validación funciona en ambos lados

TypeScript os da confianza. Zod os da seguridad. El patrón os da mantenibilidad.

Artículos relacionados

---

¿Quieres recibir contenido como este cada semana? Suscríbete a mi newsletter

Brian Mena

Brian Mena

Software engineer building profitable digital products: SaaS, directories and AI agents. All from scratch, all in production.

LinkedIn