Skip to content
Go back

AWS Lambda TypeScript: Serverless Function Development

AWS Lambda TypeScript: Serverless Function Development

AWS Lambda with TypeScript enables building scalable, cost-effective serverless applications. This comprehensive guide covers function development, API Gateway integration, database operations, monitoring, and automated deployment using AWS CDK.

Why Choose AWS Lambda with TypeScript?

Step 1: Development Environment Setup

Set up a comprehensive AWS Lambda TypeScript development environment:

# Create project directory
mkdir aws-lambda-typescript-app
cd aws-lambda-typescript-app

# Initialize package.json
npm init -y

# Install AWS SDK v3 and Lambda dependencies
npm install @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
npm install @aws-sdk/client-s3 @aws-sdk/client-ses
npm install @aws-sdk/client-secrets-manager
npm install aws-lambda

# Install TypeScript and development tools
npm install -D typescript @types/node
npm install -D @types/aws-lambda
npm install -D esbuild serverless
npm install -D jest @types/jest ts-jest
npm install -D eslint @typescript-eslint/eslint-plugin
npm install -D prettier husky lint-staged

# Install AWS CDK for infrastructure
npm install -D aws-cdk-lib constructs
npm install -D @aws-cdk/aws-lambda-nodejs

# Install utility libraries
npm install zod middy
npm install uuid @types/uuid
npm install jsonwebtoken @types/jsonwebtoken

Configure TypeScript for AWS Lambda development:

