Volver al blog

Arquitectura Serverless en 2025: De la Teoría a la Práctica con JavaScript

Hola HaWkers, el mercado de serverless está proyectado para alcanzar $17.78 mil millones en 2025. Pero ¿qué significa esto para ti, desarrollador JavaScript que quiere construir aplicaciones escalables sin gestionar servidores?

En este artículo, vamos más allá del hype y exploramos cuándo usar serverless, cómo implementar correctamente y trampas comunes que pueden costar caro si son ignoradas.

Qué Es Serverless Realmente

Serverless no significa "sin servidores" — significa que tú no gestionas servidores. La infraestructura escala automáticamente y pagas solo por lo que usas.

Principales Proveedores en 2025

// Comparación de plataformas serverless populares
const serverlessPlatforms = {
  'AWS Lambda': {
    runtime: 'Node.js 20.x, Python, Go, Java, etc',
    coldStart: '~100-300ms (Node.js)',
    pricing: '$0.20 por 1M requests + compute',
    maxDuration: '15 minutes',
    bestFor: 'Backends complejos, integración AWS',
    limitations: 'Cold starts, complejidad inicial'
  },
  'Vercel Edge Functions': {
    runtime: 'Edge Runtime (V8 isolates)',
    coldStart: '~0ms (edge locations)',
    pricing: '$20/mes hasta 500k requests',
    maxDuration: '30 seconds (hobby), 5min (pro)',
    bestFor: 'APIs rápidas, middleware, SSR',
    limitations: 'No soporta todas APIs Node.js'
  },
  'Cloudflare Workers': {
    runtime: 'V8 isolates',
    coldStart: '~0ms (distribuido globalmente)',
    pricing: '$5/mes hasta 10M requests',
    maxDuration: '50-300ms (depende del plan)',
    bestFor: 'Edge computing, baja latencia',
    limitations: 'Límite de CPU time, sin filesystem'
  },
  'Azure Functions': {
    runtime: 'Node.js, Python, C#, Java',
    coldStart: '~200-500ms',
    pricing: '$0.20 por 1M requests + compute',
    maxDuration: '10 minutes',
    bestFor: 'Integración con Microsoft stack',
    limitations: 'Cold starts similares a AWS'
  },
  'Google Cloud Functions': {
    runtime: 'Node.js, Python, Go, Java',
    coldStart: '~150-400ms',
    pricing: '$0.40 por 1M requests + compute',
    maxDuration: '9 minutes',
    bestFor: 'Integración con GCP, Firebase',
    limitations: 'Pricing ligeramente más alto'
  }
};

Cuándo Usar Serverless: Casos de Uso Ideales

1. APIs con Tráfico Variable

// Ejemplo: API de e-commerce con picos estacionales
// AWS Lambda + API Gateway

export const handler = async (event) => {
  const { httpMethod, path, body, headers } = event;

  // Enrutamiento simple
  const routes = {
    'GET /products': getProducts,
    'GET /products/{id}': getProduct,
    'POST /orders': createOrder,
    'POST /checkout': processCheckout
  };

  const routeKey = `${httpMethod} ${path}`;
  const handlerFn = routes[routeKey];

  if (!handlerFn) {
    return {
      statusCode: 404,
      body: JSON.stringify({ error: 'Not found' })
    };
  }

  try {
    const result = await handlerFn(event);

    return {
      statusCode: 200,
      headers: {
        'Content-Type': 'application/json',
        'Access-Control-Allow-Origin': '*'
      },
      body: JSON.stringify(result)
    };
  } catch (error) {
    console.error('Handler error:', error);

    return {
      statusCode: 500,
      body: JSON.stringify({ error: 'Internal server error' })
    };
  }
};

async function getProducts(event) {
  // Cold start mitigation: Lazy load DB connection
  const db = await getDBConnection();

  const products = await db.query('SELECT * FROM products WHERE active = true');

  return {
    products,
    count: products.length,
    cached: false
  };
}

