Cuando arranco un proyecto nuevo con Next.js en AWS, siempre me hago la misma pregunta. ¿Uso Route Handlers (los archivos route.ts dentro de app/api) o monto un API Gateway separado con Lambdas independientes? Ambos caminos funcionan, pero tienen implicaciones muy distintas en costo, operaciones y escalabilidad.
Este artículo es el resumen de decisiones que tomé en 3 proyectos reales, con los números de costo, latencia y complejidad operacional. La respuesta corta: para la mayoría de apps, Route Handlers son suficientes. Para casos específicos, API Gateway gana. Voy a ser específico sobre cuáles.
Qué es cada uno realmente
Route Handlers son funciones HTTP que Next.js expone como endpoints. Viven en app/api/*/route.ts y se bundlean con el resto de la aplicación. Cuando despliegas Next.js en Lambda vía Amplify Gen 2 o SST, el mismo Lambda que renderiza páginas también atiende los Route Handlers. Una sola función, múltiples responsabilidades.
// app/api/users/route.ts
import { NextResponse } from "next/server";
import { db } from "@/lib/db";
export async function GET() {
const users = await db.user.findMany();
return NextResponse.json(users);
}
export async function POST(req: Request) {
const body = await req.json();
const user = await db.user.create({ data: body });
return NextResponse.json(user, { status: 201 });
}
API Gateway es un servicio separado que enruta requests HTTP a Lambdas específicas. Cada endpoint puede ser su propia Lambda con su propio bundle, su propia memoria, su propio timeout.
// cdk/lib/api-stack.ts
import { RestApi, LambdaIntegration } from "aws-cdk-lib/aws-apigateway";
import { Function, Runtime, Code } from "aws-cdk-lib/aws-lambda";
const api = new RestApi(this, "Api");
const getUsersFn = new Function(this, "GetUsers", {
runtime: Runtime.NODEJS_20_X,
handler: "index.handler",
code: Code.fromAsset("lambdas/get-users"),
memorySize: 512,
});
api.root
.addResource("users")
.addMethod("GET", new LambdaIntegration(getUsersFn));
La diferencia clave: Route Handlers viven dentro de tu aplicación Next.js. API Gateway vive fuera, como un producto AWS separado que cualquier cliente puede consumir.
Comparativa de costos reales
Estas mediciones son de un proyecto real con tráfico de 2M requests/mes, distribuidos entre páginas SSR y endpoints JSON.
flowchart LR
subgraph "Opción A - todo en Next.js Lambda"
A[CloudFront] --> AL[Lambda Next.js 1GB]
AL --> AD[Render pages]
AL --> AR[Route Handlers]
end
subgraph "Opción B - split con API Gateway"
B[CloudFront] --> BL[Lambda Next.js 1GB]
BL --> BD[Render pages]
B --> BG[API Gateway]
BG --> BF1[Lambda users 256MB]
BG --> BF2[Lambda orders 512MB]
BG --> BF3[Lambda reports 1GB]
end
Con los números reales del proyecto:
| Componente | Opción A (todo Next.js) | Opción B (split) |
|---|---|---|
| Lambda Next.js invocations | 2M x $0.20/M = $0.40 | 500K x $0.20/M = $0.10 |
| Lambda Next.js duration | 2M x 1GB x 150ms = $50 | 500K x 1GB x 200ms = $16.67 |
| API Gateway requests | $0 | 1.5M x $3.50/M = $5.25 |
| Lambdas específicas | $0 | ~$18 (mix de memoria/duración) |
| CloudWatch logs | $12 | $18 (más Lambdas, más logs) |
| Total mensual | $62.40 | $58.02 |
Con este volumen, la diferencia es mínima. La cosa cambia en los extremos. Para tráfico chico (100K requests/mes), Opción A es obviamente más barata porque API Gateway cobra por request desde el primer uso. Para tráfico grande (50M/mes), Opción B empieza a ganar porque puedes ajustar memoria y timeout de cada Lambda a lo que realmente necesita.
Cuándo Route Handlers ganan
1. Proyectos pequeños a medianos.
Si estás arrancando, Route Handlers son menos código, menos infra, menos cosas que se pueden romper. Todo vive en el mismo repo, mismo deploy, mismo CloudWatch log group.
2. Endpoints que comparten código con Server Components.
Si tu Server Component y tu Route Handler llaman a la misma función de lib/db.ts, mantenerlos en el mismo bundle evita duplicación. Con API Gateway separado tendrías que publicar ese código como layer o paquete compartido.
3. Cuando necesitas cookies de sesión disponibles en ambos.
Los Route Handlers heredan el contexto de cookies de Next.js. Los Lambdas detrás de API Gateway no saben nada del sistema de auth de Next.js a menos que implementes el parseo tú mismo.
// app/api/profile/route.ts
import { getSession } from "@/lib/session";
export async function GET() {
const session = await getSession();
if (!session) {
return Response.json({ error: "unauthorized" }, { status: 401 });
}
// lógica con session.userId
}
Eso Just Works™. En API Gateway separado, tendrías que parsear la cookie sid, consultar Redis o tu store de sesiones, y eso se vuelve repetitivo.
4. Prototipado rápido.
Agregar un endpoint nuevo en Next.js es crear un archivo. En API Gateway es modificar CDK/Terraform, generar un nuevo bundle, configurar IAM, etc. Para iteración rápida con stakeholders, Route Handlers ganan por velocidad.
Cuándo API Gateway gana
1. Endpoints con perfil muy distinto al resto de la app.
Supongamos que tienes un endpoint que procesa PDFs. Necesita 3GB de RAM, timeout de 60 segundos, y solo se invoca 100 veces al día. Meterlo en el Lambda de Next.js significa que TODO el Lambda tiene 3GB, incluyendo los requests que solo renderizan una página de login. Despilfarro puro.
// lambdas/process-pdf/index.ts
import { S3Event } from "aws-lambda";
export const handler = async (event: S3Event) => {
// procesamiento pesado de PDF
// necesita 3GB RAM, 60s timeout
};
Separar esa lógica en un Lambda dedicado detrás de API Gateway te permite pagar solo por lo que usas en esos casos específicos.
2. APIs públicas consumidas por terceros.
Si construyes una API que clientes externos van a consumir (ej: webhooks que dispararon integradores, API pública de tu SaaS), API Gateway te da features clave: API keys, throttling, usage plans, WAF integration, request/response transformation. Route Handlers no tienen nada de eso nativamente.
3. Cuando necesitas múltiples entornos runtime.
Route Handlers corren en Node.js (o Edge Runtime con limitaciones). Si tienes un endpoint que necesita Python para usar una librería específica (OpenCV, scikit-learn), necesitas Lambda separado con Python runtime. API Gateway lo enruta sin problema.
4. Cuando el tráfico de API es mucho mayor que el tráfico de páginas.
Si tu app es 90% API y 10% pages, tiene sentido que el API tenga su propia infra optimizada. Route Handlers en el Lambda de Next.js cargan todo el bundle incluyendo React, dependencias de rendering, etc. Un Lambda dedicado al API puede ser 10x más pequeño y frío menos.
Una arquitectura híbrida real
El patrón que uso en proyectos medianos a grandes: Route Handlers para lo que es parte del user flow normal, API Gateway para lo que es pesado, externo, o con perfil distinto.
flowchart TB
U[Usuario] --> CF[CloudFront]
CF -->|páginas + API usuarios| NL[Lambda Next.js]
NL --> NA[Route Handlers /api/users, /api/projects]
NL --> NR[Render SSR]
CF -->|rutas específicas| AG[API Gateway]
AG --> PF[Lambda Process PDF 3GB]
AG --> RF[Lambda Reports 1GB]
AG --> WH[Lambda Webhooks 256MB]
NL --> DB[(DynamoDB)]
PF --> DB
RF --> DB
WH --> DB
En un proyecto SaaS reciente:
- 80% del tráfico pasa por Next.js Lambda (pages + Route Handlers típicos)
- 15% va a
/api/reports/*en API Gateway (Lambda 1GB con 30s timeout) - 5% va a
/api/webhooks/*también en API Gateway (Lambda 256MB muy rápido)
El CloudFront routing se configura con behaviors:
// cdk/lib/cloudfront-stack.ts
const distribution = new Distribution(this, "Distribution", {
defaultBehavior: {
origin: new HttpOrigin(nextjsLambdaUrl),
cachePolicy: CachePolicy.CACHING_DISABLED,
},
additionalBehaviors: {
"/api/reports/*": {
origin: new RestApiOrigin(reportsApi),
cachePolicy: CachePolicy.CACHING_DISABLED,
},
"/api/webhooks/*": {
origin: new RestApiOrigin(webhooksApi),
cachePolicy: CachePolicy.CACHING_DISABLED,
allowedMethods: AllowedMethods.ALLOW_ALL,
},
"/_next/static/*": {
origin: new S3Origin(staticBucket),
cachePolicy: CachePolicy.CACHING_OPTIMIZED,
},
},
});
CloudFront decide a dónde ir según el path. Para el cliente es transparente, todos los endpoints viven bajo el mismo dominio.
Route Handlers con streaming
Una feature que a menudo se olvida: los Route Handlers pueden hacer streaming con Lambda Response Streaming, igual que las pages.
// app/api/export/route.ts
import { db } from "@/lib/db";
export async function GET() {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
controller.enqueue(encoder.encode("id,name,email\n"));
let cursor: string | undefined;
do {
const batch = await db.user.findMany({
take: 1000,
skip: cursor ? 1 : 0,
cursor: cursor ? { id: cursor } : undefined,
orderBy: { id: "asc" },
});
for (const user of batch) {
controller.enqueue(
encoder.encode(`${user.id},${user.name},${user.email}\n`)
);
}
cursor = batch[batch.length - 1]?.id;
if (batch.length < 1000) break;
} while (cursor);
controller.close();
},
});
return new Response(stream, {
headers: {
"Content-Type": "text/csv",
"Content-Disposition": "attachment; filename=users.csv",
},
});
}
Esto exporta 100K usuarios sin cargar todos en memoria ni esperar 30s antes de empezar a enviar bytes. En Lambda con Response Streaming habilitado, funciona hasta 15 minutos de duración.
API Gateway REST no soporta streaming. Si necesitas streams, HTTP API (v2) o Function URLs sí lo soportan.
Monitoreo comparativo
Un punto que no se discute mucho: cómo ves qué está pasando en producción.
Con Route Handlers, todos los requests (pages + API) van al mismo CloudWatch log group del Lambda de Next.js. Filtrar por endpoint requiere queries en CloudWatch Logs Insights:
fields @timestamp, @message
| filter @message like /\/api\/users/
| stats count() by bin(5m)
Con API Gateway separado, cada Lambda tiene su log group. API Gateway además emite métricas nativas (4xx, 5xx, latencia p50/p95/p99) sin instrumentación. Integración con CloudWatch Metrics es directa.
Para observabilidad fina, la Opción B gana claro. Para observabilidad básica, Opción A es suficiente.
Lo que aprendí aplicando esto
1. Empiezo siempre con Route Handlers.
Migro a API Gateway solo cuando hay un motivo claro. En 3 de 5 proyectos recientes nunca hice esa migración porque nunca apareció el motivo. Premature architecture es peor que refactor después.
2. Los Route Handlers heredan los cold starts del Lambda de Next.js.
Si tu Lambda de Next.js pesa 80MB con todas las dependencias, cada cold start de un Route Handler paga ese arranque. Para APIs críticas con requisito de latencia baja, un Lambda dedicado con bundle de 5MB tiene cold starts más cortos.
3. Las funciones serverless de CDK son más fáciles de testear.
Un Lambda independiente se testea unitariamente sin levantar Next.js. Los Route Handlers son funciones Next.js con todo el contexto; testearlos requiere mocks de cookies(), headers(), etc. Para lógica de negocio compleja, separarla a Lambda dedicada y testear a fondo compensa.
4. El vendor lock-in es diferente.
Route Handlers son Next.js. Si decides cambiar a Remix, Astro o lo que sea, tienes que reescribirlos (aunque la lógica se porta). API Gateway + Lambda es AWS, pero es AWS genérico. Puedes mover el frontend sin tocar los APIs.
5. El costo real depende del perfil de tráfico.
Hice cálculos asumiendo distribución uniforme. Si tu tráfico es bursty (ej: picos por email campaigns), API Gateway throttling te protege mejor que confiar en el Lambda provisioned concurrency de Next.js.
6. Los timeouts son diferentes por default.
Amplify Gen 2 da 30s al Lambda de Next.js. API Gateway REST tiene hard limit de 29s. Si tu endpoint debe correr más, necesitas Function URLs con streaming o mover la operación a background (SQS + Lambda).
Cuándo NO elegir ninguna de las dos
Si tu necesidad es tráfico mínimo con latencia baja garantizada (dashboards internos con 10 usuarios), Next.js puede ser overkill. Un Cloudflare Worker o Deno Deploy sale más barato y frío menos.
Si tus endpoints son esencialmente consultas GraphQL, AppSync con resolvers directos a DynamoDB es más eficiente que cualquier de los dos.
Si tu app tiene tanta lógica server que Next.js se siente forzado, considera un backend separado (NestJS, Go, lo que quieras) detrás de ALB o API Gateway, y Next.js solo hace frontend.
En el próximo artículo: cómo correr Playwright en AWS CodeBuild con contenedores custom para E2E serio. Configuración de browsers, paralelismo real, y artefactos que sirvan para debuggear fallas de tests de producción.
Top comments (0)