{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["ES2022"],
    "module": "CommonJS",
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "allowJs": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "removeComments": true,
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "noImplicitThis": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@/types/*": ["src/types/*"],
      "@/utils/*": ["src/utils/*"],
      "@/services/*": ["src/services/*"],
      "@/middleware/*": ["src/middleware/*"]
    }
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "cdk.out"]
}tsconfig.json

Step 2: Type Definitions and Interfaces

Create comprehensive type definitions for Lambda functions:

import {
  APIGatewayProxyEvent,
  APIGatewayProxyResult,
  Context,
  DynamoDBStreamEvent,
  S3Event,
  SQSEvent,
  EventBridgeEvent,
} from "aws-lambda";

// Base API response structure
export interface ApiResponse<T = any> {
  success: boolean;
  data?: T;
  message?: string;
  errors?: Record<string, string[]>;
  meta?: {
    timestamp: string;
    requestId: string;
    version: string;
  };
}

// Enhanced Lambda context with custom properties
export interface LambdaContext extends Context {
  correlationId?: string;
  userId?: string;
  tenantId?: string;
}

// User types
export interface User {
  id: string;
  email: string;
  username: string;
  firstName: string;
  lastName: string;
  avatar?: string;
  role: UserRole;
  isActive: boolean;
  emailVerified: boolean;
  preferences: UserPreferences;
  metadata: Record<string, any>;
  createdAt: string;
  updatedAt: string;
}

export type UserRole = "admin" | "user" | "moderator";

export interface UserPreferences {
  theme: "light" | "dark" | "system";
  language: string;
  timezone: string;
  notifications: {
    email: boolean;
    push: boolean;
    sms: boolean;
  };
}

// Authentication types
export interface AuthTokenPayload {
  userId: string;
  email: string;
  role: UserRole;
  iat: number;
  exp: number;
}

export interface LoginRequest {
  email: string;
  password: string;
  rememberMe?: boolean;
}

export interface RegisterRequest {
  email: string;
  username: string;
  password: string;
  firstName: string;
  lastName: string;
}

// Product types
export interface Product {
  id: string;
  name: string;
  description: string;
  price: number;
  currency: string;
  category: string;
  images: string[];
  specifications: Record<string, string>;
  inventory: {
    quantity: number;
    inStock: boolean;
    reservedQuantity: number;
  };
  seo: {
    title?: string;
    description?: string;
    keywords?: string[];
  };
  isActive: boolean;
  createdAt: string;
  updatedAt: string;
}

// Order types
export interface Order {
  id: string;
  userId: string;
  items: OrderItem[];
  status: OrderStatus;
  totalAmount: number;
  currency: string;
  shippingAddress: Address;
  billingAddress: Address;
  paymentMethod: PaymentMethod;
  shippingMethod: ShippingMethod;
  notes?: string;
  metadata: Record<string, any>;
  createdAt: string;
  updatedAt: string;
}

export interface OrderItem {
  productId: string;
  name: string;
  price: number;
  quantity: number;
  totalPrice: number;
}

export type OrderStatus =
  | "pending"
  | "confirmed"
  | "processing"
  | "shipped"
  | "delivered"
  | "cancelled"
  | "refunded";

export interface Address {
  street: string;
  city: string;
  state: string;
  zipCode: string;
  country: string;
}

export interface PaymentMethod {
  type: "credit_card" | "debit_card" | "paypal" | "stripe";
  last4?: string;
  brand?: string;
}

export interface ShippingMethod {
  id: string;
  name: string;
  cost: number;
  estimatedDays: number;
}

// DynamoDB types
export interface DynamoDBItem {
  pk: string; // Partition key
  sk: string; // Sort key
  gsi1pk?: string; // Global secondary index 1 partition key
  gsi1sk?: string; // Global secondary index 1 sort key
  entityType: string;
  ttl?: number;
  createdAt: string;
  updatedAt: string;
}

export interface UserDynamoDBItem extends DynamoDBItem {
  entityType: "USER";
  email: string;
  username: string;
  passwordHash: string;
  profile: {
    firstName: string;
    lastName: string;
    avatar?: string;
  };
  role: UserRole;
  status: "active" | "inactive" | "suspended";
  emailVerified: boolean;
  preferences: UserPreferences;
  metadata: Record<string, any>;
}

export interface ProductDynamoDBItem extends DynamoDBItem {
  entityType: "PRODUCT";
  name: string;
  description: string;
  price: number;
  currency: string;
  category: string;
  images: string[];
  specifications: Record<string, string>;
  inventory: {
    quantity: number;
    inStock: boolean;
    reservedQuantity: number;
  };
  seo: {
    title?: string;
    description?: string;
    keywords?: string[];
  };
  isActive: boolean;
}

// Event types
export interface UserRegisteredEvent {
  eventType: "USER_REGISTERED";
  userId: string;
  email: string;
  timestamp: string;
  metadata: Record<string, any>;
}

export interface OrderCreatedEvent {
  eventType: "ORDER_CREATED";
  orderId: string;
  userId: string;
  totalAmount: number;
  currency: string;
  items: OrderItem[];
  timestamp: string;
  metadata: Record<string, any>;
}

export interface ProductUpdatedEvent {
  eventType: "PRODUCT_UPDATED";
  productId: string;
  changes: Partial<Product>;
  timestamp: string;
  metadata: Record<string, any>;
}

// Lambda function types
export type LambdaHandler<TEvent = any, TResult = any> = (
  event: TEvent,
  context: LambdaContext
) => Promise<TResult>;

export type ApiHandler = LambdaHandler<
  APIGatewayProxyEvent,
  APIGatewayProxyResult
>;

export type DynamoDBHandler = LambdaHandler<DynamoDBStreamEvent, void>;

export type S3Handler = LambdaHandler<S3Event, void>;

export type SQSHandler = LambdaHandler<SQSEvent, void>;

export type EventBridgeHandler<T = any> = LambdaHandler<
  EventBridgeEvent<string, T>,
  void
>;

// Utility types
export type CreateRequest<T> = Omit<T, "id" | "createdAt" | "updatedAt">;

export type UpdateRequest<T> = Partial<
  Omit<T, "id" | "createdAt" | "updatedAt">
>;

export type PaginationParams = {
  limit?: number;
  nextToken?: string;
};

export type PaginatedResponse<T> = {
  items: T[];
  nextToken?: string;
  count: number;
};

// Error types
export interface LambdaError extends Error {
  statusCode?: number;
  code?: string;
  details?: any;
}

// Configuration types
export interface LambdaConfig {
  region: string;
  stage: string;
  tableName: string;
  bucketName: string;
  jwtSecret: string;
  corsOrigin: string;
  logLevel: "debug" | "info" | "warn" | "error";
}

// Middleware types
export interface MiddlewareContext {
  event: APIGatewayProxyEvent;
  context: LambdaContext;
  config: LambdaConfig;
  user?: User;
  correlationId: string;
}src/types/index.ts

Step 3: Core Utilities and Services

Create comprehensive utility functions and services:

import { APIGatewayProxyResult } from "aws-lambda";
import { ApiResponse } from "@/types";

export class ResponseBuilder {
  static success<T>(
    data?: T,
    message?: string,
    statusCode = 200
  ): APIGatewayProxyResult {
    const response: ApiResponse<T> = {
      success: true,
      data,
      message,
      meta: {
        timestamp: new Date().toISOString(),
        requestId: "",
        version: process.env.API_VERSION || "1.0.0",
      },
    };

    return {
      statusCode,
      headers: {
        "Content-Type": "application/json",
        "Access-Control-Allow-Origin": process.env.CORS_ORIGIN || "*",
        "Access-Control-Allow-Headers":
          "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token",
        "Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS",
      },
      body: JSON.stringify(response),
    };
  }

  static error(
    message: string,
    statusCode = 400,
    errors?: Record<string, string[]>
  ): APIGatewayProxyResult {
    const response: ApiResponse = {
      success: false,
      message,
      errors,
      meta: {
        timestamp: new Date().toISOString(),
        requestId: "",
        version: process.env.API_VERSION || "1.0.0",
      },
    };

    return {
      statusCode,
      headers: {
        "Content-Type": "application/json",
        "Access-Control-Allow-Origin": process.env.CORS_ORIGIN || "*",
        "Access-Control-Allow-Headers":
          "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token",
        "Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS",
      },
      body: JSON.stringify(response),
    };
  }

  static notFound(message = "Resource not found"): APIGatewayProxyResult {
    return this.error(message, 404);
  }

  static unauthorized(message = "Unauthorized"): APIGatewayProxyResult {
    return this.error(message, 401);
  }

  static forbidden(message = "Forbidden"): APIGatewayProxyResult {
    return this.error(message, 403);
  }

  static validation(errors: Record<string, string[]>): APIGatewayProxyResult {
    return this.error("Validation failed", 422, errors);
  }

  static internal(message = "Internal server error"): APIGatewayProxyResult {
    return this.error(message, 500);
  }

  static created<T>(
    data: T,
    message = "Resource created successfully"
  ): APIGatewayProxyResult {
    return this.success(data, message, 201);
  }

  static noContent(): APIGatewayProxyResult {
    return {
      statusCode: 204,
      headers: {
        "Access-Control-Allow-Origin": process.env.CORS_ORIGIN || "*",
        "Access-Control-Allow-Headers":
          "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token",
        "Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS",
      },
      body: "",
    };
  }
}

export const createResponse = ResponseBuilder;src/utils/response.ts
import { z, ZodSchema } from "zod";
import { APIGatewayProxyEvent } from "aws-lambda";

export class ValidationError extends Error {
  constructor(
    public errors: Record<string, string[]>,
    message = "Validation failed"
  ) {
    super(message);
    this.name = "ValidationError";
  }
}

export class Validator {
  static validateBody<T>(event: APIGatewayProxyEvent, schema: ZodSchema<T>): T {
    if (!event.body) {
      throw new ValidationError({ body: ["Request body is required"] });
    }

    try {
      const parsedBody = JSON.parse(event.body);
      return schema.parse(parsedBody);
    } catch (error) {
      if (error instanceof z.ZodError) {
        const errors: Record<string, string[]> = {};

        error.errors.forEach(err => {
          const path = err.path.join(".");
          if (!errors[path]) {
            errors[path] = [];
          }
          errors[path].push(err.message);
        });

        throw new ValidationError(errors);
      }

      throw new ValidationError({ body: ["Invalid JSON format"] });
    }
  }

  static validatePathParameters<T>(
    event: APIGatewayProxyEvent,
    schema: ZodSchema<T>
  ): T {
    if (!event.pathParameters) {
      throw new ValidationError({ path: ["Path parameters are required"] });
    }

    try {
      return schema.parse(event.pathParameters);
    } catch (error) {
      if (error instanceof z.ZodError) {
        const errors: Record<string, string[]> = {};

        error.errors.forEach(err => {
          const path = `path.${err.path.join(".")}`;
          if (!errors[path]) {
            errors[path] = [];
          }
          errors[path].push(err.message);
        });

        throw new ValidationError(errors);
      }

      throw new ValidationError({ path: ["Invalid path parameters"] });
    }
  }

  static validateQueryParameters<T>(
    event: APIGatewayProxyEvent,
    schema: ZodSchema<T>
  ): T {
    const queryParams = event.queryStringParameters || {};

    try {
      return schema.parse(queryParams);
    } catch (error) {
      if (error instanceof z.ZodError) {
        const errors: Record<string, string[]> = {};

        error.errors.forEach(err => {
          const path = `query.${err.path.join(".")}`;
          if (!errors[path]) {
            errors[path] = [];
          }
          errors[path].push(err.message);
        });

        throw new ValidationError(errors);
      }

      throw new ValidationError({ query: ["Invalid query parameters"] });
    }
  }

  static validateHeaders<T>(
    event: APIGatewayProxyEvent,
    schema: ZodSchema<T>
  ): T {
    const headers = event.headers || {};

    try {
      return schema.parse(headers);
    } catch (error) {
      if (error instanceof z.ZodError) {
        const errors: Record<string, string[]> = {};

        error.errors.forEach(err => {
          const path = `headers.${err.path.join(".")}`;
          if (!errors[path]) {
            errors[path] = [];
          }
          errors[path].push(err.message);
        });

        throw new ValidationError(errors);
      }

      throw new ValidationError({ headers: ["Invalid headers"] });
    }
  }
}

// Common validation schemas
export const CommonSchemas = {
  id: z.string().uuid("Invalid ID format"),
  email: z.string().email("Invalid email format"),
  pagination: z.object({
    limit: z
      .string()
      .transform(val => parseInt(val))
      .pipe(z.number().min(1).max(100))
      .optional(),
    nextToken: z.string().optional(),
  }),
  pathId: z.object({
    id: z.string().uuid("Invalid ID format"),
  }),
  authHeaders: z.object({
    authorization: z.string().min(1, "Authorization header is required"),
  }),
};src/utils/validation.ts
import {
  DynamoDBClient,
  GetItemCommand,
  PutItemCommand,
  UpdateItemCommand,
  DeleteItemCommand,
  QueryCommand,
  ScanCommand,
  TransactWriteItemsCommand,
} from "@aws-sdk/client-dynamodb";
import {
  DynamoDBDocumentClient,
  GetCommand,
  PutCommand,
  UpdateCommand,
  DeleteCommand,
  QueryCommand as DocQueryCommand,
  ScanCommand as DocScanCommand,
  TransactWriteCommand,
} from "@aws-sdk/lib-dynamodb";
import { DynamoDBItem, PaginatedResponse } from "@/types";

export class DynamoDBService {
  private client: DynamoDBDocumentClient;
  private tableName: string;

  constructor(tableName: string, region = "us-east-1") {
    const dynamoClient = new DynamoDBClient({ region });
    this.client = DynamoDBDocumentClient.from(dynamoClient);
    this.tableName = tableName;
  }

  // Get single item by primary key
  async getItem<T extends DynamoDBItem>(
    pk: string,
    sk: string
  ): Promise<T | null> {
    try {
      const command = new GetCommand({
        TableName: this.tableName,
        Key: { pk, sk },
      });

      const response = await this.client.send(command);
      return response.Item as T | null;
    } catch (error) {
      console.error("DynamoDB getItem error:", error);
      throw new Error("Failed to get item from database");
    }
  }

  // Put single item
  async putItem<T extends DynamoDBItem>(item: T): Promise<void> {
    try {
      const now = new Date().toISOString();
      const itemWithTimestamps = {
        ...item,
        createdAt: item.createdAt || now,
        updatedAt: now,
      };

      const command = new PutCommand({
        TableName: this.tableName,
        Item: itemWithTimestamps,
      });

      await this.client.send(command);
    } catch (error) {
      console.error("DynamoDB putItem error:", error);
      throw new Error("Failed to save item to database");
    }
  }

  // Update item
  async updateItem<T extends DynamoDBItem>(
    pk: string,
    sk: string,
    updates: Partial<Omit<T, "pk" | "sk" | "createdAt" | "updatedAt">>
  ): Promise<T | null> {
    try {
      const updateExpressions: string[] = [];
      const expressionAttributeNames: Record<string, string> = {};
      const expressionAttributeValues: Record<string, any> = {};

      // Add updatedAt timestamp
      updates.updatedAt = new Date().toISOString() as any;

      Object.entries(updates).forEach(([key, value]) => {
        if (value !== undefined) {
          updateExpressions.push(`#${key} = :${key}`);
          expressionAttributeNames[`#${key}`] = key;
          expressionAttributeValues[`:${key}`] = value;
        }
      });

      if (updateExpressions.length === 0) {
        throw new Error("No valid updates provided");
      }

      const command = new UpdateCommand({
        TableName: this.tableName,
        Key: { pk, sk },
        UpdateExpression: `SET ${updateExpressions.join(", ")}`,
        ExpressionAttributeNames: expressionAttributeNames,
        ExpressionAttributeValues: expressionAttributeValues,
        ReturnValues: "ALL_NEW",
      });

      const response = await this.client.send(command);
      return response.Attributes as T | null;
    } catch (error) {
      console.error("DynamoDB updateItem error:", error);
      throw new Error("Failed to update item in database");
    }
  }

  // Delete item
  async deleteItem(pk: string, sk: string): Promise<void> {
    try {
      const command = new DeleteCommand({
        TableName: this.tableName,
        Key: { pk, sk },
      });

      await this.client.send(command);
    } catch (error) {
      console.error("DynamoDB deleteItem error:", error);
      throw new Error("Failed to delete item from database");
    }
  }

  // Query items by partition key
  async queryItems<T extends DynamoDBItem>(
    pk: string,
    options: {
      sk?: string;
      skCondition?: "begins_with" | "between";
      skValue2?: string;
      limit?: number;
      nextToken?: string;
      sortDescending?: boolean;
      indexName?: string;
    } = {}
  ): Promise<PaginatedResponse<T>> {
    try {
      let keyConditionExpression = "pk = :pk";
      const expressionAttributeValues: Record<string, any> = { ":pk": pk };

      if (options.sk) {
        if (options.skCondition === "begins_with") {
          keyConditionExpression += " AND begins_with(sk, :sk)";
          expressionAttributeValues[":sk"] = options.sk;
        } else if (options.skCondition === "between" && options.skValue2) {
          keyConditionExpression += " AND sk BETWEEN :sk1 AND :sk2";
          expressionAttributeValues[":sk1"] = options.sk;
          expressionAttributeValues[":sk2"] = options.skValue2;
        } else {
          keyConditionExpression += " AND sk = :sk";
          expressionAttributeValues[":sk"] = options.sk;
        }
      }

      const command = new DocQueryCommand({
        TableName: this.tableName,
        IndexName: options.indexName,
        KeyConditionExpression: keyConditionExpression,
        ExpressionAttributeValues: expressionAttributeValues,
        Limit: options.limit || 50,
        ExclusiveStartKey: options.nextToken
          ? JSON.parse(options.nextToken)
          : undefined,
        ScanIndexForward: !options.sortDescending,
      });

      const response = await this.client.send(command);

      return {
        items: response.Items as T[],
        nextToken: response.LastEvaluatedKey
          ? JSON.stringify(response.LastEvaluatedKey)
          : undefined,
        count: response.Count || 0,
      };
    } catch (error) {
      console.error("DynamoDB queryItems error:", error);
      throw new Error("Failed to query items from database");
    }
  }

  // Scan table with filters
  async scanItems<T extends DynamoDBItem>(
    options: {
      filterExpression?: string;
      expressionAttributeNames?: Record<string, string>;
      expressionAttributeValues?: Record<string, any>;
      limit?: number;
      nextToken?: string;
      indexName?: string;
    } = {}
  ): Promise<PaginatedResponse<T>> {
    try {
      const command = new DocScanCommand({
        TableName: this.tableName,
        IndexName: options.indexName,
        FilterExpression: options.filterExpression,
        ExpressionAttributeNames: options.expressionAttributeNames,
        ExpressionAttributeValues: options.expressionAttributeValues,
        Limit: options.limit || 50,
        ExclusiveStartKey: options.nextToken
          ? JSON.parse(options.nextToken)
          : undefined,
      });

      const response = await this.client.send(command);

      return {
        items: response.Items as T[],
        nextToken: response.LastEvaluatedKey
          ? JSON.stringify(response.LastEvaluatedKey)
          : undefined,
        count: response.Count || 0,
      };
    } catch (error) {
      console.error("DynamoDB scanItems error:", error);
      throw new Error("Failed to scan items from database");
    }
  }

  // Transaction write
  async transactWrite(
    operations: Array<{
      operation: "put" | "update" | "delete";
      item?: DynamoDBItem;
      pk?: string;
      sk?: string;
      updates?: Record<string, any>;
    }>
  ): Promise<void> {
    try {
      const transactItems = operations.map(op => {
        const now = new Date().toISOString();

        switch (op.operation) {
          case "put":
            if (!op.item) throw new Error("Item is required for put operation");
            return {
              Put: {
                TableName: this.tableName,
                Item: {
                  ...op.item,
                  createdAt: op.item.createdAt || now,
                  updatedAt: now,
                },
              },
            };

          case "update":
            if (!op.pk || !op.sk || !op.updates) {
              throw new Error(
                "pk, sk, and updates are required for update operation"
              );
            }

            const updateExpressions: string[] = [];
            const expressionAttributeNames: Record<string, string> = {};
            const expressionAttributeValues: Record<string, any> = {};

            op.updates.updatedAt = now;

            Object.entries(op.updates).forEach(([key, value]) => {
              if (value !== undefined) {
                updateExpressions.push(`#${key} = :${key}`);
                expressionAttributeNames[`#${key}`] = key;
                expressionAttributeValues[`:${key}`] = value;
              }
            });

            return {
              Update: {
                TableName: this.tableName,
                Key: { pk: op.pk, sk: op.sk },
                UpdateExpression: `SET ${updateExpressions.join(", ")}`,
                ExpressionAttributeNames: expressionAttributeNames,
                ExpressionAttributeValues: expressionAttributeValues,
              },
            };

          case "delete":
            if (!op.pk || !op.sk) {
              throw new Error("pk and sk are required for delete operation");
            }
            return {
              Delete: {
                TableName: this.tableName,
                Key: { pk: op.pk, sk: op.sk },
              },
            };

          default:
            throw new Error(`Unknown operation: ${op.operation}`);
        }
      });

      const command = new TransactWriteCommand({
        TransactItems: transactItems,
      });

      await this.client.send(command);
    } catch (error) {
      console.error("DynamoDB transactWrite error:", error);
      throw new Error("Failed to execute transaction");
    }
  }
}src/services/dynamodbService.ts

Step 4: Lambda Function Handlers

Create comprehensive Lambda function handlers:

import { APIGatewayProxyEvent, Context } from "aws-lambda";
import { z } from "zod";
import { v4 as uuidv4 } from "uuid";
import bcrypt from "bcryptjs";
import jwt from "jsonwebtoken";

import {
  ApiHandler,
  User,
  UserDynamoDBItem,
  CreateRequest,
  UpdateRequest,
} from "@/types";
import { createResponse } from "@/utils/response";
import { Validator, ValidationError, CommonSchemas } from "@/utils/validation";
import { DynamoDBService } from "@/services/dynamodbService";
import { withMiddleware } from "@/middleware";

// Initialize DynamoDB service
const dynamoService = new DynamoDBService(process.env.TABLE_NAME!);

// Validation schemas
const CreateUserSchema = z.object({
  email: z.string().email(),
  username: z.string().min(3).max(50),
  password: z.string().min(8),
  firstName: z.string().min(1).max(100),
  lastName: z.string().min(1).max(100),
});

const UpdateUserSchema = z
  .object({
    username: z.string().min(3).max(50).optional(),
    firstName: z.string().min(1).max(100).optional(),
    lastName: z.string().min(1).max(100).optional(),
    avatar: z.string().url().optional(),
  })
  .strict();

const LoginSchema = z.object({
  email: z.string().email(),
  password: z.string().min(1),
});

// Helper functions
const hashPassword = async (password: string): Promise<string> => {
  return bcrypt.hash(password, 12);
};

const verifyPassword = async (
  password: string,
  hash: string
): Promise<boolean> => {
  return bcrypt.compare(password, hash);
};

const generateToken = (user: User): string => {
  return jwt.sign(
    {
      userId: user.id,
      email: user.email,
      role: user.role,
    },
    process.env.JWT_SECRET!,
    { expiresIn: "24h" }
  );
};

const mapDynamoToUser = (item: UserDynamoDBItem): User => ({
  id: item.pk.replace("USER#", ""),
  email: item.email,
  username: item.username,
  firstName: item.profile.firstName,
  lastName: item.profile.lastName,
  avatar: item.profile.avatar,
  role: item.role,
  isActive: item.status === "active",
  emailVerified: item.emailVerified,
  preferences: item.preferences,
  metadata: item.metadata,
  createdAt: item.createdAt,
  updatedAt: item.updatedAt,
});

// Create user handler
export const createUser: ApiHandler = async (
  event: APIGatewayProxyEvent,
  context: Context
) => {
  try {
    // Validate request body
    const userData = Validator.validateBody(event, CreateUserSchema);

    // Check if user already exists
    const existingUser = await dynamoService.queryItems<UserDynamoDBItem>(
      "USER",
      {
        sk: `EMAIL#${userData.email.toLowerCase()}`,
        indexName: "GSI1",
      }
    );

    if (existingUser.items.length > 0) {
      return createResponse.error("User with this email already exists", 409);
    }

    // Check if username is taken
    const existingUsername = await dynamoService.queryItems<UserDynamoDBItem>(
      "USER",
      {
        sk: `USERNAME#${userData.username.toLowerCase()}`,
        indexName: "GSI1",
      }
    );

    if (existingUsername.items.length > 0) {
      return createResponse.error("Username is already taken", 409);
    }

    // Hash password
    const passwordHash = await hashPassword(userData.password);

    // Create user ID
    const userId = uuidv4();

    // Create DynamoDB item
    const userItem: UserDynamoDBItem = {
      pk: `USER#${userId}`,
      sk: `USER#${userId}`,
      gsi1pk: "USER",
      gsi1sk: `EMAIL#${userData.email.toLowerCase()}`,
      entityType: "USER",
      email: userData.email.toLowerCase(),
      username: userData.username.toLowerCase(),
      passwordHash,
      profile: {
        firstName: userData.firstName,
        lastName: userData.lastName,
      },
      role: "user",
      status: "active",
      emailVerified: false,
      preferences: {
        theme: "system",
        language: "en",
        timezone: "UTC",
        notifications: {
          email: true,
          push: true,
          sms: false,
        },
      },
      metadata: {},
      createdAt: new Date().toISOString(),
      updatedAt: new Date().toISOString(),
    };

    // Create username index item
    const usernameItem: UserDynamoDBItem = {
      ...userItem,
      pk: `USERNAME#${userData.username.toLowerCase()}`,
      sk: `USER#${userId}`,
      gsi1pk: "USER",
      gsi1sk: `USERNAME#${userData.username.toLowerCase()}`,
    };

    // Save both items in transaction
    await dynamoService.transactWrite([
      { operation: "put", item: userItem },
      { operation: "put", item: usernameItem },
    ]);

    // Convert to user object and remove sensitive data
    const user = mapDynamoToUser(userItem);
    const { metadata, ...safeUser } = user;

    return createResponse.created(safeUser, "User created successfully");
  } catch (error) {
    console.error("Create user error:", error);

    if (error instanceof ValidationError) {
      return createResponse.validation(error.errors);
    }

    return createResponse.internal("Failed to create user");
  }
};

// Login handler
export const login: ApiHandler = async (
  event: APIGatewayProxyEvent,
  context: Context
) => {
  try {
    // Validate request body
    const credentials = Validator.validateBody(event, LoginSchema);

    // Find user by email
    const userResult = await dynamoService.queryItems<UserDynamoDBItem>(
      "USER",
      {
        sk: `EMAIL#${credentials.email.toLowerCase()}`,
        indexName: "GSI1",
      }
    );

    if (userResult.items.length === 0) {
      return createResponse.unauthorized("Invalid credentials");
    }

    const userItem = userResult.items[0];

    // Verify password
    const isPasswordValid = await verifyPassword(
      credentials.password,
      userItem.passwordHash
    );

    if (!isPasswordValid) {
      return createResponse.unauthorized("Invalid credentials");
    }

    // Check if user is active
    if (userItem.status !== "active") {
      return createResponse.forbidden("Account is not active");
    }

    // Generate JWT token
    const user = mapDynamoToUser(userItem);
    const token = generateToken(user);

    // Remove sensitive data
    const { metadata, ...safeUser } = user;

    return createResponse.success(
      { user: safeUser, token },
      "Login successful"
    );
  } catch (error) {
    console.error("Login error:", error);

    if (error instanceof ValidationError) {
      return createResponse.validation(error.errors);
    }

    return createResponse.internal("Login failed");
  }
};

// Get user by ID handler
export const getUserById: ApiHandler = withMiddleware(["auth"])(async (
  event: APIGatewayProxyEvent,
  context: Context
) => {
  try {
    // Validate path parameters
    const { id } = Validator.validatePathParameters(
      event,
      CommonSchemas.pathId
    );

    // Get user from database
    const userItem = await dynamoService.getItem<UserDynamoDBItem>(
      `USER#${id}`,
      `USER#${id}`
    );

    if (!userItem) {
      return createResponse.notFound("User not found");
    }

    // Convert to user object
    const user = mapDynamoToUser(userItem);
    const { metadata, ...safeUser } = user;

    return createResponse.success(safeUser);
  } catch (error) {
    console.error("Get user error:", error);

    if (error instanceof ValidationError) {
      return createResponse.validation(error.errors);
    }

    return createResponse.internal("Failed to get user");
  }
});

// Update user handler
export const updateUser: ApiHandler = withMiddleware(["auth"])(async (
  event: APIGatewayProxyEvent,
  context: Context
) => {
  try {
    // Validate path parameters and body
    const { id } = Validator.validatePathParameters(
      event,
      CommonSchemas.pathId
    );
    const updates = Validator.validateBody(event, UpdateUserSchema);

    // Check if user exists
    const existingItem = await dynamoService.getItem<UserDynamoDBItem>(
      `USER#${id}`,
      `USER#${id}`
    );

    if (!existingItem) {
      return createResponse.notFound("User not found");
    }

    // Build update object
    const updateData: Partial<UserDynamoDBItem> = {};

    if (updates.username) {
      // Check if new username is available
      const existingUsername = await dynamoService.queryItems<UserDynamoDBItem>(
        "USER",
        {
          sk: `USERNAME#${updates.username.toLowerCase()}`,
          indexName: "GSI1",
        }
      );

      if (
        existingUsername.items.length > 0 &&
        existingUsername.items[0].pk !== `USER#${id}`
      ) {
        return createResponse.error("Username is already taken", 409);
      }

      updateData.username = updates.username.toLowerCase();
    }

    if (updates.firstName || updates.lastName || updates.avatar) {
      updateData.profile = {
        ...existingItem.profile,
        ...(updates.firstName && { firstName: updates.firstName }),
        ...(updates.lastName && { lastName: updates.lastName }),
        ...(updates.avatar && { avatar: updates.avatar }),
      };
    }

    // Update user
    const updatedItem = await dynamoService.updateItem<UserDynamoDBItem>(
      `USER#${id}`,
      `USER#${id}`,
      updateData
    );

    if (!updatedItem) {
      return createResponse.internal("Failed to update user");
    }

    // Convert to user object
    const user = mapDynamoToUser(updatedItem);
    const { metadata, ...safeUser } = user;

    return createResponse.success(safeUser, "User updated successfully");
  } catch (error) {
    console.error("Update user error:", error);

    if (error instanceof ValidationError) {
      return createResponse.validation(error.errors);
    }

    return createResponse.internal("Failed to update user");
  }
});

// Delete user handler
export const deleteUser: ApiHandler = withMiddleware(["auth", "admin"])(async (
  event: APIGatewayProxyEvent,
  context: Context
) => {
  try {
    // Validate path parameters
    const { id } = Validator.validatePathParameters(
      event,
      CommonSchemas.pathId
    );

    // Check if user exists
    const existingItem = await dynamoService.getItem<UserDynamoDBItem>(
      `USER#${id}`,
      `USER#${id}`
    );

    if (!existingItem) {
      return createResponse.notFound("User not found");
    }

    // Delete user and username index
    await dynamoService.transactWrite([
      { operation: "delete", pk: `USER#${id}`, sk: `USER#${id}` },
      {
        operation: "delete",
        pk: `USERNAME#${existingItem.username}`,
        sk: `USER#${id}`,
      },
    ]);

    return createResponse.noContent();
  } catch (error) {
    console.error("Delete user error:", error);

    if (error instanceof ValidationError) {
      return createResponse.validation(error.errors);
    }

    return createResponse.internal("Failed to delete user");
  }
});

// List users handler
export const listUsers: ApiHandler = withMiddleware(["auth", "admin"])(async (
  event: APIGatewayProxyEvent,
  context: Context
) => {
  try {
    // Validate query parameters
    const queryParams = Validator.validateQueryParameters(
      event,
      CommonSchemas.pagination
    );

    // Query users
    const result = await dynamoService.queryItems<UserDynamoDBItem>("USER", {
      limit: queryParams.limit,
      nextToken: queryParams.nextToken,
      indexName: "GSI1",
    });

    // Convert to user objects
    const users = result.items.map(item => {
      const user = mapDynamoToUser(item);
      const { metadata, ...safeUser } = user;
      return safeUser;
    });

    return createResponse.success({
      users,
      nextToken: result.nextToken,
      count: result.count,
    });
  } catch (error) {
    console.error("List users error:", error);

    if (error instanceof ValidationError) {
      return createResponse.validation(error.errors);
    }

    return createResponse.internal("Failed to list users");
  }
});src/handlers/users.ts

Step 5: Middleware System

Create a comprehensive middleware system:

import {
  APIGatewayProxyEvent,
  Context,
  APIGatewayProxyResult,
} from "aws-lambda";
import jwt from "jsonwebtoken";
import { ApiHandler, AuthTokenPayload, User, LambdaContext } from "@/types";
import { createResponse } from "@/utils/response";
import { DynamoDBService } from "@/services/dynamodbService";

// Middleware types
type MiddlewareFunction = (
  event: APIGatewayProxyEvent,
  context: LambdaContext,
  next: () => Promise<APIGatewayProxyResult>
) => Promise<APIGatewayProxyResult>;

type MiddlewareOptions = Array<
  "cors" | "auth" | "admin" | "logging" | "validation" | "rateLimit"
>;

// Enhanced context with middleware data
interface EnhancedContext extends LambdaContext {
  user?: User;
  correlationId: string;
}

// CORS middleware
const corsMiddleware: MiddlewareFunction = async (event, context, next) => {
  // Handle preflight requests
  if (event.httpMethod === "OPTIONS") {
    return {
      statusCode: 204,
      headers: {
        "Access-Control-Allow-Origin": process.env.CORS_ORIGIN || "*",
        "Access-Control-Allow-Headers":
          "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Correlation-Id",
        "Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS",
        "Access-Control-Max-Age": "86400",
      },
      body: "",
    };
  }

  // Continue to next middleware
  const result = await next();

  // Add CORS headers to response
  if (result.headers) {
    result.headers["Access-Control-Allow-Origin"] =
      process.env.CORS_ORIGIN || "*";
    result.headers["Access-Control-Allow-Headers"] =
      "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Correlation-Id";
    result.headers["Access-Control-Allow-Methods"] =
      "GET,POST,PUT,DELETE,OPTIONS";
  }

  return result;
};

// Authentication middleware
const authMiddleware: MiddlewareFunction = async (event, context, next) => {
  try {
    const authHeader =
      event.headers.authorization || event.headers.Authorization;

    if (!authHeader) {
      return createResponse.unauthorized("Authorization header is required");
    }

    const token = authHeader.replace("Bearer ", "");

    if (!token) {
      return createResponse.unauthorized("Invalid authorization format");
    }

    // Verify JWT token
    let decoded: AuthTokenPayload;
    try {
      decoded = jwt.verify(token, process.env.JWT_SECRET!) as AuthTokenPayload;
    } catch (error) {
      return createResponse.unauthorized("Invalid or expired token");
    }

    // Get user from database
    const dynamoService = new DynamoDBService(process.env.TABLE_NAME!);
    const userItem = await dynamoService.getItem(
      `USER#${decoded.userId}`,
      `USER#${decoded.userId}`
    );

    if (!userItem) {
      return createResponse.unauthorized("User not found");
    }

    // Add user to context
    const enhancedContext = context as EnhancedContext;
    enhancedContext.user = {
      id: decoded.userId,
      email: decoded.email,
      role: decoded.role,
    } as User;
    enhancedContext.userId = decoded.userId;

    return await next();
  } catch (error) {
    console.error("Auth middleware error:", error);
    return createResponse.unauthorized("Authentication failed");
  }
};

// Admin authorization middleware
const adminMiddleware: MiddlewareFunction = async (event, context, next) => {
  const enhancedContext = context as EnhancedContext;

  if (!enhancedContext.user) {
    return createResponse.unauthorized("Authentication required");
  }

  if (enhancedContext.user.role !== "admin") {
    return createResponse.forbidden("Admin access required");
  }

  return await next();
};

// Logging middleware
const loggingMiddleware: MiddlewareFunction = async (event, context, next) => {
  const startTime = Date.now();
  const correlationId =
    event.headers["x-correlation-id"] ||
    event.headers["X-Correlation-Id"] ||
    context.awsRequestId;

  // Add correlation ID to context
  const enhancedContext = context as EnhancedContext;
  enhancedContext.correlationId = correlationId;

  // Log request
  console.log("Request started:", {
    correlationId,
    method: event.httpMethod,
    path: event.path,
    queryParams: event.queryStringParameters,
    userAgent: event.headers["user-agent"],
    sourceIp: event.requestContext.identity.sourceIp,
    userId: enhancedContext.userId,
  });

  try {
    const result = await next();

    // Log successful response
    const duration = Date.now() - startTime;
    console.log("Request completed:", {
      correlationId,
      statusCode: result.statusCode,
      duration: `${duration}ms`,
    });

    // Add correlation ID to response headers
    if (result.headers) {
      result.headers["X-Correlation-Id"] = correlationId;
    }

    return result;
  } catch (error) {
    // Log error
    const duration = Date.now() - startTime;
    console.error("Request failed:", {
      correlationId,
      error: error instanceof Error ? error.message : "Unknown error",
      duration: `${duration}ms`,
    });

    throw error;
  }
};

// Rate limiting middleware (simple implementation)
const rateLimitMiddleware: MiddlewareFunction = async (
  event,
  context,
  next
) => {
  // This is a simplified rate limiting implementation
  // In production, you would use DynamoDB or Redis to track rates

  const clientIp = event.requestContext.identity.sourceIp;
  const key = `rate_limit:${clientIp}`;

  // For demo purposes, we'll allow all requests
  // In production, implement proper rate limiting logic

  return await next();
};

// Validation middleware (generic error handling)
const validationMiddleware: MiddlewareFunction = async (
  event,
  context,
  next
) => {
  try {
    return await next();
  } catch (error) {
    console.error("Validation middleware error:", error);

    if (error instanceof Error && error.name === "ValidationError") {
      return createResponse.validation({
        validation: [error.message],
      });
    }

    throw error;
  }
};

// Middleware registry
const middlewareRegistry: Record<string, MiddlewareFunction> = {
  cors: corsMiddleware,
  auth: authMiddleware,
  admin: adminMiddleware,
  logging: loggingMiddleware,
  validation: validationMiddleware,
  rateLimit: rateLimitMiddleware,
};

// Main middleware composer
export function withMiddleware(
  options: MiddlewareOptions = ["cors", "logging"]
) {
  return (handler: ApiHandler): ApiHandler => {
    return async (event: APIGatewayProxyEvent, context: Context) => {
      // Get middleware functions
      const middlewares = options
        .map(name => middlewareRegistry[name])
        .filter(Boolean);

      // Create middleware chain
      let index = 0;

      const next = async (): Promise<APIGatewayProxyResult> => {
        if (index >= middlewares.length) {
          // All middleware processed, call the actual handler
          return await handler(event, context as LambdaContext);
        }

        const middleware = middlewares[index++];
        return await middleware(event, context as LambdaContext, next);
      };

      try {
        return await next();
      } catch (error) {
        console.error("Middleware chain error:", error);

        // Return generic error response
        return createResponse.internal(
          error instanceof Error ? error.message : "Internal server error"
        );
      }
    };
  };
}

// Shorthand for common middleware combinations
export const withAuth = withMiddleware(["cors", "logging", "auth"]);
export const withAdmin = withMiddleware(["cors", "logging", "auth", "admin"]);
export const withPublic = withMiddleware(["cors", "logging"]);src/middleware/index.ts

Step 6: AWS CDK Infrastructure

Create infrastructure as code using AWS CDK:

import * as cdk from "aws-cdk-lib";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as apigateway from "aws-cdk-lib/aws-apigateway";
import * as dynamodb from "aws-cdk-lib/aws-dynamodb";
import * as s3 from "aws-cdk-lib/aws-s3";
import * as iam from "aws-cdk-lib/aws-iam";
import * as logs from "aws-cdk-lib/aws-logs";
import * as events from "aws-cdk-lib/aws-events";
import * as targets from "aws-cdk-lib/aws-events-targets";
import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs";
import { Construct } from "constructs";

export class ServerlessAppStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // Environment variables
    const stage = this.node.tryGetContext("stage") || "dev";
    const region = cdk.Stack.of(this).region;

    // DynamoDB Table
    const table = new dynamodb.Table(this, "AppTable", {
      tableName: `serverless-app-${stage}`,
      partitionKey: {
        name: "pk",
        type: dynamodb.AttributeType.STRING,
      },
      sortKey: {
        name: "sk",
        type: dynamodb.AttributeType.STRING,
      },
      billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
      encryption: dynamodb.TableEncryption.AWS_MANAGED,
      pointInTimeRecovery: stage === "prod",
      removalPolicy:
        stage === "prod" ? cdk.RemovalPolicy.RETAIN : cdk.RemovalPolicy.DESTROY,
      stream: dynamodb.StreamViewType.NEW_AND_OLD_IMAGES,
    });

    // Add Global Secondary Indexes
    table.addGlobalSecondaryIndex({
      indexName: "GSI1",
      partitionKey: {
        name: "gsi1pk",
        type: dynamodb.AttributeType.STRING,
      },
      sortKey: {
        name: "gsi1sk",
        type: dynamodb.AttributeType.STRING,
      },
    });

    // S3 Bucket for file storage
    const bucket = new s3.Bucket(this, "AppBucket", {
      bucketName: `serverless-app-${stage}-${region}`,
      encryption: s3.BucketEncryption.S3_MANAGED,
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
      enforceSSL: true,
      removalPolicy:
        stage === "prod" ? cdk.RemovalPolicy.RETAIN : cdk.RemovalPolicy.DESTROY,
      autoDeleteObjects: stage !== "prod",
    });

    // Lambda execution role
    const lambdaRole = new iam.Role(this, "LambdaExecutionRole", {
      assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"),
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName(
          "service-role/AWSLambdaBasicExecutionRole"
        ),
      ],
      inlinePolicies: {
        DynamoDBAccess: new iam.PolicyDocument({
          statements: [
            new iam.PolicyStatement({
              effect: iam.Effect.ALLOW,
              actions: [
                "dynamodb:GetItem",
                "dynamodb:PutItem",
                "dynamodb:UpdateItem",
                "dynamodb:DeleteItem",
                "dynamodb:Query",
                "dynamodb:Scan",
                "dynamodb:BatchGetItem",
                "dynamodb:BatchWriteItem",
                "dynamodb:TransactWriteItems",
                "dynamodb:TransactGetItems",
              ],
              resources: [table.tableArn, `${table.tableArn}/*`],
            }),
          ],
        }),
        S3Access: new iam.PolicyDocument({
          statements: [
            new iam.PolicyStatement({
              effect: iam.Effect.ALLOW,
              actions: ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
              resources: [`${bucket.bucketArn}/*`],
            }),
          ],
        }),
      },
    });

    // Common Lambda function configuration
    const lambdaDefaults = {
      runtime: lambda.Runtime.NODEJS_18_X,
      architecture: lambda.Architecture.ARM_64,
      timeout: cdk.Duration.seconds(30),
      memorySize: 512,
      role: lambdaRole,
      environment: {
        NODE_ENV: stage,
        STAGE: stage,
        REGION: region,
        TABLE_NAME: table.tableName,
        BUCKET_NAME: bucket.bucketName,
        JWT_SECRET: "your-jwt-secret", // Use AWS Secrets Manager in production
        CORS_ORIGIN: stage === "prod" ? "https://yourdomain.com" : "*",
        LOG_LEVEL: stage === "prod" ? "info" : "debug",
      },
      bundling: {
        minify: true,
        sourceMap: true,
        target: "es2020",
        keepNames: true,
        externalModules: ["aws-sdk"],
      },
    };

    // User Management Functions
    const createUserFunction = new NodejsFunction(this, "CreateUserFunction", {
      ...lambdaDefaults,
      functionName: `serverless-app-${stage}-create-user`,
      entry: "src/handlers/users.ts",
      handler: "createUser",
      description: "Create a new user",
    });

    const loginFunction = new NodejsFunction(this, "LoginFunction", {
      ...lambdaDefaults,
      functionName: `serverless-app-${stage}-login`,
      entry: "src/handlers/users.ts",
      handler: "login",
      description: "User login",
    });

    const getUserFunction = new NodejsFunction(this, "GetUserFunction", {
      ...lambdaDefaults,
      functionName: `serverless-app-${stage}-get-user`,
      entry: "src/handlers/users.ts",
      handler: "getUserById",
      description: "Get user by ID",
    });

    const updateUserFunction = new NodejsFunction(this, "UpdateUserFunction", {
      ...lambdaDefaults,
      functionName: `serverless-app-${stage}-update-user`,
      entry: "src/handlers/users.ts",
      handler: "updateUser",
      description: "Update user",
    });

    const deleteUserFunction = new NodejsFunction(this, "DeleteUserFunction", {
      ...lambdaDefaults,
      functionName: `serverless-app-${stage}-delete-user`,
      entry: "src/handlers/users.ts",
      handler: "deleteUser",
      description: "Delete user",
    });

    const listUsersFunction = new NodejsFunction(this, "ListUsersFunction", {
      ...lambdaDefaults,
      functionName: `serverless-app-${stage}-list-users`,
      entry: "src/handlers/users.ts",
      handler: "listUsers",
      description: "List users",
    });

    // API Gateway
    const api = new apigateway.RestApi(this, "ServerlessApi", {
      restApiName: `serverless-app-${stage}`,
      description: "Serverless application API",
      deployOptions: {
        stageName: stage,
        throttlingRateLimit: 1000,
        throttlingBurstLimit: 2000,
        loggingLevel: apigateway.MethodLoggingLevel.INFO,
        dataTraceEnabled: stage !== "prod",
        metricsEnabled: true,
      },
      defaultCorsPreflightOptions: {
        allowOrigins:
          stage === "prod"
            ? ["https://yourdomain.com"]
            : apigateway.Cors.ALL_ORIGINS,
        allowMethods: apigateway.Cors.ALL_METHODS,
        allowHeaders: [
          "Content-Type",
          "X-Amz-Date",
          "Authorization",
          "X-Api-Key",
          "X-Amz-Security-Token",
          "X-Correlation-Id",
        ],
      },
    });

    // API Gateway Resources and Methods
    const usersResource = api.root.addResource("users");
    const userResource = usersResource.addResource("{id}");

    // User endpoints
    usersResource.addMethod(
      "POST",
      new apigateway.LambdaIntegration(createUserFunction)
    );
    usersResource.addMethod(
      "GET",
      new apigateway.LambdaIntegration(listUsersFunction)
    );
    userResource.addMethod(
      "GET",
      new apigateway.LambdaIntegration(getUserFunction)
    );
    userResource.addMethod(
      "PUT",
      new apigateway.LambdaIntegration(updateUserFunction)
    );
    userResource.addMethod(
      "DELETE",
      new apigateway.LambdaIntegration(deleteUserFunction)
    );

    // Auth endpoints
    const authResource = api.root.addResource("auth");
    authResource
      .addResource("login")
      .addMethod("POST", new apigateway.LambdaIntegration(loginFunction));

    // CloudWatch Log Groups
    new logs.LogGroup(this, "ApiGatewayLogGroup", {
      logGroupName: `/aws/apigateway/serverless-app-${stage}`,
      retention:
        stage === "prod"
          ? logs.RetentionDays.ONE_MONTH
          : logs.RetentionDays.ONE_WEEK,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });

    // EventBridge for event-driven architecture
    const eventBus = new events.EventBus(this, "AppEventBus", {
      eventBusName: `serverless-app-${stage}`,
    });

    // DynamoDB Stream processor
    const streamProcessorFunction = new NodejsFunction(
      this,
      "StreamProcessorFunction",
      {
        ...lambdaDefaults,
        functionName: `serverless-app-${stage}-stream-processor`,
        entry: "src/handlers/streamProcessor.ts",
        handler: "processStream",
        description: "Process DynamoDB stream events",
        environment: {
          ...lambdaDefaults.environment,
          EVENT_BUS_NAME: eventBus.eventBusName,
        },
      }
    );

    // Grant permissions for EventBridge
    eventBus.grantPutEventsTo(streamProcessorFunction);

    // Connect DynamoDB Stream to Lambda
    streamProcessorFunction.addEventSource(
      new lambda.DynamoEventSource(table, {
        startingPosition: lambda.StartingPosition.TRIM_HORIZON,
        batchSize: 10,
        maxBatchingWindow: cdk.Duration.seconds(5),
        retryAttempts: 3,
      })
    );

    // CloudFormation Outputs
    new cdk.CfnOutput(this, "ApiGatewayUrl", {
      value: api.url,
      description: "API Gateway URL",
      exportName: `serverless-app-${stage}-api-url`,
    });

    new cdk.CfnOutput(this, "TableName", {
      value: table.tableName,
      description: "DynamoDB Table Name",
      exportName: `serverless-app-${stage}-table-name`,
    });

    new cdk.CfnOutput(this, "BucketName", {
      value: bucket.bucketName,
      description: "S3 Bucket Name",
      exportName: `serverless-app-${stage}-bucket-name`,
    });

    new cdk.CfnOutput(this, "EventBusName", {
      value: eventBus.eventBusName,
      description: "EventBridge Bus Name",
      exportName: `serverless-app-${stage}-event-bus-name`,
    });
  }
}

// CDK App
const app = new cdk.App();
const stage = app.node.tryGetContext("stage") || "dev";

new ServerlessAppStack(app, `ServerlessAppStack-${stage}`, {
  env: {
    account: process.env.CDK_DEFAULT_ACCOUNT,
    region: process.env.CDK_DEFAULT_REGION || "us-east-1",
  },
  stackName: `serverless-app-${stage}`,
  description: `Serverless application infrastructure for ${stage} environment`,
  tags: {
    Environment: stage,
    Project: "serverless-app",
    Owner: "development-team",
  },
});infrastructure/main.ts

Best Practices Summary

  1. Type Safety: Use comprehensive TypeScript types throughout the application
  2. Error Handling: Implement proper error handling with custom error types
  3. Validation: Use Zod for request validation and data sanitization
  4. Security: Implement JWT authentication and authorization middleware
  5. Database Design: Use single-table design pattern with DynamoDB
  6. Monitoring: Add comprehensive logging and monitoring
  7. Infrastructure as Code: Use AWS CDK for reproducible deployments
  8. Performance: Optimize bundle size and cold start times
  9. Testing: Write unit and integration tests for critical functions
  10. Documentation: Maintain comprehensive API documentation

Development Commands

# Install dependencies
npm install

# Run local development
npm run dev

# Build functions
npm run build

# Deploy infrastructure
npm run deploy

# Run tests
npm test

# Lint code
npm run lint

Your AWS Lambda TypeScript serverless application is now ready for production with comprehensive infrastructure, type safety, middleware, and monitoring capabilities!


Share this post on:

Previous Post
Node.js Performance Optimization: Production-Ready Server Development
Next Post
Expo React Native TypeScript: Cross-Platform Mobile Development