Gestores de Rutas API
Receta
Construye puntos de conexión de API REST seguros en tipos usando Next.js 15+ App Router Route Handlers con NextRequest, NextResponse, validación de solicitud y códigos de estado HTTP apropiados.
Ejemplo Funcional
Gestor de Ruta CRUD Básico
// app/api/posts/route.ts
import { NextRequest, NextResponse } from "next/server";
type Post = {
id: string;
title: string;
content: string;
createdAt: string;
};
// GET /api/posts
export async function GET(request: NextRequest) {
const { searchParams } = request.nextUrl;
const page = parseInt(searchParams.get("page") ?? "1", 10);
const limit = parseInt(searchParams.get("limit") ?? "10", 10);
const posts = await db.post.findMany({
skip: (page - 1) * limit,
take: limit,
orderBy: { createdAt: "desc" },
});
return NextResponse.json({ posts, page, limit });
}
// POST /api/posts
export async function POST(request: NextRequest) {
const body = await request.json();
if (!body.title || !body.content) {
return NextResponse.json(
{ error: "title and content are required" },
{ status: 400 }
);
}
const post = await db.post.create({
data: { title: body.title, content: body.content },
});
return NextResponse.json(post, { status: 201 });
}Parámetro de Ruta Dinámica
// app/api/posts/[id]/route.ts
import { NextRequest, NextResponse } from "next/server";
type Params = { params: Promise<{ id: string }> };
// GET /api/posts/:id
export async function GET(request: NextRequest, { params }: Params) {
const { id } = await params;
const post = await db.post.findUnique({ where: { id } });
if (!post) {
return NextResponse.json({ error: "Post not found" }, { status: 404 });
}
return NextResponse.json(post);
}
// PATCH /api/posts/:id
export async function PATCH(request: NextRequest, { params }: Params) {
const { id } = await params;
const body = await request.json();
const post = await db.post.update({
where: { id },
data: body,
});
return NextResponse.json(post);
}
// DELETE /api/posts/:id
export async function DELETE(request: NextRequest, { params }: Params) {
const { id } = await params;
await db.post.delete({ where: { id } });
return new NextResponse(null, { status: 204 });
}Gestor de Ruta con Encabezados y Cookies
// app/api/auth/me/route.ts
import { NextRequest, NextResponse } from "next/server";
import { verifySession } from "@/lib/auth";
export async function GET(request: NextRequest) {
const token = request.cookies.get("session-token")?.value;
if (!token) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const session = await verifySession(token);
if (!session) {
return NextResponse.json({ error: "Invalid session" }, { status: 401 });
}
const response = NextResponse.json({ user: session });
response.headers.set("X-Request-Id", crypto.randomUUID());
return response;
}Análisis Profundo
Cómo Funciona
- Route Handlers se definen exportando funciones de método HTTP (
GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS) desde un archivoroute.tsdentro del directorioapp. NextRequestextiende laRequestestándar con propiedades de conveniencia:.nextUrlpara URL analizada,.cookiespara acceso a cookies, y.geo/.ipen despliegues edge.NextResponseextiendeResponsecon ayudantes estáticos:.json(),.redirect()y.rewrite().- Los parámetros dinámicos ahora son asincronos en Next.js 15+. El segundo argumento es
{ params: Promise<{ ... }> }, y debesawait paramsantes de acceder a los valores. - Los Route Handlers se almacenan en caché de forma predeterminada para solicitudes
GETsin entrada dinámica. Usarequest.nextUrl.searchParams,cookies()oheaders()para optar por el renderizado dinámico automáticamente. - Un archivo
route.tsen el mismo directorio quepage.tsxcausará conflicto. Los Route Handlers y las páginas no pueden coexistir en el mismo segmento de ruta.
Variaciones
Respuesta de Streaming:
export async function GET() {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
for (const chunk of ["Hello", " ", "World"]) {
controller.enqueue(encoder.encode(chunk));
await new Promise((r) => setTimeout(r, 100));
}
controller.close();
},
});
return new Response(stream, {
headers: { "Content-Type": "text/plain" },
});
}Manejo de Datos de Formulario:
export async function POST(request: NextRequest) {
const formData = await request.formData();
const file = formData.get("file") as File;
if (!file) {
return NextResponse.json({ error: "No file uploaded" }, { status: 400 });
}
const bytes = await file.arrayBuffer();
const buffer = Buffer.from(bytes);
// Guarda buffer al almacenamiento...
return NextResponse.json({ filename: file.name, size: file.size });
}Encabezados CORS:
export async function OPTIONS() {
return new NextResponse(null, {
status: 204,
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
},
});
}Notas de TypeScript
- El tipo de params cambió en Next.js 15: usa
Promise<{ id: string }>en lugar del antiguo sincrónico{ id: string }. request.json()devuelvePromise<any>. Moldea o valida el resultado con Zod para seguridad en tipos.- Usa
NextRequest(noRequest) para acceder a.nextUrl,.cookiesy otras extensiones de Next.js.
Trampa
- Los Route Handlers
GETsin entrada dinámica se almacenan en caché estáticamente en tiempo de compilación. Accede asearchParams,cookies()oheaders()para hacerlos dinámicos, o exportaconst dynamic = "force-dynamic". route.tsypage.tsxno pueden compartir el mismo directorio. El Route Handler sombreará la página. Coloca rutas API bajoapp/api/para evitar conflictos.request.json()lanza en cuerpos vacíos o con formato incorrecto. Enrólalo en try/catch para seguridad.- Devolver
new Response(null, { status: 204 })se requiere para respuestas sin contenido.NextResponse.json(null, { status: 204 })enviará un cuerpo JSON vacío, no una respuesta verdadera sin contenido. - Los Route Handlers de Edge Runtime no pueden usar APIs de Node.js como
fs,Bufferocrypto(usaglobalThis.cryptoen su lugar).
Alternativas
| Enfoque | Ventajas | Desventajas |
|---|---|---|
| Route Handlers | Nativo, cero configuración, colocalizado | Sin validación incorporada o cadena middleware |
| Server Actions | Sin ruta API necesaria, amigable con formularios | No RESTful, difícil de llamar externamente |
| tRPC | Seguridad de tipos de extremo a extremo | Dependencia extra, curva de aprendizaje |
| Servidor personalizado Express/Fastify | Ecosistema middleware completo | Pierde optimización estática automática |
| Hono en Edge Runtime | Ligero, cadena middleware | No oficialmente soportado por Next.js |
Ejemplo del Mundo Real
De una aplicación SaaS de Next.js 15 / React 19 en producción (SystemsArchitect.io).
// Ejemplo de producción: Ruta API con composición de seguridad y seguimiento de uso atómico
// Archivo: app/api/generate/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { withCsrfProtection } from '@/lib/csrf';
import { withRateLimit } from '@/lib/rate-limit';
import { prisma } from '@/lib/prisma';
import { getSession } from '@/lib/auth';
export const maxDuration = 60; // Tiempo de espera de función Vercel en segundos
async function handler(request: NextRequest) {
const session = await getSession();
if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const body = await request.json();
const { prompt, projectId } = body;
// Seguimiento de uso atómico: deduce crédito antes de la operación
const updated = await prisma.user.updateMany({
where: {
id: session.userId,
credits: { gt: 0 },
},
data: {
credits: { decrement: 1 },
},
});
if (updated.count === 0) {
return NextResponse.json({ error: 'No credits remaining' }, { status: 402 });
}
try {
const result = await generateContent(prompt, projectId);
return NextResponse.json({ result });
} catch (error) {
// Revierte: restaura el crédito en fallo
await prisma.user.update({
where: { id: session.userId },
data: { credits: { increment: 1 } },
});
return NextResponse.json({ error: 'Generation failed' }, { status: 500 });
}
}
// Composición middleware de seguridad: CSRF check envuelve rate limiter envuelve handler
export const POST = withCsrfProtection(withRateLimit(handler));Lo que esto demuestra en producción:
withCsrfProtection(withRateLimit(handler))compone middleware de seguridad como funciones de orden superior. La verificación CSRF se ejecuta primero. Si pasa, se ejecuta el limitador de velocidad. Si ese pasa, se ejecuta el handler. Este patrón de composición evita cadenas middleware profundamente anidadas.export const maxDuration = 60establece el tiempo de espera de la función serverless de Vercel. El predeterminado es 10 segundos, que es demasiado corto para la generación de contenido de IA. Esta configuración es específica para despliegues de Vercel.- La deducción de crédito usa
updateManycon una condiciónwhere: { credits: { gt: 0 } }. Esta es una operación atómica: el crédito solo se deduce si el usuario tiene créditos restantes. Previene condiciones de carrera donde dos solicitudes simultáneas podrían ambas leercredits: 1y ambas proceder. updated.count === 0significa que el usuario no fue encontrado o tenía cero créditos. Este verificación única maneja ambos casos sin una consulta de lectura separada.- La reversión en el bloque catch incrementa el crédito de nuevo si la generación falla. Esto asegura que los usuarios no sean cobrados por operaciones fallidas. En un escenario de alta concurrencia, considera usar una transacción de base de datos en su lugar.
- Exportar
POSTcomo la función compuesta (nohandlerdirectamente) asegura que cada solicitud POST pase a través de ambas capas de seguridad. Las exportaciones nombradas comoGET,POST,PUTse asignan directamente a métodos HTTP en gestores de ruta de Next.js.
Ejemplo del Mundo Real: Limitación de Velocidad Atómica
De una aplicación SaaS de Next.js 15 / React 19 en producción (SystemsArchitect.io).
// Ejemplo de producción: Limitación de velocidad segura contra TOCTOU con upsert de Prisma
// Archivo: src/lib/tools/usage-limits.ts
export async function checkAndIncrementUsage(
toolId: string,
userId: string | null,
ipAddress: string,
userTier: UserTier
): Promise<{ allowed: boolean; limit: number; used: number; resetsAt: string }> {
const limit = await getToolUsageLimitAsync(toolId, userTier);
if (limit === -1) {
await incrementToolUsage(toolId, userId, ipAddress);
return { allowed: true, limit: -1, used: 0, resetsAt: new Date(Date.now() + 86400000).toISOString() };
}
const today = new Date();
today.setUTCHours(0, 0, 0, 0);
const tomorrow = new Date(today);
tomorrow.setUTCDate(tomorrow.getUTCDate() + 1);
const sessionId = userId || ipAddress;
// Upsert atómico: incrementa primero, luego verifica
const updated = await prisma.toolUsage.upsert({
where: {
sessionId_ipAddress_toolName: { sessionId, ipAddress, toolName: toolId },
},
create: { toolName: toolId, userId, sessionId, ipAddress, usageCount: 1, lastUsedAt: new Date() },
update: { usageCount: { increment: 1 }, lastUsedAt: new Date() },
});
// Reinicio de límite de día
if (updated.lastUsedAt < today) {
const reset = await prisma.toolUsage.update({
where: { id: updated.id },
data: { usageCount: 1, lastUsedAt: new Date() },
});
return { allowed: true, limit, used: reset.usageCount, resetsAt: tomorrow.toISOString() };
}
// ¿Superó el límite? Revierte el incremento
if (updated.usageCount > limit) {
await prisma.toolUsage.update({
where: { id: updated.id },
data: { usageCount: { decrement: 1 } },
});
return { allowed: false, limit, used: updated.usageCount - 1, resetsAt: tomorrow.toISOString() };
}
return { allowed: true, limit, used: updated.usageCount, resetsAt: tomorrow.toISOString() };
}Lo que esto demuestra en producción:
- Prevención de TOCTOU: En lugar de "leer conteo, verificar límite, luego incrementar" (que tiene una ventana de carrera), esto hace "incrementar primero, luego verificar si superó el límite, revertir si es necesario"
upsertcrea el registro en primer uso o incrementa en usos posteriores. Sin consulta "¿existe esto?" separada- La restricción única compuesta
sessionId_ipAddress_toolNameasegura un contador por usuario por herramienta por día userId || ipAddresscomo sessionId maneja tanto usuarios autenticados como anónimoslimit === -1significa ilimitado (tier admin). Aún rastrea el uso para análisis pero nunca niega- Verificación de límite de día:
updated.lastUsedAt < todaydetecta registros obsoletos de ayer y reinicia a 1 - La reversión
{ decrement: 1 }deshace el incremento optimista cuando se excede el límite
Preguntas Frecuentes
¿Qué métodos HTTP puede exportar un Route Handler?
GET,POST,PUT,PATCH,DELETE,HEADyOPTIONS.- Cada uno es una exportación nombrada desde un archivo
route.tsdentro del directorioapp. - Solo exporta los métodos que tu punto de conexión soporta.
¿Por qué los Route Handlers GET se almacenan en caché de forma predeterminada?
- Next.js almacena en caché estáticamente los gestores
GETsin entrada dinámica en tiempo de compilación. - Acceder a
searchParams,cookies()oheaders()opta por el renderizado dinámico automáticamente. - Usa
export const dynamic = "force-dynamic"para forzar el comportamiento dinámico explícitamente.
¿Cuál es la diferencia entre NextRequest y la Request estándar?
NextRequestextiendeRequestcon.nextUrl(URL analizada),.cookies,.geoe.ip.- Usa
NextRequestpara acceder a estas conveniencias específicas de Next.js. - La
Requestestándar funciona pero carece de estos ayudantes.
¿Por qué los parámetros dinámicos son una Promise en Next.js 15+?
type Params = { params: Promise<{ id: string }> };
export async function GET(request: NextRequest, { params }: Params) {
const { id } = await params;
}- En Next.js 15+, los params son asincronos y deben ser esperados antes del uso.
- Este es un cambio disruptivo de versiones anteriores donde los params eran sincronos.
Trampa: ¿Pueden route.ts y page.tsx coexistir en el mismo directorio?
- No. Un Route Handler sombreará la página en el mismo segmento de ruta.
- Coloca rutas API bajo
app/api/para evitar conflictos con rutas de página.
¿Cómo devuelves una respuesta adecuada de 204 Sin Contenido?
return new NextResponse(null, { status: 204 });- No uses
NextResponse.json(null, { status: 204 })ya que envía un cuerpo JSON vacío. - Usa
new NextResponse(null, { status: 204 })para una respuesta verdadera sin contenido.
Trampa: ¿Qué ocurre si request.json() recibe un cuerpo vacío o con formato incorrecto?
- Lanza un error en tiempo de ejecución.
- Siempre envuelve
request.json()en un bloque try/catch. - Devuelve un estado 400 con un mensaje de error en fallo de análisis.
¿Cómo funciona el patrón de composición middleware de seguridad en el ejemplo del mundo real?
withCsrfProtection(withRateLimit(handler))encadena funciones de orden superior.- La verificación CSRF se ejecuta primero; si pasa, la limitación de velocidad se ejecuta; si eso pasa, se ejecuta el handler.
- Esto evita middleware profundamente anidado y mantiene cada preocupación aislada.
¿Qué es la deducción de crédito atómica y por qué previene condiciones de carrera?
updateManyconwhere: { credits: { gt: 0 } }verifica y decrementa en una sola consulta.- Dos solicitudes simultáneas no pueden ambas leer
credits: 1y ambas proceder. updated.count === 0significa que la deducción falló (sin créditos o usuario no encontrado).
¿Cómo escribirías de forma segura el cuerpo JSON desde request.json() en TypeScript?
import { z } from "zod";
const bodySchema = z.object({
title: z.string().min(1),
content: z.string().min(1),
});
const parsed = bodySchema.safeParse(await request.json());
if (!parsed.success) {
return NextResponse.json(
{ error: parsed.error.flatten() },
{ status: 400 }
);
}
// parsed.data is fully typed¿Cómo manejas CORS en Route Handlers?
- Exporta una función
OPTIONSque devuelve encabezados CORS con un estado 204. - Establece
Access-Control-Allow-Origin,Access-Control-Allow-MethodsyAccess-Control-Allow-Headers. - Para solicitudes no preflight, agrega encabezados CORS a la respuesta real también.
¿Cuál es el problema TOCTOU en limitación de velocidad y cómo el patrón upsert lo resuelve?
- TOCTOU (Tiempo-de-Verificación-Tiempo-de-Uso): leer el conteo, verificar el límite, luego incrementar tiene una ventana de carrera.
- El patrón upsert incrementa primero, luego verifica si superó el límite, y revierte si es necesario.
- Esto asegura que no hay brecha entre la verificación y la actualización.
Relacionado
- Authentication - protegiendo rutas API con verificaciones de sesión
- Error Handling - respuestas de error consistentes
- Environment Variables - accediendo secretos en Route Handlers
- Documentación de Next.js Route Handlers