Arquitectura Serverless con JavaScript: AWS Lambda, Vercel y Edge Functions en 2025
Hola HaWkers, serverless se estableció como arquitectura preferida para muchos casos de uso. En 2025, con Edge Functions maduras y costos optimizados, entender serverless es esencial para desarrolladores JavaScript.
¿Ya consideraste cómo serverless puede simplificar tu infraestructura y reducir costos? Vamos a explorar las principales plataformas y patrones de arquitectura.
Por Qué Serverless en 2025
Beneficios
Operacionales:
- Zero gestión de servidores
- Escalado automático
- Pay-per-use (paga solo lo que usa)
- Alta disponibilidad built-in
Desarrollo:
- Deploy rápido
- Focus en código, no infraestructura
- Fácil integración con servicios cloud
- Ideal para microservices
Cuándo Usar
Ideal para:
- APIs REST/GraphQL
- Webhooks y integraciones
- Procesamiento de eventos
- Tareas background/scheduled
- MVPs y prototipos rápidos
Menos ideal para:
- Aplicaciones con estado persistente
- Conexiones de larga duración (WebSockets)
- Cargas de trabajo constantes y predecibles
- Aplicaciones que necesitan GPU
AWS Lambda con JavaScript
Setup Básico
// handler.js
export const handler = async (event, context) => {
try {
const body = JSON.parse(event.body || '{}');
// Tu lógica aquí
const result = await processData(body);
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify(result),
};
} catch (error) {
console.error('Error:', error);
return {
statusCode: 500,
body: JSON.stringify({ error: 'Internal Server Error' }),
};
}
};
async function processData(data) {
// Procesamiento
return { processed: true, data };
}Configuración con SAM (Serverless Application Model)
# template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Globals:
Function:
Timeout: 30
Runtime: nodejs20.x
MemorySize: 256
Environment:
Variables:
NODE_ENV: production
Resources:
ApiFunction:
Type: AWS::Serverless::Function
Properties:
Handler: src/handlers/api.handler
Events:
Api:
Type: Api
Properties:
Path: /api/{proxy+}
Method: ANY
ProcessorFunction:
Type: AWS::Serverless::Function
Properties:
Handler: src/handlers/processor.handler
Events:
SQSEvent:
Type: SQS
Properties:
Queue: !GetAtt ProcessingQueue.Arn
BatchSize: 10
ProcessingQueue:
Type: AWS::SQS::Queue
Properties:
VisibilityTimeout: 180
Outputs:
ApiEndpoint:
Description: API Gateway endpoint
Value: !Sub 'https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/'Patrones Comunes AWS Lambda
// src/handlers/api.js
import { DynamoDB } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocument } from '@aws-sdk/lib-dynamodb';
const client = new DynamoDB({});
const docClient = DynamoDBDocument.from(client);
// CRUD API Handler
export const handler = async (event) => {
const { httpMethod, path, pathParameters, body } = event;
try {
switch (httpMethod) {
case 'GET':
if (pathParameters?.id) {
return await getItem(pathParameters.id);
}
return await listItems();
case 'POST':
return await createItem(JSON.parse(body));
case 'PUT':
return await updateItem(pathParameters.id, JSON.parse(body));
case 'DELETE':
return await deleteItem(pathParameters.id);
default:
return response(405, { error: 'Method not allowed' });
}
} catch (error) {
console.error('Error:', error);
return response(500, { error: 'Internal server error' });
}
};
async function getItem(id) {
const result = await docClient.get({
TableName: process.env.TABLE_NAME,
Key: { id },
});
if (!result.Item) {
return response(404, { error: 'Item not found' });
}
return response(200, result.Item);
}
async function listItems() {
const result = await docClient.scan({
TableName: process.env.TABLE_NAME,
Limit: 100,
});
return response(200, { items: result.Items });
}
async function createItem(data) {
const item = {
id: crypto.randomUUID(),
...data,
createdAt: new Date().toISOString(),
};
await docClient.put({
TableName: process.env.TABLE_NAME,
Item: item,
});
return response(201, item);
}
function response(statusCode, body) {
return {
statusCode,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify(body),
};
}
Vercel Functions
Setup con Next.js
// app/api/users/route.ts
import { NextResponse } from 'next/server';
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const page = parseInt(searchParams.get('page') || '1');
const users = await fetchUsers(page);
return NextResponse.json(users);
}
export async function POST(request: Request) {
const body = await request.json();
// Validación
if (!body.email || !body.name) {
return NextResponse.json(
{ error: 'Email y nombre son obligatorios' },
{ status: 400 }
);
}
const user = await createUser(body);
return NextResponse.json(user, { status: 201 });
}// app/api/users/[id]/route.ts
import { NextResponse } from 'next/server';
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const user = await getUser(params.id);
if (!user) {
return NextResponse.json(
{ error: 'Usuario no encontrado' },
{ status: 404 }
);
}
return NextResponse.json(user);
}
export async function PUT(
request: Request,
{ params }: { params: { id: string } }
) {
const body = await request.json();
const user = await updateUser(params.id, body);
return NextResponse.json(user);
}
export async function DELETE(
request: Request,
{ params }: { params: { id: string } }
) {
await deleteUser(params.id);
return new NextResponse(null, { status: 204 });
}Configuración de Runtime
// app/api/heavy-task/route.ts
// Aumentar timeout y memoria
export const runtime = 'nodejs';
export const maxDuration = 60; // 60 segundos (Pro plan)
export async function POST(request: Request) {
const data = await request.json();
// Tarea pesada que puede tomar tiempo
const result = await processHeavyTask(data);
return Response.json(result);
}
Edge Functions
Edge Functions ejecutan en la edge de la CDN, más cerca del usuario.
Vercel Edge Functions
// app/api/geo/route.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export const runtime = 'edge';
export async function GET(request: NextRequest) {
const geo = request.geo;
const ip = request.ip;
return NextResponse.json({
ip,
city: geo?.city,
country: geo?.country,
region: geo?.region,
latitude: geo?.latitude,
longitude: geo?.longitude,
});
}// middleware.ts (Ejecuta en la edge para todas las requests)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// Redirect basado en geolocalización
const country = request.geo?.country || 'US';
if (country === 'BR' && !request.nextUrl.pathname.startsWith('/pt')) {
return NextResponse.redirect(new URL('/pt' + request.nextUrl.pathname, request.url));
}
// A/B Testing
const bucket = Math.random() < 0.5 ? 'A' : 'B';
const response = NextResponse.next();
response.cookies.set('ab-bucket', bucket);
return response;
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};Cloudflare Workers
// worker.js
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
// Routing
if (url.pathname === '/api/data') {
return handleData(request, env);
}
if (url.pathname.startsWith('/api/')) {
return new Response('Not Found', { status: 404 });
}
// Proxy para origen
return fetch(request);
},
};
async function handleData(request, env) {
// Usar KV Storage
const cached = await env.KV.get('data', 'json');
if (cached) {
return Response.json(cached);
}
// Fetch de origen y cachear
const data = await fetchFromOrigin();
await env.KV.put('data', JSON.stringify(data), { expirationTtl: 3600 });
return Response.json(data);
}
Patrones de Arquitectura Serverless
1. API Gateway + Lambda
Cliente → API Gateway → Lambda → DynamoDB
→ S3
→ Otros servicios// Patrón de validación y procesamiento
export const handler = async (event) => {
// 1. Validar input
const validation = validateInput(event.body);
if (!validation.valid) {
return errorResponse(400, validation.errors);
}
// 2. Autenticar/Autorizar
const auth = await authenticate(event.headers);
if (!auth.valid) {
return errorResponse(401, 'Unauthorized');
}
// 3. Procesar
const result = await process(validation.data, auth.user);
// 4. Responder
return successResponse(result);
};2. Event-Driven Architecture
S3 Upload → Lambda → SQS → Lambda → Notificación
→ DynamoDB// Procesador de eventos S3
export const handler = async (event) => {
for (const record of event.Records) {
const bucket = record.s3.bucket.name;
const key = decodeURIComponent(record.s3.object.key);
console.log(`Processing ${key} from ${bucket}`);
// Procesar archivo
const file = await s3.getObject({ Bucket: bucket, Key: key });
const processed = await processFile(file.Body);
// Guardar resultado
await db.put({
TableName: 'ProcessedFiles',
Item: {
key,
processedAt: new Date().toISOString(),
result: processed,
},
});
// Notificar
await sns.publish({
TopicArn: process.env.NOTIFICATION_TOPIC,
Message: JSON.stringify({ key, status: 'processed' }),
});
}
};3. Scheduled Tasks (Cron)
# serverless.yml
functions:
dailyCleanup:
handler: src/cron/cleanup.handler
events:
- schedule: cron(0 2 * * ? *) # Todo día a las 2am UTC
hourlySync:
handler: src/cron/sync.handler
events:
- schedule: rate(1 hour)// src/cron/cleanup.js
export const handler = async () => {
console.log('Starting daily cleanup...');
// Limpiar items antiguos
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - 30);
await db.deleteOlderThan(cutoffDate);
// Limpiar S3
await s3.deleteOldFiles('temp-bucket', 7);
console.log('Cleanup completed');
return { success: true };
};
Optimización de Cold Starts
Técnicas de Reducción
// 1. Imports lazy (solo cargar cuando necesario)
let heavyModule;
export const handler = async (event) => {
if (event.path === '/heavy-operation') {
// Solo cargar si va a usar
heavyModule = heavyModule || await import('./heavy-module.js');
return heavyModule.process(event);
}
// Operaciones livianas no cargan el módulo pesado
return lightOperation(event);
};// 2. Conexiones fuera del handler
import { createPool } from 'mysql2/promise';
// Crear pool una vez (reutilizado entre invocaciones)
const pool = createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
waitForConnections: true,
connectionLimit: 10,
});
export const handler = async (event) => {
// Reutiliza conexión existente
const [rows] = await pool.query('SELECT * FROM users');
return { statusCode: 200, body: JSON.stringify(rows) };
};// 3. Provisioned Concurrency (AWS Lambda)
// template.yaml
Resources:
MyFunction:
Type: AWS::Serverless::Function
Properties:
Handler: index.handler
AutoPublishAlias: live
ProvisionedConcurrencyConfig:
ProvisionedConcurrentExecutions: 5Edge Functions vs Regional Functions
// Edge: Ultra-baja latencia, límites más restrictivos
export const runtime = 'edge';
// Bueno para:
// - Redirects y rewrites
// - A/B testing
// - Personalización por geolocalización
// - Respuestas simples
// Regional: Más recursos, mayor latencia
export const runtime = 'nodejs';
export const maxDuration = 60;
// Bueno para:
// - Operaciones de banco de datos
// - Procesamiento pesado
// - Integraciones complejasCostos y Buenas Prácticas
Optimización de Costos
// 1. Minimizar duración de execución
export const handler = async (event) => {
// Usar Promise.all para operaciones paralelas
const [users, products, orders] = await Promise.all([
fetchUsers(),
fetchProducts(),
fetchOrders(),
]);
return { users, products, orders };
};
// 2. Caching agressivo
const cache = new Map();
export const handler = async (event) => {
const cacheKey = event.queryStringParameters?.key;
if (cache.has(cacheKey)) {
return cache.get(cacheKey);
}
const result = await expensiveOperation();
cache.set(cacheKey, result);
return result;
};
// 3. Rightsizing de memoria
// Más memoria = CPU más rápido = menor duración
// Encontrar el punto óptimo através de testsConclusión
Serverless es una opción excelente para muchos casos de uso en 2025. Con las herramientas y patrones correctos, puedes construir aplicaciones escalables, económicas y fáciles de mantener.
Puntos clave:
- Elegir entre regional y edge basado en el caso de uso
- Optimizar cold starts con técnicas apropiadas
- Usar event-driven architecture para desacoplamiento
- Monitorear costos y optimizar continuamente
Si quieres profundizar en otras arquitecturas modernas, recomiendo que veas otro artículo: Monorepos con Turborepo y Nx donde vas a descubrir cómo organizar proyectos de escala.

