Volver al blog

Arquitectura Serverless: Cómo Reducir Costos y Escalar Infinitamente con JavaScript

Hola HaWkers, serverless dejó de ser tendencia futurista para convertirse en arquitectura estándar en 2025. Con el mercado SaaS superando $300 mil millones y gastos en cloud pública alcanzando $723.4 mil millones, entender serverless no es más opcional.

JavaScript, con su naturaleza event-driven, es perfectamente compatible con plataformas como AWS Lambda, Google Cloud Functions y Azure Functions. ¿Pero cómo aprovechar esto en la práctica?

Qué Es Serverless y Por Qué Importa

Serverless no significa "sin servidores" - significa que tú no gestionas servidores. La infraestructura es completamente abstraída:

// Modelo Tradicional - Tú gestionas todo
const traditionalModel = {
  infrastructure: 'Tú provisionas servidores',
  scaling: 'Tú configuras auto-scaling',
  availability: 'Tú garantizas uptime',
  costs: 'Pagas 24/7, incluso sin tráfico',
  maintenance: 'Patches, updates, seguridad = tu responsabilidad'
};

// Modelo Serverless - Cloud gestiona todo
const serverlessModel = {
  infrastructure: 'Provisionado automáticamente',
  scaling: 'Escala automáticamente (0 a millones)',
  availability: 'SLA de 99.95%+ garantizado',
  costs: 'Pagas SOLO por ejecuciones',
  maintenance: 'Gestionado por el provider'
};

// Economía real
const costComparison = {
  traditional: {
    server: 'EC2 t3.medium = ~$30/mes',
    usage: 'Ejecutando 24/7',
    traffic: '10.000 requests/mes',
    costPerRequest: '$30 / 10.000 = $0.003'
  },
  serverless: {
    lambda: '10.000 requests = $0.20',
    freeEjecuciones: '1M ejecuciones/mes free tier',
    costPerRequest: '$0.20 / 10.000 = $0.00002',
    savings: '99.3% economía vs tradicional'
  }
};

serverless scaling

AWS Lambda con Node.js: Implementación Práctica

Función Lambda Básica

// handler.js - AWS Lambda Function
export const handler = async (event) => {
  try {
    // Parse del body (si es POST)
    const body = event.body ? JSON.parse(event.body) : {};

    // Lógica de negocio
    const result = await processData(body);

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

    return {
      statusCode: 500,
      body: JSON.stringify({
        success: false,
        error: error.message
      })
    };
  }
};

async function processData(data) {
  // Tu lógica aquí
  return {
    processed: true,
    timestamp: new Date().toISOString()
  };
}

API REST Completa con Lambda

// api/users.js - CRUD de usuarios
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import {
  DynamoDBDocumentClient,
  GetCommand,
  PutCommand,
  DeleteCommand,
  ScanCommand
} from '@aws-sdk/lib-dynamodb';

const client = new DynamoDBClient({});
const dynamo = DynamoDBDocumentClient.from(client);

const TABLE_NAME = process.env.USERS_TABLE;

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

  try {
    switch (httpMethod) {
      case 'GET':
        return await getUser(pathParameters?.id);
      case 'POST':
        return await createUser(JSON.parse(body));
      case 'PUT':
        return await updateUser(pathParameters?.id, JSON.parse(body));
      case 'DELETE':
        return await deleteUser(pathParameters?.id);
      default:
        return response(405, { error: 'Method not allowed' });
    }
  } catch (error) {
    console.error(error);
    return response(500, { error: error.message });
  }
};

async function getUser(id) {
  if (!id) {
    // List all users
    const result = await dynamo.send(
      new ScanCommand({ TableName: TABLE_NAME })
    );
    return response(200, result.Items);
  }

  // Get specific user
  const result = await dynamo.send(
    new GetCommand({
      TableName: TABLE_NAME,
      Key: { id }
    })
  );

  return response(200, result.Item);
}

async function createUser(data) {
  const user = {
    id: crypto.randomUUID(),
    ...data,
    createdAt: new Date().toISOString()
  };

  await dynamo.send(
    new PutCommand({
      TableName: TABLE_NAME,
      Item: user
    })
  );

  return response(201, user);
}

async function updateUser(id, data) {
  const user = {
    id,
    ...data,
    updatedAt: new Date().toISOString()
  };

  await dynamo.send(
    new PutCommand({
      TableName: TABLE_NAME,
      Item: user
    })
  );

  return response(200, user);
}

async function deleteUser(id) {
  await dynamo.send(
    new DeleteCommand({
      TableName: TABLE_NAME,
      Key: { id }
    })
  );

  return response(204, {});
}

function response(statusCode, body) {
  return {
    statusCode,
    headers: {
      'Content-Type': 'application/json',
      'Access-Control-Allow-Origin': '*'
    },
    body: JSON.stringify(body)
  };
}

Serverless Framework: Deploy Simplificado

# serverless.yml
service: my-api

provider:
  name: aws
  runtime: nodejs20.x
  region: us-east-1
  environment:
    USERS_TABLE: ${self:service}-users-${self:provider.stage}
  iam:
    role:
      statements:
        - Effect: Allow
          Action:
            - dynamodb:Query
            - dynamodb:Scan
            - dynamodb:GetItem
            - dynamodb:PutItem
            - dynamodb:UpdateItem
            - dynamodb:DeleteItem
          Resource: "arn:aws:dynamodb:${self:provider.region}:*:table/${self:provider.environment.USERS_TABLE}"