async function createOrder(event) {
  const orderData = JSON.parse(event.body);

  // Validación
  if (!orderData.customerId || !orderData.items) {
    throw new Error('Invalid order data');
  }

  const db = await getDBConnection();

  // Transacción
  const order = await db.transaction(async (trx) => {
    const [orderId] = await trx('orders').insert({
      customer_id: orderData.customerId,
      total: orderData.total,
      status: 'pending'
    });

    await trx('order_items').insert(
      orderData.items.map(item => ({
        order_id: orderId,
        product_id: item.productId,
        quantity: item.quantity,
        price: item.price
      }))
    );

    return orderId;
  });

  // Trigger asíncrono para procesamiento
  await triggerOrderProcessing(order);

  return { orderId: order };
}

// Connection pooling para reducir cold starts
let dbConnection = null;
async function getDBConnection() {
  if (!dbConnection) {
    const { Pool } = require('pg');
    dbConnection = new Pool({
      connectionString: process.env.DATABASE_URL,
      max: 1 // Lambda usa 1 conexión por instancia
    });
  }
  return dbConnection;
}

2. Edge Functions para Performance Global

// Vercel Edge Function para personalización de contenido
// Corre en edge locations cerca del usuario

export const config = {
  runtime: 'edge'
};

export default async function handler(request) {
  const { geo, nextUrl } = request;

  // Personalización basada en ubicación
  const userCountry = geo.country || 'US';
  const userCity = geo.city || 'Unknown';

  // A/B testing en el edge
  const variant = getABTestVariant(request);

  // Reescribir URL basado en contexto
  if (userCountry === 'BR') {
    nextUrl.pathname = `/br${nextUrl.pathname}`;
  }

  // Agregar headers customizados
  const response = await fetch(nextUrl, {
    headers: {
      'X-User-Country': userCountry,
      'X-User-City': userCity,
      'X-AB-Variant': variant
    }
  });

  // Modificar respuesta antes de enviar al usuario
  const html = await response.text();
  const modifiedHtml = html.replace(
    '<head>',
    `<head>
      <script>window.__USER_CONTEXT__ = ${JSON.stringify({ userCountry, userCity, variant })}</script>`
  );

  return new Response(modifiedHtml, {
    headers: {
      'Content-Type': 'text/html',
      'Cache-Control': 's-maxage=60, stale-while-revalidate'
    }
  });
}

function getABTestVariant(request) {
  const cookie = request.cookies.get('ab_test_variant');

  if (cookie) {
    return cookie.value;
  }

  // 50/50 split
  return Math.random() < 0.5 ? 'A' : 'B';
}

3. Background Jobs y Procesamiento Asíncrono

// AWS Lambda triggered por SQS para procesamiento de imágenes
export const handler = async (event) => {
  // SQS puede enviar múltiples mensajes en batch
  const records = event.Records;

  // Procesar en paralelo (respetando límites de memoria/CPU)
  const results = await Promise.all(
    records.map(async (record) => {
      try {
        const message = JSON.parse(record.body);
        await processImage(message);

        return { success: true, messageId: record.messageId };
      } catch (error) {
        console.error('Failed to process message:', error);

        // Retornar para retry (DLQ después de X intentos)
        throw error;
      }
    })
  );

  return { processedCount: results.length };
};

async function processImage(message) {
  const { imageUrl, userId, transformations } = message;

  // 1. Download imagen del S3
  const imageBuffer = await downloadFromS3(imageUrl);

  // 2. Aplicar transformaciones (resize, watermark, etc)
  const sharp = require('sharp');
  let image = sharp(imageBuffer);

  for (const transform of transformations) {
    if (transform.type === 'resize') {
      image = image.resize(transform.width, transform.height);
    } else if (transform.type === 'watermark') {
      image = image.composite([{
        input: await downloadWatermark(),
        gravity: 'southeast'
      }]);
    }
  }

  const processedBuffer = await image.toBuffer();

  // 3. Upload resultado de vuelta al S3
  const resultKey = `processed/${userId}/${Date.now()}.jpg`;
  await uploadToS3(resultKey, processedBuffer);

  // 4. Notificar usuario via SNS
  await notifyUser(userId, resultKey);

  console.log(`Image processed successfully: ${resultKey}`);
}

Patrones Arquitecturales Serverless

1. API Gateway + Lambda (Arquitectura Clásica)

