November 3, 2025

Complete Guide: Serverless API with AWS Lambda and DDD | TypeScript


πŸ’‘ Introduction to Serverless Architecture

The serverless architecture is a cloud computing paradigm where the cloud provider fully manages the server infrastructure. Despite the name, physical servers do exist, but as developers we don't have to worry about provisioning, scaling or maintaining them.

In a serverless model, we write functions that run in response to events (HTTP requests, queue messages, scheduled events, etc.) and we only pay for the actual execution time of these functions. AWS Lambda, Google Cloud Functions and Azure Functions are popular examples of serverless platforms.

βœ… Advantages of Serverless

  1. πŸ“ˆ Automatic scaling: Functions scale automatically based on demand, from zero to thousands of concurrent executions with no manual intervention.

  2. πŸ’° Pay-per-use cost model: You only pay for the actual execution time of your functions (measured in milliseconds) and the number of invocations. If there's no traffic, there are no infrastructure costs.

  3. πŸ”§ Lower operational complexity: No servers to manage, no security patches, load balancers or complex network configurations.

  4. ⚑ Reduced time-to-market: Lets you focus on business logic without worrying about the underlying infrastructure.

  5. πŸ›‘οΈ Built-in high availability: Cloud providers guarantee high availability and redundancy across multiple zones.

⚠️ Drawbacks and Challenges

  1. ❄️ Cold starts: When a function hasn't been invoked recently, it can experience extra latency on its first start (cold start). This can affect the user experience in applications with strict latency requirements.

  2. ⏱️ Execution limits: AWS Lambda has a maximum timeout of 15 minutes. It's not suitable for long-running processes.

  3. πŸ”’ Vendor lock-in: Code can become coupled to provider-specific services, making future migrations harder.

  4. πŸ› Debugging and observability: Local debugging can be more complex than in traditional applications. Having a good logging and monitoring strategy is crucial.

  5. πŸ”€ Complexity in large architectures: Managing dozens or hundreds of Lambda functions can become complex without the right tools and practices.

🎯 Key Aspects to Consider

  • πŸ”„ Design for failure: Functions must be idempotent and handle retries correctly.
  • πŸ’Ύ State management: Serverless functions are stateless by nature. State should be managed in external services (databases, caches, etc.).
  • πŸ“¦ Bundle optimization: Keeping function size small reduces cold starts.
  • πŸ“Š Monitoring and logging: Implementing observability from the start is critical.
  • πŸ” Secret management: Use services like AWS Secrets Manager or Parameter Store for credentials.

πŸ—οΈ Building a Complete Serverless API

To demonstrate how to build a robust and scalable serverless application, I've developed an example project that implements a REST API with advanced features such as cron jobs and asynchronous events.

πŸ› οΈ Tech Stack

The project uses a modern combination of technologies:

βš™οΈ Framework and Runtime:

  • Serverless Framework: To define infrastructure as code and deploy to AWS
  • Node.js 20.x: AWS Lambda runtime
  • TypeScript: Type-safe development with ES modules
  • Hono: Lightweight, fast web framework to handle routing and middleware

πŸ›οΈ Architecture and Patterns:

  • Domain-Driven Design (DDD): Well-defined layered architecture
  • InversifyJS: Dependency inversion container
  • Zod: Runtime schema validation

πŸ”¨ Build Tools:

  • tsup: Fast esbuild-based bundler to compile TypeScript
  • ESLint + Prettier: Code quality and formatting

πŸ›οΈ Project Architecture

The project follows a Domain-Driven Design (DDD) architecture with a clear separation of responsibilities across four layers:

