Selecciona el idioma

Guía Completa: API Serverless con AWS Lambda y DDD | TypeScript
lunes, 3 de noviembre de 2025 17 minutos de lectura

💡 Introducción a la Arquitectura Serverless

La arquitectura serverless (sin servidor) es un paradigma de computación en la nube donde el proveedor cloud gestiona completamente la infraestructura de servidores. A pesar del nombre, sí existen servidores físicos, pero como desarrolladores no tenemos que preocuparnos por su aprovisionamiento, escalado o mantenimiento.

En un modelo serverless, escribimos funciones que se ejecutan en respuesta a eventos (peticiones HTTP, mensajes en colas, eventos programados, etc.) y solo pagamos por el tiempo de ejecución real de estas funciones. AWS Lambda, Google Cloud Functions y Azure Functions son ejemplos populares de plataformas serverless.

✅ Ventajas de Serverless

  1. 📈 Escalado automático: Las funciones escalan automáticamente según la demanda, desde cero hasta miles de ejecuciones concurrentes sin intervención manual.

  2. 💰 Modelo de costos pay-per-use: Solo pagas por el tiempo de ejecución real de tus funciones (medido en milisegundos) y el número de invocaciones. Si no hay tráfico, no hay costos de infraestructura.

  3. 🔧 Menor complejidad operacional: No hay que gestionar servidores, parches de seguridad, balanceadores de carga ni configuraciones de red complejas.

  4. ⚡ Time-to-market reducido: Permite enfocarse en la lógica de negocio sin preocuparse por la infraestructura subyacente.

  5. 🛡️ Alta disponibilidad integrada: Los proveedores cloud garantizan alta disponibilidad y redundancia en múltiples zonas.

⚠️ Inconvenientes y Desafíos

  1. ❄️ Cold starts: Cuando una función no ha sido invocada recientemente, puede experimentar latencia adicional en el primer arranque (cold start). Esto puede afectar la experiencia del usuario en aplicaciones con requisitos estrictos de latencia.

  2. ⏱️ Límites de ejecución: AWS Lambda tiene un timeout máximo de 15 minutos. No es adecuado para procesos de larga duración.

  3. 🔒 Vendor lock-in: El código puede quedar acoplado a servicios específicos del proveedor cloud, dificultando migraciones futuras.

  4. 🐛 Debugging y observabilidad: El debugging local puede ser más complejo que en aplicaciones tradicionales. Es crucial tener una buena estrategia de logging y monitoring.

  5. 🔀 Complejidad en arquitecturas grandes: Gestionar decenas o cientos de funciones Lambda puede volverse complejo sin las herramientas y prácticas adecuadas.

🎯 Aspectos Clave a Considerar

  • 🔄 Diseño para fallos: Las funciones deben ser idempotentes y manejar reintentos correctamente.
  • 💾 Gestión de estado: Las funciones serverless son stateless por naturaleza. El estado debe gestionarse en servicios externos (bases de datos, caches, etc.).
  • 📦 Optimización del bundle: Mantener el tamaño de las funciones pequeño reduce los cold starts.
  • 📊 Monitoreo y logging: Implementar observabilidad desde el principio es crítico.
  • 🔐 Gestión de secretos: Usar servicios como AWS Secrets Manager o Parameter Store para credenciales.

🏗️ Construyendo una API Serverless Completa

Para demostrar cómo construir una aplicación serverless robusta y escalable, he desarrollado un proyecto de ejemplo que implementa una API REST con funcionalidades avanzadas como cron jobs y eventos asíncronos.

🛠️ Stack Tecnológico

El proyecto utiliza una combinación moderna de tecnologías:

⚙️ Framework y Runtime:

  • Serverless Framework: Para definir infraestructura como código y desplegar a AWS
  • Node.js 20.x: Runtime de AWS Lambda
  • TypeScript: Desarrollo type-safe con ES modules
  • Hono: Framework web ligero y rápido para manejar routing y middleware

🏛️ Arquitectura y Patrones:

  • Domain-Driven Design (DDD): Arquitectura en capas bien definidas
  • InversifyJS: Contenedor de inversión de dependencias
  • Zod: Validación de esquemas en runtime