functions:
  api:
    handler: api/users.handler
    events:
      - httpApi:
          path: /users
          method: GET
      - httpApi:
          path: /users/{id}
          method: GET
      - httpApi:
          path: /users
          method: POST
      - httpApi:
          path: /users/{id}
          method: PUT
      - httpApi:
          path: /users/{id}
          method: DELETE

resources:
  Resources:
    UsersTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: ${self:provider.environment.USERS_TABLE}
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: S
        KeySchema:
          - AttributeName: id
            KeyType: HASH
        BillingMode: PAY_PER_REQUEST
# Deploy completo con un comando
serverless deploy

# Logs en tiempo real
serverless logs -f api --tail

# Remover stack completa
serverless remove

Casos de Uso Perfectos para Serverless

1. APIs REST/GraphQL

// GraphQL con Apollo Server en Lambda
import { ApolloServer } from '@apollo/server';
import { startServerAndCreateLambdaHandler } from '@as-integrations/aws-lambda';

const typeDefs = `#graphql
  type User {
    id: ID!
    name: String!
    email: String!
  }

  type Query {
    users: [User!]!
    user(id: ID!): User
  }

  type Mutation {
    createUser(name: String!, email: String!): User!
  }
`;

const resolvers = {
  Query: {
    users: async () => {
      // Buscar de DynamoDB
      return await getAllUsers();
    },
    user: async (_, { id }) => {
      return await getUserById(id);
    }
  },
  Mutation: {
    createUser: async (_, { name, email }) => {
      return await createNewUser({ name, email });
    }
  }
};

const server = new ApolloServer({
  typeDefs,
  resolvers
});

export const handler = startServerAndCreateLambdaHandler(server);

2. Procesamiento de Archivos

// Lambda triggered por upload S3
import { S3Client, GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
import sharp from 'sharp';

const s3 = new S3Client({});

export const handler = async (event) => {
  // Event de S3 upload
  const bucket = event.Records[0].s3.bucket.name;
  const key = decodeURIComponent(event.Records[0].s3.object.key);

  try {
    // Download de la imagen original
    const { Body } = await s3.send(
      new GetObjectCommand({ Bucket: bucket, Key: key })
    );

    const imageBuffer = await streamToBuffer(Body);

    // Redimensionar para diferentes tamaños
    const sizes = [
      { name: 'thumbnail', width: 150 },
      { name: 'medium', width: 500 },
      { name: 'large', width: 1200 }
    ];

    await Promise.all(
      sizes.map(async ({ name, width }) => {
        const resized = await sharp(imageBuffer)
          .resize(width)
          .jpeg({ quality: 80 })
          .toBuffer();

        const newKey = key.replace(/\.\w+$/, `-${name}.jpg`);

        await s3.send(
          new PutObjectCommand({
            Bucket: bucket,
            Key: newKey,
            Body: resized,
            ContentType: 'image/jpeg'
          })
        );
      })
    );

    return { statusCode: 200, body: 'Images processed successfully' };
  } catch (error) {
    console.error(error);
    throw error;
  }
};

async function streamToBuffer(stream) {
  const chunks = [];
  for await (const chunk of stream) {
    chunks.push(chunk);
  }
  return Buffer.concat(chunks);
}

3. Scheduled Tasks (Cron Jobs)

// Lambda ejecutada diariamente
export const handler = async (event) => {
  console.log('Running daily cleanup task...');

  try {
    // Limpiar datos antiguos
    await cleanupOldData();

    // Enviar reporte
    await sendDailyReport();

    // Backup de datos importantes
    await backupCriticalData();

    return {
      statusCode: 200,
      body: JSON.stringify({ message: 'Daily tasks completed' })
    };
  } catch (error) {
    console.error('Daily task error:', error);
    await notifyAdmins(error);
    throw error;
  }
};

async function cleanupOldData() {
  // Deletar registros con más de 90 días
  const ninetyDaysAgo = new Date();
  ninetyDaysAgo.setDate(ninetyDaysAgo.getDate() - 90);

  // Implementación específica
}

async function sendDailyReport() {
  // Generar y enviar reporte por email
}

async function backupCriticalData() {
  // Backup para S3 Glacier
}

async function notifyAdmins(error) {
  // SNS o SES para notificación
}

Ventajas y Desventajas

✅ Ventajas

  1. Costo: Paga solo por lo que usas
  2. Escalabilidad: Automática e infinita
  3. Mantenimiento: Zero overhead operacional
  4. Deploy: Extremadamente rápido
  5. Seguridad: Gestionada por el provider

⚠️ Desventajas

  1. Cold Start: ~100-500ms en la primera ejecución
  2. Tiempo Límite: AWS Lambda = 15 min máximo
  3. Vendor Lock-in: Código acoplado al provider
  4. Debugging: Más complejo que tradicional
  5. Costos en Alto Volumen: Puede volverse caro

Cuándo Usar Serverless

Usa Serverless cuando:

  • Tráfico variable/impredecible
  • Procesamiento batch/background
  • APIs con bajo/medio tráfico
  • Webhooks e integraciones
  • Prototipos y MVPs

Evita Serverless cuando:

  • Procesamiento muy largo (>15 min)
  • Tráfico altísimo y constante
  • Necesita state en memoria
  • Latencia crítica (<10ms)

Si quieres entender más sobre cómo optimizar código JavaScript para ambientes serverless, lee Performance en JavaScript: Técnicas de Optimización Avanzadas donde descubrirás cómo escribir código más eficiente.

¡Vamos a por ello! 🦅

💻 Domina JavaScript Para el Cloud

Serverless es JavaScript puro ejecutado en la cloud. Cuanto mejor sea tu JavaScript, más aprovechas serverless.

Si quieres construir una base sólida en 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