// Estructura de proyecto serverless
const serverlessArchitecture = {
  'API Gateway': {
    purpose: 'Enrutamiento HTTP, rate limiting, auth',
    routes: [
      'GET /api/users → Lambda: getUsers',
      'POST /api/users → Lambda: createUser',
      'GET /api/orders → Lambda: getOrders'
    ]
  },
  'Lambda Functions': {
    deployment: 'Función por ruta (micro) o monolítica',
    layers: 'Compartir dependencias via Lambda Layers'
  },
  'DynamoDB / RDS': {
    choice: 'DynamoDB para NoSQL, Aurora Serverless para SQL',
    pattern: 'Connection pooling esencial'
  },
  'S3': {
    purpose: 'Storage de assets (imágenes, uploads)',
    trigger: 'Lambda puede ser triggered por S3 events'
  },
  'SQS / EventBridge': {
    purpose: 'Comunicación asíncrona entre servicios',
    pattern: 'Event-driven architecture'
  }
};

// Ejemplo de monorepo serverless
const projectStructure = `
/my-serverless-api
  /functions
    /users
      - get.js
      - create.js
      - update.js
    /orders
      - get.js
      - create.js
  /layers
    /database
      - connection.js
    /utils
      - validation.js
  /infrastructure
    - serverless.yml (o CDK)
  /tests
    - integration.test.js
`;

2. JAMstack con Serverless Backend

// Next.js App Router + Serverless Functions
// app/api/newsletter/route.ts

import { NextRequest, NextResponse } from 'next/server';

export async function POST(request: NextRequest) {
  try {
    const { email } = await request.json();

    // Validación
    if (!isValidEmail(email)) {
      return NextResponse.json(
        { error: 'Invalid email' },
        { status: 400 }
      );
    }

    // Agregar al newsletter (ej: Mailchimp, SendGrid)
    await addToNewsletter(email);

    // Trigger welcome email (asíncrono via queue)
    await queueWelcomeEmail(email);

    return NextResponse.json(
      { success: true, message: 'Subscribed successfully' },
      { status: 200 }
    );
  } catch (error) {
    console.error('Newsletter subscription error:', error);

    return NextResponse.json(
      { error: 'Failed to subscribe' },
      { status: 500 }
    );
  }
}

async function addToNewsletter(email: string) {
  // Ejemplo con API externa
  const response = await fetch('https://api.mailchimp.com/3.0/lists/{listId}/members', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.MAILCHIMP_API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      email_address: email,
      status: 'subscribed'
    })
  });

  if (!response.ok) {
    throw new Error('Failed to add to Mailchimp');
  }
}