🔨 Build Tools:

  • tsup: Bundler rápido basado en esbuild para compilar TypeScript
  • ESLint + Prettier: Calidad y formato de código

🏛️ Arquitectura del Proyecto

El proyecto sigue una arquitectura Domain-Driven Design (DDD) con separación clara de responsabilidades en cuatro capas:

src/
├── shared/                          # Infraestructura compartida
│   ├── domain/                      # Tipos base (Entity, errors)
│   ├── application/                 # Servicios core (Logger, Context)
│   ├── infra/                       # Implementaciones (AWS Event Bidge, Logger)
│   └── presentation/                # Factories y base classes (Lambda handlers, Cron, Events)

├── book/                            # Módulo de dominio: Books
│   ├── domain/
│   │   ├── entities/                # Book.entity.ts
│   │   └── repositories/            # BookRepo (abstracto)
│   ├── application/                 # Use cases (crear, listar, actualizar books)
│   ├── infra/                       # MemoryBookRepo (implementación)
│   └── presentation/
│       ├── routers/                 # BookRouter (rutas HTTP)
│       ├── functions/
│       │   ├── http/                # book.http.ts (Lambda handler)
│       │   └── event/               # published-book.event.ts (EventBridge handler)
│       └── dtos/                    # Schemas Zod para validación

└── author/                          # Módulo de dominio: Authors
    ├── domain/
    ├── application/
    ├── infra/
    └── presentation/
        ├── routers/                 # AuthorRouter
        ├── functions/
        │   ├── http/                # author.http.ts
        │   └── cron/                # authors-list.cron.ts (Cron job)
        └── dtos/

📚 Capas de la Arquitectura DDD

1️⃣ Domain Layer (Capa de Dominio)

  • Contiene la lógica de negocio pura
  • Define entidades (Book, Author) que extienden de una clase base Entity
  • Define interfaces de repositorios (contratos abstractos)
  • Sin dependencias externas, solo lógica de negocio

2️⃣ Application Layer (Capa de Aplicación)

  • Orquesta la lógica de negocio a través de Use Cases
  • Cada operación de negocio es un caso de uso independiente
  • Ejemplo: BookCreationUseCase, AuthorListUseCase
  • Solo depende de la capa de dominio

3️⃣ Infrastructure Layer (Capa de Infraestructura)

  • Implementaciones concretas de los repositorios
  • MemoryBookRepo implementa BookRepo
  • Mapea entre modelos de base de datos y entidades de dominio
  • Gestiona conexiones y transacciones

4️⃣ Presentation Layer (Capa de Presentación)

  • Expone la API a través de routers HTTP
  • Define DTOs (Data Transfer Objects) con schemas Zod
  • Contiene los Lambda handlers (entry points)
  • Transforma DTOs ↔ Entidades de dominio

✨ Esta arquitectura garantiza:

  • ✅ Testabilidad: Cada capa se puede testear independientemente
  • 🔧 Mantenibilidad: Cambios en infraestructura no afectan la lógica de negocio
  • 🔄 Flexibilidad: Fácil cambiar implementaciones

⚡ Sistema de Build Multi-Handler

Una de las características clave es el sistema de build automático que descubre y compila múltiples Lambda handlers:

// tsup.config.ts
import { glob } from 'glob';

const httpHandlers = glob.sync('src/*/presentation/functions/http/*.http.ts');
const cronHandlers = glob.sync('src/*/presentation/functions/cron/*.cron.ts');
const eventHandlers = glob.sync('src/*/presentation/functions/event/*.event.ts');

export default defineConfig({
  entry: [...httpHandlers, ...cronHandlers, ...eventHandlers],
  format: ['cjs'],
  outDir: 'dist',
  // Preserva la estructura de carpetas
  outExtension: () => ({ js: '.cjs' }),
  // ...
});