src/
β”œβ”€β”€ shared/                          # Shared infrastructure
β”‚   β”œβ”€β”€ domain/                      # Base types (Entity, errors)
β”‚   β”œβ”€β”€ application/                 # Core services (Logger, Context)
β”‚   β”œβ”€β”€ infra/                       # Implementations (AWS Event Bridge, Logger)
β”‚   └── presentation/                # Factories and base classes (Lambda handlers, Cron, Events)
β”‚
β”œβ”€β”€ book/                            # Domain module: Books
β”‚   β”œβ”€β”€ domain/
β”‚   β”‚   β”œβ”€β”€ entities/                # Book.entity.ts
β”‚   β”‚   └── repositories/            # BookRepo (abstract)
β”‚   β”œβ”€β”€ application/                 # Use cases (create, list, update books)
β”‚   β”œβ”€β”€ infra/                       # MemoryBookRepo (implementation)
β”‚   └── presentation/
β”‚       β”œβ”€β”€ routers/                 # BookRouter (HTTP routes)
β”‚       β”œβ”€β”€ functions/
β”‚       β”‚   β”œβ”€β”€ http/                # book.http.ts (Lambda handler)
β”‚       β”‚   └── event/               # published-book.event.ts (EventBridge handler)
β”‚       └── dtos/                    # Zod schemas for validation
β”‚
└── author/                          # Domain module: Authors
    β”œβ”€β”€ domain/
    β”œβ”€β”€ application/
    β”œβ”€β”€ infra/
    └── presentation/
        β”œβ”€β”€ routers/                 # AuthorRouter
        β”œβ”€β”€ functions/
        β”‚   β”œβ”€β”€ http/                # author.http.ts
        β”‚   └── cron/                # authors-list.cron.ts (Cron job)
        └── dtos/

πŸ“š DDD Architecture Layers

1️⃣ Domain Layer

  • Contains pure business logic
  • Defines entities (Book, Author) that extend a base Entity class
  • Defines repository interfaces (abstract contracts)
  • No external dependencies, only business logic

2️⃣ Application Layer

  • Orchestrates business logic through Use Cases
  • Each business operation is an independent use case
  • Example: BookCreationUseCase, AuthorListUseCase
  • Depends only on the domain layer

3️⃣ Infrastructure Layer

  • Concrete implementations of the repositories
  • MemoryBookRepo implements BookRepo
  • Maps between database models and domain entities
  • Manages connections and transactions

4️⃣ Presentation Layer

  • Exposes the API through HTTP routers
  • Defines DTOs (Data Transfer Objects) with Zod schemas
  • Contains the Lambda handlers (entry points)
  • Transforms DTOs ↔ domain entities

✨ This architecture guarantees:

  • βœ… Testability: Each layer can be tested independently
  • πŸ”§ Maintainability: Infrastructure changes don't affect business logic
  • πŸ”„ Flexibility: Easy to swap implementations

⚑ Multi-Handler Build System

One of the key features is the automatic build system that discovers and compiles multiple 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',
  // Preserves the folder structure
  outExtension: () => ({ js: '.cjs' }),
  // ...
});

Each *.http.ts, *.cron.ts or *.event.ts file automatically becomes an independent Lambda handler:

  • src/book/presentation/functions/http/book.http.ts β†’ dist/book/functions/http/book.cjs
  • src/author/presentation/functions/cron/authors-list.cron.ts β†’ dist/author/functions/cron/authors-list.cjs
  • src/book/presentation/functions/event/published-book.event.ts β†’ dist/book/functions/event/published-book.cjs

This lets you scale the application by adding new handlers without modifying the build configuration.

πŸ“Š Data Model Implementation

The project implements two example domains: πŸ“š Books and ✍️ Authors.

Domain Entity:

// 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;
  }
}

The base Entity class provides common fields (id, createdAt, updatedAt) that all entities inherit.

🌐 REST API with AWS Lambda and API Gateway

⚑ Lambda HTTP Handlers

Each domain module has its own Lambda handler for HTTP operations:

// 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);

The createHttpHandler factory:

  1. Resolves the router from the DI container
  2. Configures a Hono app with middleware (CORS, logging, error handling)
  3. Returns an AWS Lambda-compatible handler
  4. Uses lazy initialization to avoid top-level await (CommonJS compatibility)

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);
 
      // Publish an event if the book is created as published
      if (book.isPublished) {
        await this.eventPublisher.publish({
          source: 'custom.books',
          detailType: 'BookPublished',
          detail: { book },
        });
      }
 
      return c.json({ book }, 201);
    });
 
    // ...
  }
}

Serverless Framework Configuration

# 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: '*'

Using {proxy+} lets a single Lambda handle all sub-routes (e.g. /api/books, /api/books/123, /api/books/123/publish).

✨ Advantages of this approach:

  • 🎯 Each domain has its own Lambda (isolation, independent deployments)
  • ⚑ Reduced cold starts thanks to smaller functions
  • πŸ“ˆ Independent scaling per domain
  • πŸ” Easy monitoring and debugging per domain