function isValidEmail(email: string): boolean {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

Optimizaciones y Best Practices

1. Reducir Cold Starts

// Técnicas para mitigar cold starts

// 1. Provisioned Concurrency (AWS Lambda)
const serverlessConfig = {
  functions: {
    api: {
      handler: 'src/api.handler',
      provisionedConcurrency: 5, // Mantiene 5 instancias warm
      reservedConcurrency: 100   // Límite máximo de instancias
    }
  }
};

// 2. Lazy Loading de Dependencias
// ❌ Malo: Importar todo arriba
import AWS from 'aws-sdk';
import sharp from 'sharp';
import { Pool } from 'pg';

// ✅ Bueno: Importar solo cuando necesario
export const handler = async (event) => {
  if (event.action === 'image') {
    const sharp = require('sharp'); // Lazy load
    // procesar imagen
  } else if (event.action === 'database') {
    const { Pool } = require('pg');
    // query database
  }
};

// 3. Connection Reuse (Global Scope)
let dbPool = null;

export const handler = async (event) => {
  // Reusar conexión entre invocaciones (warm starts)
  if (!dbPool) {
    const { Pool } = require('pg');
    dbPool = new Pool({
      connectionString: process.env.DATABASE_URL,
      max: 1
    });
  }

  const client = await dbPool.connect();
  try {
    const result = await client.query('SELECT * FROM users');
    return result.rows;
  } finally {
    client.release();
  }
};

// 4. Bundle Size Optimization
// Usa esbuild o Webpack con tree-shaking
const esbuildConfig = {
  entryPoints: ['src/handler.ts'],
  bundle: true,
  minify: true,
  sourcemap: false,
  target: 'node20',
  platform: 'node',
  external: ['aws-sdk'], // Ya incluido en Lambda runtime
  outfile: 'dist/handler.js'
};

2. Gestión de Costos

// Monitoreo y optimización de costos serverless
class ServerlessCostOptimizer {
  async analyzeCosts() {
    // 1. Identificar funciones caras
    const expensiveFunctions = await this.getTopCostFunctions();

    // 2. Analizar patrones de uso
    for (const fn of expensiveFunctions) {
      const metrics = await this.getFunctionMetrics(fn.name);

      console.log(`Function: ${fn.name}`);
      console.log(`  Monthly cost: $${fn.cost}`);
      console.log(`  Invocations: ${metrics.invocations}`);
      console.log(`  Avg duration: ${metrics.avgDuration}ms`);
      console.log(`  Avg memory: ${metrics.avgMemory}MB`);

      // 3. Sugerencias de optimización
      const suggestions = this.generateSuggestions(metrics);
      console.log('  Suggestions:', suggestions);
    }
  }

  generateSuggestions(metrics) {
    const suggestions = [];

    // Memoria muy alta pero poco usada
    if (metrics.configuredMemory > metrics.avgMemory * 1.5) {
      suggestions.push(
        `Reduce memory from ${metrics.configuredMemory}MB to ${Math.ceil(metrics.avgMemory * 1.2)}MB`
      );
    }

    // Duración alta sugiere optimización de código
    if (metrics.avgDuration > 3000) {
      suggestions.push('Consider code optimization or caching');
    }

    // Alto número de cold starts
    if (metrics.coldStartRate > 0.1) {
      suggestions.push('Consider provisioned concurrency for critical paths');
    }

    return suggestions;
  }
}

// Prácticas de economía
const costSavingTips = {
  'Caching agresivo': {
    where: 'CloudFront, API Gateway, Lambda response',
    savings: '30-60% en invocaciones'
  },
  'Batch processing': {
    where: 'Procesar múltiples items por invocación',
    savings: '50-70% en costos'
  },
  'Memory tuning': {
    where: 'Ajustar memoria al necesario',
    savings: '20-40% en costos'
  },
  'Reserved capacity': {
    where: 'Funciones con tráfico previsible',
    savings: '30-50% en costos'
  }
};

Cuándo NO Usar Serverless

// Casos donde serverless puede no ser ideal
const serverlessLimitations = {
  'Long-running tasks': {
    problem: 'Límite de 15min (Lambda) o 30s (Edge)',
    alternative: 'ECS Fargate, Kubernetes Jobs'
  },
  'WebSockets persistentes': {
    problem: 'Difícil mantener conexiones abiertas',
    alternative: 'EC2, ECS con connection pooling'
  },
  'Procesamiento intenso de CPU': {
    problem: 'Costo puede ser alto vs servidor dedicado',
    alternative: 'EC2 con instancias optimizadas'
  },
  'Latencia ultra-baja crítica': {
    problem: 'Cold starts pueden afectar',
    alternative: 'Containers siempre-on o bare metal'
  },
  'Alto volumen constante': {
    problem: 'Puede ser más caro que servidor fijo',
    calculation: 'Break-even generalmente en 50-70% de utilización constante'
  }
};

Conclusión: Serverless en 2025

Serverless maduró. No es más hype — es herramienta esencial para:

  • Startups: Reduce costo inicial y acelera time-to-market
  • Scale-ups: Escala automáticamente con crecimiento
  • Empresas: Permite enfocar en features, no en infra

Si quieres entender mejor JavaScript asíncrono (esencial para serverless), recomiendo que des una mirada a otro artículo: JavaScript Asíncrono: Dominando Promises y Async/Await donde vas a descubrir las bases para trabajar con funciones serverless eficientemente.

¡Vamos a por ello! 🦅

💻 Domina JavaScript de Verdad

El conocimiento que adquiriste en este artículo es solo el comienzo. Hay técnicas, patrones y prácticas que transforman desarrolladores principiantes en profesionales requeridos.

Invierte en Tu Futuro

Preparé un material completo para que domines JavaScript:

Formas de pago:

  • $9.90 USD (pago único)

📖 Ver Contenido Completo

Comentarios (0)

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

Añadir comentarios