Cada archivo *.http.ts, *.cron.ts o *.event.ts se convierte automáticamente en un Lambda handler independiente:

  • src/book/presentation/functions/http/book.http.tsdist/book/functions/http/book.cjs
  • src/author/presentation/functions/cron/authors-list.cron.tsdist/author/functions/cron/authors-list.cjs
  • src/book/presentation/functions/event/published-book.event.tsdist/book/functions/event/published-book.cjs

Esto permite escalar la aplicación agregando nuevos handlers sin modificar la configuración de build.

📊 Implementación del Modelo de Datos

El proyecto implementa dos dominios de ejemplo: 📚 Books y ✍️ Authors.

Entidad de Dominio:

// src/book/domain/entities/book.entity.ts
import { Entity, EntityProps } from '@/shared/domain/types/entity.type';

export class Book extends Entity {
  authorId: string;

  isPublished: boolean;

  title: string;

  constructor(params: Partial<EntityProps> & Pick<Book, 'authorId' | 'isPublished' | 'title'>) {
    super(params);
    this.authorId = params.authorId;
    this.isPublished = params.isPublished;
    this.title = params.title;
  }
}

La clase base Entity proporciona campos comunes (id, createdAt, updatedAt) que todas las entidades heredan.

🌐 API REST con AWS Lambda y API Gateway

⚡ Lambda HTTP Handlers

Cada módulo de dominio tiene su propio Lambda handler para operaciones HTTP:

// src/book/presentation/functions/http/book.http.ts
import { createHttpHandler } from '@/shared/presentation/lambda-handler-factory';
import { BookRouter } from '@/book/presentation/routers/book.router';

export const handler = createHttpHandler(BookRouter.name);

El factory createHttpHandler:

  1. Resuelve el router desde el contenedor de DI
  2. Configura una app Hono con middleware (CORS, logging, error handling)
  3. Retorna un handler compatible con AWS Lambda
  4. Usa inicialización lazy para evitar top-level await (compatibilidad con CommonJS)

Router Implementation:

// src/book/presentation/routers/book.router.ts
import { zValidator } from '@hono/zod-validator';

@injectable()
export class BookRouter extends HonoRouter {
  constructor(
    @inject(BookRepo.name) private readonly bookRepo: BookRepo,
    @inject(EventPublisher.name) private readonly eventPublisher: EventPublisher,
    @inject(LoggerService.name) private readonly logger: LoggerService,
  ) {
    super({ basePath: '/api/books' });
  }

  run(app: Hono<HonoEnv>): void {
    // GET /api/books
    app.get('/', zValidator('query', BooksListGet), async (c) => {
      const { limit, authorId, title, skip } = c.req.valid('query');
      const books = await this.bookRepo.findMany({ authorId, title, limit, skip });
      return c.json(books);
    });

    // GET /api/books/:bookId
    app.get('/:bookId', async (c) => {
      const bookId = c.req.param('bookId');
      const book = await this.bookRepo.findOneById(bookId);

      if (!book) {
        throw new BookError.NotFoundById(`Book not found by id: ${bookId}`);
      }

      return c.json({ book });
    });

    // POST /api/books
    app.post('/', zValidator('json', BookPost), async (c) => {
      const data = c.req.valid('json');

      const book = new Book({
        authorId: data.authorId,
        isPublished: data.isPublished,
        title: data.title,
      });

      await this.bookRepo.create(book);

      // Publicar evento si el libro se crea como publicado
      if (book.isPublished) {
        await this.eventPublisher.publish({
          source: 'custom.books',
          detailType: 'BookPublished',
          detail: { book },
        });
      }

      return c.json({ book }, 201);
    });

    // ...
}

Configuración en Serverless Framework

# serverless.yml
functions:
  bookHandler:
    handler: dist/book/functions/http/book.handler
    events:
      - httpApi:
          path: /api/books
          method: '*'
      - httpApi:
          path: /api/books/{proxy+}
          method: '*'

  authorHandler:
    handler: dist/author/functions/http/author.handler
    events:
      - httpApi:
          path: /api/authors
          method: '*'
      - httpApi:
          path: /api/authors/{proxy+}
          method: '*'

El uso de {proxy+} permite que un solo Lambda maneje todas las sub-rutas (ej: /api/books, /api/books/123, /api/books/123/publish).

✨ Ventajas de este enfoque:

  • 🎯 Cada dominio tiene su propio Lambda (aislamiento, despliegues independientes)
  • ⚡ Cold starts reducidos por función más pequeña
  • 📈 Escalado independiente por dominio
  • 🔍 Fácil monitoreo y debugging por dominio

⏰ Cron Jobs con AWS EventBridge Scheduler

Para tareas programadas, el proyecto usa AWS EventBridge Scheduler (anteriormente CloudWatch Events).

Ejemplo: Listar autores periódicamente

// src/author/presentation/crons/authors-list.cron.ts
import { inject, injectable } from 'inversify';
import { CronJob } from '@/shared/presentation/cron-job';
import { AuthorRepo } from '@/author/domain/repositories/author.repository';

@injectable()
export class AuthorsListCron extends CronJob {
  protected readonly cronName = 'AuthorsListCron';

  constructor(@inject(AuthorRepo.name) private readonly authorRepo: AuthorRepo) {
    super();
  }

  protected async run(): Promise<void> {
    this.logger.info('Executing authors list cron logic...');

    const authors = await this.authorRepo.findMany({});

    authors.results.forEach(author => {
      this.logger.info(`There is an author "${author.name}" with id: ${author.id}`);
    });
  }
}

La clase base CronJob proporciona:

  • Logger automático
  • Context service para tracking
  • Manejo de errores y logging estructurado
  • Lifecycle hooks (before, run, after)

Lambda Handler:

// src/author/presentation/functions/cron/authors-list.cron.ts
import { createCronHandler } from '@/shared/presentation/lambda-handler-factory';
import { AuthorsListCron } from '@/author/presentation/crons/authors-list.cron';

export const handler = createCronHandler(AuthorsListCron.name);

Configuración del Schedule:

# serverless.yml
functions:
  authorsListCron:
    handler: dist/author/functions/cron/authors-list.handler
    events:
      - schedule:
          rate: cron(0 2 * * ? *)  # Cada día a las 2 AM UTC
          description: 'Daily authors list job'

EventBridge Scheduler soporta expresiones cron estándar y rates (rate(1 hour), rate(30 minutes)).

📡 Eventos Asíncronos con AWS EventBridge

Para comunicación asíncrona entre servicios, el proyecto utiliza AWS EventBridge en lugar de alternativas como SNS o SQS.

🤔 ¿Por qué EventBridge en lugar de SNS?

Amazon SNS (Simple Notification Service):

  • Servicio pub/sub básico
  • Los suscriptores reciben todos los mensajes del topic
  • Filtrado limitado (solo a nivel de atributos de mensaje)
  • Ideal para notificaciones simples (emails, SMS, push)

Amazon EventBridge:

  • Bus de eventos empresarial con capacidades avanzadas
  • Filtrado basado en contenido: Reglas complejas con pattern matching
  • Schema Registry: Descubrimiento y versionado de eventos
  • Transformación de eventos: Modificar payloads antes de enviar a destinos
  • Múltiples destinos: Un evento puede ir a Lambda, SQS, Step Functions, etc.
  • Integración nativa: Soporte para eventos de +90 servicios AWS
  • Replay de eventos: Reenviar eventos históricos (útil para debugging)
  • Cross-account/cross-region: Eventos entre cuentas y regiones AWS

✨ Ventajas de EventBridge para este proyecto:

  1. 🔌 Desacoplamiento real: Los publicadores no necesitan conocer a los suscriptores
  2. 📈 Escalabilidad: Múltiples consumidores pueden procesar el mismo evento sin duplicar lógica
  3. 🎯 Filtrado inteligente: Suscribirse solo a eventos específicos basados en su contenido
  4. 🔄 Evolución del sistema: Agregar nuevos consumidores sin modificar publicadores
  5. 📊 Observabilidad: Logs detallados de cada evento y entrega

Ejemplo de implementación:

// Publicar evento cuando un libro se publica
import { container } from '@/inversify.config';

const eventPublisher = container.get<EventPublisher>(EventPublisher.name);

const book = await this.publishUseCase.execute({ id: '<book_id>' });

// Publicar evento a EventBridge
await this.eventPublisher.publish({
  source: 'custom.books',
  detailType: 'BookPublished',
  detail: { book }
});

Consumir el evento:

// src/book/presentation/events/published-book.event.ts
import { inject, injectable } from 'inversify';
import { Event } from '@/shared/presentation/event';
import { Book } from '@/book/domain/entities/book.entity';
import { AuthorRepo } from '@/author/domain/repositories/author.repository';

type Input = { book: Book };

@injectable()
export class PublishedBook extends Event<Input> {
  protected eventName = 'PublishedBook';