⏰ Cron Jobs with AWS EventBridge Scheduler

For scheduled tasks, the project uses AWS EventBridge Scheduler (formerly CloudWatch Events).

Example: List authors periodically

// 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}`);
    });
  }
}

The base CronJob class provides:

  • Automatic logger
  • Context service for tracking
  • Error handling and structured logging
  • 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);

Schedule Configuration:

# serverless.yml
functions:
  authorsListCron:
    handler: dist/author/functions/cron/authors-list.handler
    events:
      - schedule:
          rate: cron(0 2 * * ? *) # Every day at 2 AM UTC
          description: 'Daily authors list job'

EventBridge Scheduler supports standard cron expressions and rates (rate(1 hour), rate(30 minutes)).

πŸ“‘ Asynchronous Events with AWS EventBridge

For asynchronous communication between services, the project uses AWS EventBridge instead of alternatives like SNS or SQS.

πŸ€” Why EventBridge instead of SNS?

Amazon SNS (Simple Notification Service):

  • Basic pub/sub service
  • Subscribers receive all messages from the topic
  • Limited filtering (only at message-attribute level)
  • Ideal for simple notifications (emails, SMS, push)

Amazon EventBridge:

  • Enterprise event bus with advanced capabilities
  • Content-based filtering: Complex rules with pattern matching
  • Schema Registry: Event discovery and versioning
  • Event transformation: Modify payloads before sending to targets
  • Multiple targets: A single event can go to Lambda, SQS, Step Functions, etc.
  • Native integration: Support for events from 90+ AWS services
  • Event replay: Resend historical events (useful for debugging)
  • Cross-account/cross-region: Events between AWS accounts and regions

✨ Advantages of EventBridge for this project:

  1. πŸ”Œ Real decoupling: Publishers don't need to know about subscribers
  2. πŸ“ˆ Scalability: Multiple consumers can process the same event without duplicating logic
  3. 🎯 Smart filtering: Subscribe only to specific events based on their content
  4. πŸ”„ System evolution: Add new consumers without modifying publishers
  5. πŸ“Š Observability: Detailed logs of each event and delivery

Implementation example:

// Publish an event when a book is published
import { container } from '@/inversify.config';
 
const eventPublisher = container.get<EventPublisher>(EventPublisher.name);
 
const book = await this.publishUseCase.execute({ id: '<book_id>' });
 
// Publish the event to EventBridge
await this.eventPublisher.publish({
  source: 'custom.books',
  detailType: 'BookPublished',
  detail: { book },
});

Consume the event:

// 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 { PublishedBook } from '@/book/presentation/events/published-book.event';
 
type Input = { book: Book };
 
export const handler = createEventHandler<Input>(PublishedBook.name);

Configuration:

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

The pattern matching lets you subscribe only to specific events:

# Advanced filtering example
pattern:
  source:
    - custom.books
  detail-type:
    - BookPublished
  detail:
    book:
      isPublished:
        - true
      authorId:
        - prefix: 'author-premium-'

This would only process published books from premium authors, with no extra code needed.

πŸ’‰ Dependency Injection with InversifyJS

The project uses InversifyJS to manage dependencies and promote decoupled, testable code.

Container configuration:

// 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);

Domain module:

// 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();
});

✨ Benefits:

  • βœ… Testability: Easy to mock dependencies in tests
  • πŸ”„ Flexibility: Change implementations without modifying consumers
  • πŸ“ Clarity: Explicit dependencies in constructors

βœ”οΈ Validation with Zod

All HTTP inputs are validated with Zod using the zValidator middleware from @hono/zod-validator, providing runtime type-safety:

// 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();

Usage in the router with zValidator:

import { zValidator } from '@hono/zod-validator';
 
// Query params validation
app.get('/', zValidator('query', BooksListGet), async (c) => {
  const { limit, authorId, title, skip } = c.req.valid('query');
  // Data is already validated and typed
  const books = await this.bookRepo.findMany({ authorId, title, limit, skip });
  return c.json(books);
});
 
// JSON body validation
app.post('/', zValidator('json', BookPost), async (c) => {
  const data = c.req.valid('json'); // Type-safe and validated
  const book = new Book(data);
  await this.bookRepo.create(book);
  return c.json({ book }, 201);
});

