Volver al blog

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: 5

Edge 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 complejas

Costos 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 tests

Conclusió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.

¡Vamos a por ello! 🦅

Comentarios (0)

Este artículo aún no tiene comentarios 😢. ¡Sé el primero! 🚀🦅

Añadir comentarios