  constructor(@inject(AuthorRepo.name) private readonly authorRepo: AuthorRepo) {
    super();
  }

  protected async run(input: Input) {
    this.logger.info('Executing PublishedBook event');
    this.logger.info(input);

    const author = await this.authorRepo.findOneById(input?.book?.authorId);

    this.logger.info(`${input?.book?.title} has been published!`);
    this.logger.info(`Thanks to the author ${author?.name}`);
  }
}

Lambda Handler:

// src/book/presentation/functions/event/published-book.event.ts
import { createEventHandler } from '@/shared/presentation/lambda-handler-factory';
import { PublishedBookEvent } from '@/book/presentation/events/published-book.event';

type Input = { book: Book };

export const handler = createEventHandler<Input>(PublishedBookEvent.name);

Configuración:

# serverless.yml
functions:
  publishedBookEvent:
    handler: dist/book/functions/event/published-book.handler
    events:
      - eventBridge:
          pattern:
            source:
              - custom.books
            detail-type:
              - BookPublished

El pattern matching permite suscribirse solo a eventos específicos:

# Ejemplo de filtrado avanzado
pattern:
  source:
    - custom.books
  detail-type:
    - BookPublished
  detail:
    book:
      isPublished:
        - true
      authorId:
        - prefix: "author-premium-"

Esto procesaría solo libros publicados de autores premium, sin necesidad de código adicional.

💉 Inyección de Dependencias con InversifyJS

El proyecto usa InversifyJS para gestionar dependencias y promover código desacoplado y testeable.

Configuración del contenedor:

// src/inversify.config.ts
import { Container } from 'inversify';
import { AuthorModule } from '@/author/author.module';
import { BookModule } from '@/book/book.module';
import { SharedModule } from '@/shared/shared.module';

export const container = new Container();

container.load(AuthorModule, BookModule, SharedModule);

Módulo de dominio:

// src/book/book.module.ts
import { ContainerModule } from 'inversify';
import { BookRepo } from '@/book/domain/repositories/book.repository';
import { MemoryBookRepo } from '@/book/infra/memory-book.repository';
import { PublishedBook } from '@/book/presentation/events/published-book.event';
import { BookRouter } from '@/book/presentation/routers/book.router';

export const BookModule = new ContainerModule(({ bind }) => {
  bind<BookRepo>(BookRepo.name).to(MemoryBookRepo).inSingletonScope();

  bind<PublishedBook>(PublishedBook.name).to(PublishedBook).inSingletonScope();

  bind<BookRouter>(BookRouter.name).to(BookRouter).inSingletonScope();
});

✨ Beneficios:

  • ✅ Testabilidad: Fácil mockear dependencias en tests
  • 🔄 Flexibilidad: Cambiar implementaciones sin modificar consumidores
  • 📝 Claridad: Dependencias explícitas en constructores

✔️ Validación con Zod

Todos los inputs HTTP se validan con Zod usando el middleware zValidator de @hono/zod-validator, proporcionando type-safety en runtime:

// src/book/presentation/dtos/book.dto.ts
import { z } from 'zod';
import { PaginationParamsSchema } from '@/shared/application/core/schemas/pagination.schema';

export const BooksListGet = z
  .object({ title: z.string(), authorId: z.uuid() })
  .extend(PaginationParamsSchema.shape)
  .partial();

export const BookPost = z.object({
  title: z.string().nonempty(),
  isPublished: z.boolean(),
  authorId: z.uuid(),
});

export const BookPatch = BookPost.partial();

Uso en el router con zValidator:

import { zValidator } from '@hono/zod-validator';

// Validación de query params
app.get('/', zValidator('query', BooksListGet), async (c) => {
  const { limit, authorId, title, skip } = c.req.valid('query');
  // Los datos ya están validados y tipados
  const books = await this.bookRepo.findMany({ authorId, title, limit, skip });
  return c.json(books);
});

// Validación de body JSON
app.post('/', zValidator('json', BookPost), async (c) => {
  const data = c.req.valid('json'); // Type-safe y validado
  const book = new Book(data);
  await this.bookRepo.create(book);
  return c.json({ book }, 201);
});

✨ Ventajas de zValidator:

  • ✅ Validación automática antes de ejecutar el handler
  • 🚫 Errores 400 bien formateados automáticamente
  • 🎯 Type inference completo con TypeScript
  • 📋 Soporta validación de json, query, param, header, y form

🚀 Deployment y CI/CD

Despliegue local:

npm run deploy         # Despliega a stage 'dev'
npm run deploy:prod    # Despliega a stage 'prod'

Serverless Framework genera:

  • Funciones Lambda con sus IAM roles
  • API Gateway HTTP API con rutas configuradas
  • EventBridge rules para crons y eventos
  • CloudWatch Log Groups
  • Todo versionado como CloudFormation stack

Output del deploy:

✔ Service deployed to stack serverless-example-dev (120s)

endpoints:
  ANY - https://abc123.execute-api.eu-west-3.amazonaws.com/dev/api/books
  ANY - https://abc123.execute-api.eu-west-3.amazonaws.com/dev/api/authors

functions:
  bookHandler: serverless-example-dev-bookHandler
  authorHandler: serverless-example-dev-authorHandler
  authorsListCron: serverless-example-dev-authorsListCron
  publishedBookEvent: serverless-example-dev-publishedBookEvent

✅ Best practices para producción:

  • 🏗️ Usar stages separados (dev, staging, prod)
  • 🔧 Environment variables diferentes por stage
  • 🌐 Custom domains con Route53 (elimina el prefijo /dev)
  • 🚨 CloudWatch Alarms para monitoreo
  • 🔍 X-Ray tracing para debugging distribuido
  • 🔄 CI/CD pipeline con GitHub Actions o AWS CodePipeline

📊 Monitoreo y Observabilidad

El proyecto incluye logging estructurado con Pino, implementando el patrón de abstracción con una clase base abstracta y una implementación concreta.

Clase base abstracta:

// src/shared/application/core/services/logger.service.ts
export abstract class LoggerService {
  protected readonly MESSAGE_KEY = 'message';
  protected readonly OBJECT_KEY = 'data';

  constructor(private readonly contextService: ContextService) {}

  abstract debug<T>(obj: T, msg?: string): void;
  abstract error(error: unknown, msg?: string): void;
  abstract info<T>(obj: T, msg?: string): void;
  abstract warn<T>(obj: T, msg?: string): void;

  getMetadataFromStore() {
    const store = this.contextService.getStore();
    const { request, traceId, cron, event } = store || {};

    return {
      ...(request && { request }),
      ...(cron && { cron }),
      ...(event && { event }),
      ...(traceId && { traceId }),
    };
  }

  normalizeLogData<T>(obj: T, msg?: string) {
    return {
      ...(typeof obj === 'object' && { [this.OBJECT_KEY]: obj }),
      ...this.getMetadataFromStore(),
      [this.MESSAGE_KEY]: typeof obj === 'string' ? obj : msg,
    };
  }
}

Implementación con Pino:

// src/shared/infra/logger/pino-logger.ts
@injectable()
export class PinoLogger extends LoggerService {
  private pinoLogger: Logger;