✨ Advantages of zValidator:

  • βœ… Automatic validation before the handler runs
  • 🚫 Well-formatted 400 errors automatically
  • 🎯 Full type inference with TypeScript
  • πŸ“‹ Supports validation of json, query, param, header, and form

πŸš€ Deployment and CI/CD

Local deployment:

npm run deploy         # Deploys to the 'dev' stage
npm run deploy:prod    # Deploys to the 'prod' stage

Serverless Framework generates:

  • Lambda functions with their IAM roles
  • API Gateway HTTP API with configured routes
  • EventBridge rules for crons and events
  • CloudWatch Log Groups
  • All versioned as a CloudFormation stack

Deploy output:

βœ” 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

βœ… Production best practices:

  • πŸ—οΈ Use separate stages (dev, staging, prod)
  • πŸ”§ Different environment variables per stage
  • 🌐 Custom domains with Route53 (removes the /dev prefix)
  • 🚨 CloudWatch Alarms for monitoring
  • πŸ” X-Ray tracing for distributed debugging
  • πŸ”„ CI/CD pipeline with GitHub Actions or AWS CodePipeline

πŸ“Š Monitoring and Observability

The project includes structured logging with Pino, implementing the abstraction pattern with an abstract base class and a concrete implementation.

Abstract base class:

// 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,
    };
  }
}

Pino implementation:

// 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,
    });
  }
}

✨ Key features of the logging system:

  1. πŸ“‹ Automatic metadata: Each log automatically includes request (HTTP), cron or event context depending on the Lambda type
  2. πŸ”– Unique TraceId: For tracking requests in CloudWatch Logs
  3. πŸ“¦ Structured JSON format: Makes searching and filtering in CloudWatch easier
  4. 🎨 Pretty printing in development: pino-pretty is only enabled in local mode, not in Lambda (reducing overhead)

Source maps are enabled so stack traces show the original TypeScript lines:

// lambda-handler-factory.ts
import 'source-map-support/register'; // Loaded automatically
 
// Errors show:
// Error: Book not found
//   at MemoryBookRepo.findOneById (src/book/infra/memory-book.repository.ts:45:12)
// Instead of:
//   at Object.handler (dist/book/functions/http/book.cjs:1:2847)

This is crucial for debugging in production, letting you pinpoint exactly where an error occurred in the original TypeScript source, not in the minified bundle.

🎯 Conclusions

This project demonstrates how to build a complete, production-ready serverless API following best practices:

πŸ—οΈ Solid architecture:

  • βœ… DDD for separation of responsibilities
  • πŸ’‰ Dependency injection for testability
  • 🎯 Type-safety at build-time (TypeScript) and runtime (Zod)

⚑ Complete features:

  • 🌐 REST API with multiple domains
  • ⏰ Cron jobs for scheduled tasks
  • πŸ“‘ Asynchronous events with EventBridge
  • βœ”οΈ Robust input validation

☁️ Modern infrastructure:

  • πŸš€ Serverless Framework for IaC
  • ⚑ AWS Lambda with API Gateway HTTP API v2
  • πŸ“‘ EventBridge for event-driven architecture

πŸ› οΈ Developer Experience:

  • ⚑ Automatic multi-handler build
  • πŸ”₯ Local hot reload with serverless-offline
  • πŸ—ΊοΈ Source maps for debugging
  • πŸ“Š Structured logging

The complete code is available as a reference for building your own scalable serverless applications.

πŸš€ Next Steps

To improve this project even further, consider:

  1. πŸ§ͺ Automated tests: Unit tests with Jest, integration tests with LocalStack
  2. πŸ“š API Documentation: OpenAPI/Swagger generated from Zod schemas
  3. πŸ›‘οΈ Rate limiting: Protect the API with AWS WAF or API Gateway throttling
  4. ⚑ Caching: Integrate Redis (ElastiCache) to cache frequent queries
  5. πŸ’€ Dead Letter Queues: For events that fail during processing
  6. πŸ”„ Step Functions: Orchestrate complex multi-step workflows
  7. 🌍 Multi-region deployment: For low global latency

Serverless architecture is not a silver bullet, but when applied correctly it can result in highly scalable, cost-efficient and easy-to-maintain applications.


πŸ“š References