  constructor(@inject(ContextService.name) contextService: ContextService) {
    super(contextService);

    const { NODE_ENV } = EnvVarsService.getEnvVars();
    const isDevelopment = NODE_ENV === 'development';

    this.pinoLogger = pino({
      messageKey: this.MESSAGE_KEY,
      level: 'info',
      formatters: {
        level(label) {
          return { level: label };
        },
      },
      timestamp: pino.stdTimeFunctions.isoTime,
      // Only use pino-pretty in LOCAL development, NEVER in Lambda.
      ...(isDevelopment && {
        transport: {
          target: 'pino-pretty',
          options: {
            messageKey: this.MESSAGE_KEY,
            singleLine: true,
          },
        },
      }),
    });
  }

  info<T>(obj: T, msg?: string): void {
    this.pinoLogger.info(this.normalizeLogData(obj, msg));
  }

  warn<T>(obj: T, msg?: string): void {
    this.pinoLogger.warn(this.normalizeLogData(obj, msg));
  }

  debug<T>(obj: T, msg?: string): void {
    this.pinoLogger.debug(this.normalizeLogData(obj, msg));
  }

  error(error: unknown, msg?: string): void {
    this.pinoLogger.error({
      ...this.getMetadataFromStore(),
      ...(!!msg && { [this.MESSAGE_KEY]: msg }),
      err: error,
    });
  }
}

✨ Características clave del sistema de logging:

  1. 📋 Metadata automática: Cada log incluye automáticamente contexto de la request (HTTP), cron o evento según el tipo de Lambda
  2. 🔖 TraceId único: Para tracking de requests en CloudWatch Logs
  3. 📦 Formato estructurado JSON: Facilita búsquedas y filtrado en CloudWatch
  4. 🎨 Pretty printing en desarrollo: pino-pretty solo se activa en modo local, no en Lambda (reduciendo overhead)

Source maps están habilitados para que los stack traces muestren líneas originales de TypeScript:

// lambda-handler-factory.ts
import 'source-map-support/register'; // Cargado automáticamente

// Los errores muestran:
// Error: Book not found
//   at MemoryBookRepo.findOneById (src/book/infra/memory-book.repository.ts:45:12)
// En lugar de:
//   at Object.handler (dist/book/functions/http/book.cjs:1:2847)

Esto es crucial para debugging en producción, permitiendo identificar exactamente dónde ocurrió un error en el código fuente TypeScript original, no en el bundle minificado.

🎯 Conclusiones

Este proyecto demuestra cómo construir una API serverless completa y lista para producción siguiendo mejores prácticas:

🏗️ Arquitectura sólida:

  • ✅ DDD para separación de responsabilidades
  • 💉 Inyección de dependencias para testabilidad
  • 🎯 Type-safety en build-time (TypeScript) y runtime (Zod)

⚡ Funcionalidades completas:

  • 🌐 API REST con múltiples dominios
  • ⏰ Cron jobs para tareas programadas
  • 📡 Eventos asíncronos con EventBridge
  • ✔️ Validación robusta de inputs

☁️ Infraestructura moderna:

  • 🚀 Serverless Framework para IaC
  • ⚡ AWS Lambda con API Gateway HTTP API v2
  • 📡 EventBridge para arquitectura event-driven

🛠️ Developer Experience:

  • ⚡ Build automático multi-handler
  • 🔥 Hot reload local con serverless-offline
  • 🗺️ Source maps para debugging
  • 📊 Logging estructurado

El código completo está disponible como referencia para construir tus propias aplicaciones serverless escalables.

🚀 Próximos Pasos

Para mejorar aún más este proyecto, considera:

  1. 🧪 Tests automatizados: Unit tests con Jest, integration tests con LocalStack
  2. 📚 API Documentation: OpenAPI/Swagger generado desde Zod schemas
  3. 🛡️ Rate limiting: Proteger API con AWS WAF o API Gateway throttling
  4. ⚡ Caching: Integrar Redis (ElastiCache) para cachear queries frecuentes
  5. 💀 Dead Letter Queues: Para eventos que fallan en procesamiento
  6. 🔄 Step Functions: Orquestar workflows complejos multi-step
  7. 🌍 Multi-region deployment: Para baja latencia global

La arquitectura serverless no es una bala de plata, pero cuando se aplica correctamente puede resultar en aplicaciones altamente escalables, cost-eficientes y fáciles de mantener.


📚 Referencias