skills/aws-lambda-typescript-integration/references/raw-typescript-lambda.md

27 KiB

Raw TypeScript Lambda Reference

Complete guide for creating minimal AWS Lambda functions in pure TypeScript without frameworks like NestJS.

Table of Contents

  1. Project Structure
  2. Minimal Handler
  3. Dependency Injection Patterns
  4. Cold Start Optimization
  5. TypeScript Configuration
  6. Build and Packaging
  7. Testing
  8. Deployment

Project Structure

Minimal Setup

raw-ts-lambda/
├── src/
│   ├── handlers/
│   │   ├── api.handler.ts
│   │   └── s3.handler.ts
│   ├── services/
│   │   └── user.service.ts
│   ├── models/
│   │   └── user.model.ts
│   ├── utils/
│   │   └── response.util.ts
│   └── config/
│       └── database.config.ts
├── dist/                     # Compiled output
├── tests/
│   └── handlers/
│       └── api.handler.test.ts
├── package.json
├── tsconfig.json
└── template.yaml (or serverless.yml)

Package.json

{
  "name": "raw-ts-lambda",
  "version": "1.0.0",
  "description": "Minimal TypeScript Lambda without frameworks",
  "main": "dist/handlers/api.handler.js",
  "scripts": {
    "build": "tsc",
    "build:prod": "tsc && npm prune --production",
    "test": "jest",
    "lint": "eslint src/**/*.ts",
    "deploy": "sam build && sam deploy",
    "local": "sam local start-api"
  },
  "dependencies": {
    "@aws-sdk/client-dynamodb": "^3.450.0",
    "@aws-sdk/lib-dynamodb": "^3.450.0",
    "@aws-sdk/client-s3": "^3.450.0"
  },
  "devDependencies": {
    "@types/aws-lambda": "^8.10.131",
    "@types/jest": "^29.5.10",
    "@types/node": "^20.10.0",
    "@typescript-eslint/eslint-plugin": "^6.13.0",
    "@typescript-eslint/parser": "^6.13.0",
    "aws-lambda": "^1.0.7",
    "esbuild": "^0.19.8",
    "eslint": "^8.54.0",
    "jest": "^29.7.0",
    "ts-jest": "^29.1.1",
    "typescript": "^5.3.2"
  },
  "engines": {
    "node": ">=20.0.0"
  }
}

Minimal Handler

Basic API Gateway Handler

// src/handlers/api.handler.ts
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';

// Static initialization - runs once per container
// Configure allowed origins via ALLOWED_ORIGINS env var (comma-separated list)
const allowedOrigins = (process.env.ALLOWED_ORIGINS || '').split(',').filter(Boolean);
const responseHeaders = {
  'Content-Type': 'application/json',
  ...(allowedOrigins.length > 0 && {
    'Access-Control-Allow-Origin': allowedOrigins.includes('*') ? '*' : allowedOrigins[0],
    'Access-Control-Allow-Credentials': 'true',
  }),
};

export const handler = async (
  event: APIGatewayProxyEvent,
  context: Context
): Promise<APIGatewayProxyResult> => {
  console.log('Request:', {
    requestId: context.awsRequestId,
    path: event.path,
    method: event.httpMethod,
  });

  try {
    const { httpMethod, path, pathParameters, queryStringParameters, body } = event;

    // Simple routing
    switch (`${httpMethod} ${path}`) {
      case 'GET /health':
        return successResponse(200, { status: 'ok', timestamp: new Date().toISOString() });

      case 'GET /users':
        return await getUsers(queryStringParameters);

      case 'GET /users/{id}':
        return await getUser(pathParameters?.id);

      case 'POST /users':
        return await createUser(body);

      default:
        return errorResponse(404, 'Not Found');
    }
  } catch (error) {
    console.error('Error:', error);
    return errorResponse(500, 'Internal Server Error');
  }
};

// Service functions
async function getUsers(queryParams: Record<string, string> | null): Promise<APIGatewayProxyResult> {
  // Implementation
  return successResponse(200, { users: [] });
}

async function getUser(id: string | undefined): Promise<APIGatewayProxyResult> {
  if (!id) {
    return errorResponse(400, 'User ID is required');
  }
  // Implementation
  return successResponse(200, { id, name: 'John Doe' });
}

async function createUser(body: string | null): Promise<APIGatewayProxyResult> {
  if (!body) {
    return errorResponse(400, 'Request body is required');
  }

  try {
    const user = JSON.parse(body);
    // Validate and save
    return successResponse(201, { id: '123', ...user });
  } catch (error) {
    return errorResponse(400, 'Invalid JSON in request body');
  }
}

// Utility functions
function successResponse(statusCode: number, data: unknown): APIGatewayProxyResult {
  return {
    statusCode,
    headers: responseHeaders,
    body: JSON.stringify(data),
  };
}

function errorResponse(statusCode: number, message: string): APIGatewayProxyResult {
  return {
    statusCode,
    headers: responseHeaders,
    body: JSON.stringify({ error: message }),
  };
}

Handler with Service Layer

// src/handlers/user.handler.ts
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { UserService } from '../services/user.service';
import { DynamoDbUserRepository } from '../repositories/dynamodb-user.repository';

// Lazy-initialized singleton
let userService: UserService | null = null;

function getUserService(): UserService {
  if (!userService) {
    const repository = new DynamoDbUserRepository();
    userService = new UserService(repository);
  }
  return userService;
}

export const handler = async (
  event: APIGatewayProxyEvent,
  context: Context
): Promise<APIGatewayProxyResult> => {
  const service = getUserService();

  try {
    switch (event.httpMethod) {
      case 'GET':
        if (event.pathParameters?.id) {
          const user = await service.findById(event.pathParameters.id);
          return user
            ? { statusCode: 200, body: JSON.stringify(user) }
            : { statusCode: 404, body: JSON.stringify({ error: 'User not found' }) };
        }
        const users = await service.findAll();
        return { statusCode: 200, body: JSON.stringify(users) };

      case 'POST':
        const created = await service.create(JSON.parse(event.body || '{}'));
        return { statusCode: 201, body: JSON.stringify(created) };

      case 'PUT':
        const updated = await service.update(
          event.pathParameters?.id!,
          JSON.parse(event.body || '{}')
        );
        return { statusCode: 200, body: JSON.stringify(updated) };

      case 'DELETE':
        await service.delete(event.pathParameters?.id!);
        return { statusCode: 204, body: '' };

      default:
        return { statusCode: 405, body: JSON.stringify({ error: 'Method not allowed' }) };
    }
  } catch (error) {
    console.error('Handler error:', error);
    return {
      statusCode: 500,
      body: JSON.stringify({ error: 'Internal server error' }),
    };
  }
};

S3 Event Handler

// src/handlers/s3.handler.ts
import { S3Handler, S3Event, Context } from 'aws-lambda';
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';

// Static client instance
const s3Client = new S3Client({});

export const handler: S3Handler = async (event: S3Event, context: Context): Promise<void> => {
  console.log('S3 Event:', JSON.stringify(event));

  for (const record of event.Records) {
    const bucket = record.s3.bucket.name;
    const key = decodeURIComponent(record.s3.object.key.replace(/\+/g, ' '));

    try {
      // Process the S3 object
      const response = await s3Client.send(
        new GetObjectCommand({
          Bucket: bucket,
          Key: key,
        })
      );

      // Process based on file type
      if (key.endsWith('.json')) {
        await processJsonFile(response.Body, key);
      } else if (key.endsWith('.csv')) {
        await processCsvFile(response.Body, key);
      }

      console.log(`Successfully processed s3://${bucket}/${key}`);
    } catch (error) {
      console.error(`Error processing s3://${bucket}/${key}:`, error);
      throw error; // Let Lambda retry
    }
  }
};

async function processJsonFile(body: ReadableStream | undefined, key: string): Promise<void> {
  if (!body) return;
  // Implementation
}

async function processCsvFile(body: ReadableStream | undefined, key: string): Promise<void> {
  if (!body) return;
  // Implementation
}

SQS Event Handler

// src/handlers/sqs.handler.ts
import { SQSHandler, SQSEvent, SQSRecord, Context } from 'aws-lambda';

interface MessagePayload {
  type: string;
  data: unknown;
}

export const handler: SQSHandler = async (event: SQSEvent, context: Context): Promise<void> => {
  console.log('SQS Event:', {
    recordCount: event.Records.length,
    requestId: context.awsRequestId,
  });

  const batchItemFailures: { itemIdentifier: string }[] = [];

  for (const record of event.Records) {
    try {
      await processMessage(record);
    } catch (error) {
      console.error(`Failed to process message ${record.messageId}:`, error);
      batchItemFailures.push({ itemIdentifier: record.messageId });
    }
  }

  // Return partial batch response for failed items
  if (batchItemFailures.length > 0) {
    throw new Error(`Batch item failures: ${JSON.stringify(batchItemFailures)}`);
  }
};

async function processMessage(record: SQSRecord): Promise<void> {
  const payload: MessagePayload = JSON.parse(record.body);

  console.log('Processing message:', {
    messageId: record.messageId,
    type: payload.type,
  });

  switch (payload.type) {
    case 'SEND_EMAIL':
      await sendEmail(payload.data);
      break;
    case 'PROCESS_ORDER':
      await processOrder(payload.data);
      break;
    default:
      console.warn('Unknown message type:', payload.type);
  }
}

async function sendEmail(data: unknown): Promise<void> {
  // Implementation
}

async function processOrder(data: unknown): Promise<void> {
  // Implementation
}

Dependency Injection Patterns

Simple DI Container

// src/config/container.ts
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';

// Service interfaces
export interface IUserRepository {
  findById(id: string): Promise<User | null>;
  save(user: User): Promise<User>;
  delete(id: string): Promise<void>;
}

export interface IEmailService {
  send(to: string, subject: string, body: string): Promise<void>;
}

// Simple container
class Container {
  private services = new Map<string, unknown>();

  register<T>(key: string, factory: () => T): void {
    this.services.set(key, factory);
  }

  resolve<T>(key: string): T {
    const service = this.services.get(key);
    if (!service) {
      throw new Error(`Service ${key} not registered`);
    }
    if (typeof service === 'function') {
      const instance = (service as () => T)();
      this.services.set(key, instance); // Cache instance
      return instance;
    }
    return service as T;
  }
}

// Global container instance
export const container = new Container();

// Registration
export function initializeContainer(): void {
  // Database client
  container.register('dynamoClient', () => {
    const client = new DynamoDBClient({});
    return DynamoDBDocumentClient.from(client);
  });

  // Repositories
  container.register('userRepository', () => {
    return new DynamoDbUserRepository(container.resolve('dynamoClient'));
  });

  // Services
  container.register('userService', () => {
    return new UserService(container.resolve('userRepository'));
  });

  container.register('emailService', () => {
    return new SesEmailService();
  });
}

// Type definitions
export interface User {
  id: string;
  email: string;
  name: string;
}

// Repository implementation
class DynamoDbUserRepository implements IUserRepository {
  constructor(private client: DynamoDBDocumentClient) {}

  async findById(id: string): Promise<User | null> {
    // Implementation
    return null;
  }

  async save(user: User): Promise<User> {
    // Implementation
    return user;
  }

  async delete(id: string): Promise<void> {
    // Implementation
  }
}

// Service implementation
class UserService {
  constructor(private repository: IUserRepository) {}

  async findById(id: string): Promise<User | null> {
    return this.repository.findById(id);
  }
}

// Email service
class SesEmailService implements IEmailService {
  async send(to: string, subject: string, body: string): Promise<void> {
    // SES implementation
  }
}

Handler with DI

// src/handlers/di-handler.ts
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { container, initializeContainer } from '../config/container';

// Initialize container on first import
let initialized = false;

function ensureInitialized(): void {
  if (!initialized) {
    initializeContainer();
    initialized = true;
  }
}

export const handler = async (
  event: APIGatewayProxyEvent,
  context: Context
): Promise<APIGatewayProxyResult> => {
  ensureInitialized();

  const userService = container.resolve<UserService>('userService');

  // Handler logic
  return {
    statusCode: 200,
    body: JSON.stringify({ message: 'OK' }),
  };
};

Cold Start Optimization

Lazy Loading Pattern

// src/utils/lazy-loader.ts
export class LazyLoader<T> {
  private instance: T | null = null;
  private initializing = false;
  private initPromise: Promise<T> | null = null;

  constructor(private factory: () => Promise<T> | T) {}

  async get(): Promise<T> {
    if (this.instance) {
      return this.instance;
    }

    if (!this.initPromise) {
      this.initPromise = Promise.resolve(this.factory()).then((instance) => {
        this.instance = instance;
        return instance;
      });
    }

    return this.initPromise;
  }

  getSync(): T | null {
    return this.instance;
  }
}

// Usage
const dbConnection = new LazyLoader(async () => {
  console.log('Initializing database connection...');
  const connection = await createConnection({
    host: process.env.DB_HOST,
    database: process.env.DB_NAME,
    // Connection pool optimized for Lambda
    max: 2,
    min: 0,
    idleTimeoutMillis: 10000,
  });
  return connection;
});

// In handler
export const handler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
  const db = await dbConnection.get();
  // Use db
};

Module-Level Caching

// src/config/database.ts
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';

// Module-level cache - persists across warm invocations
let cachedClient: DynamoDBDocumentClient | null = null;

export function getDynamoClient(): DynamoDBDocumentClient {
  if (!cachedClient) {
    const client = new DynamoDBClient({
      // Optimize for Lambda
      maxAttempts: 3,
      requestHandler: {
        requestTimeout: 5000,
      },
    });

    cachedClient = DynamoDBDocumentClient.from(client, {
      marshallOptions: {
        convertEmptyValues: false,
        removeUndefinedValues: true,
        convertClassInstanceToMap: true,
      },
    });

    console.log('DynamoDB client initialized');
  }

  return cachedClient;
}

// For testing - allow reset
export function resetDynamoClient(): void {
  cachedClient = null;
}

TypeScript Configuration

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "lib": ["ES2022"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "removeComments": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedIndexedAccess": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
}

tsconfig.prod.json (Production)

{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "sourceMap": false,
    "declaration": false,
    "declarationMap": false,
    "removeComments": true
  },
  "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts", "tests"]
}

Build and Packaging

esbuild Configuration

// build.js
const esbuild = require('esbuild');

async function build() {
  try {
    await esbuild.build({
      entryPoints: [
        'src/handlers/api.handler.ts',
        'src/handlers/s3.handler.ts',
        'src/handlers/sqs.handler.ts',
      ],
      bundle: true,
      platform: 'node',
      target: 'node20',
      outdir: 'dist',
      format: 'esm',
      splitting: true,
      minify: true,
      sourcemap: true,
      external: [
        'aws-sdk', // Provided by Lambda runtime
      ],
      banner: {
        js: 'import { createRequire } from "module"; import { fileURLToPath } from "url"; import { dirname } from "path"; const require = createRequire(import.meta.url); const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename);',
      },
      metafile: true,
    });

    console.log('Build completed successfully');
  } catch (error) {
    console.error('Build failed:', error);
    process.exit(1);
  }
}

build();

SAM Template

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Globals:
  Function:
    Runtime: nodejs20.x
    MemorySize: 256
    Timeout: 10
    Architectures:
      - x86_64
    Environment:
      Variables:
        NODE_OPTIONS: '--enable-source-maps'

Resources:
  ApiFunction:
    Type: AWS::Serverless::Function
    Metadata:
      BuildMethod: esbuild
      BuildProperties:
        Minify: true
        Target: es2022
        Sourcemap: true
        EntryPoints:
          - src/handlers/api.handler.ts
        Format: esm
        Platform: node
    Properties:
      FunctionName: !Sub '${AWS::StackName}-api'
      Handler: api.handler
      CodeUri: ./
      Description: Raw TypeScript API handler
      Events:
        ApiRoot:
          Type: Api
          Properties:
            Path: /
            Method: ANY
        ApiProxy:
          Type: Api
          Properties:
            Path: /{proxy+}
            Method: ANY

  S3ProcessorFunction:
    Type: AWS::Serverless::Function
    Metadata:
      BuildMethod: esbuild
      BuildProperties:
        Minify: true
        Target: es2022
        EntryPoints:
          - src/handlers/s3.handler.ts
        Format: esm
    Properties:
      FunctionName: !Sub '${AWS::StackName}-s3-processor'
      Handler: s3.handler
      CodeUri: ./
      Events:
        S3Event:
          Type: S3
          Properties:
            Bucket: !Ref InputBucket
            Events: s3:ObjectCreated:*

  SQSProcessorFunction:
    Type: AWS::Serverless::Function
    Metadata:
      BuildMethod: esbuild
      BuildProperties:
        Minify: true
        Target: es2022
        EntryPoints:
          - src/handlers/sqs.handler.ts
        Format: esm
    Properties:
      FunctionName: !Sub '${AWS::StackName}-sqs-processor'
      Handler: sqs.handler
      CodeUri: ./
      Events:
        SQSEvent:
          Type: SQS
          Properties:
            Queue: !GetAtt ProcessingQueue.Arn
            BatchSize: 10
            FunctionResponseTypes:
              - ReportBatchItemFailures

  InputBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub '${AWS::StackName}-input'

  ProcessingQueue:
    Type: AWS::SQS::Queue
    Properties:
      QueueName: !Sub '${AWS::StackName}-processing'
      VisibilityTimeout: 60

Testing

Jest Configuration

// jest.config.js
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  roots: ['<rootDir>/src', '<rootDir>/tests'],
  testMatch: ['**/*.test.ts'],
  transform: {
    '^.+\\.ts$': 'ts-jest',
  },
  collectCoverageFrom: [
    'src/**/*.ts',
    '!src/**/*.d.ts',
    '!src/handlers/*.ts',
  ],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },
  },
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
  },
};

Handler Unit Test

// tests/handlers/api.handler.test.ts
import { APIGatewayProxyEvent, Context } from 'aws-lambda';
import { handler } from '../../src/handlers/api.handler';

const mockContext: Partial<Context> = {
  awsRequestId: 'test-request-id',
  functionName: 'test-function',
  memoryLimitInMB: '256',
  invokedFunctionArn: 'arn:aws:lambda:us-east-1:123456789:function:test',
  getRemainingTimeInMillis: () => 30000,
};

describe('API Handler', () => {
  beforeEach(() => {
    jest.clearAllMocks();
  });

  it('should return 200 for health check', async () => {
    const event: Partial<APIGatewayProxyEvent> = {
      httpMethod: 'GET',
      path: '/health',
      headers: {},
      queryStringParameters: null,
      body: null,
    };

    const result = await handler(event as APIGatewayProxyEvent, mockContext as Context);

    expect(result.statusCode).toBe(200);
    const body = JSON.parse(result.body);
    expect(body.status).toBe('ok');
  });

  it('should return 404 for unknown routes', async () => {
    const event: Partial<APIGatewayProxyEvent> = {
      httpMethod: 'GET',
      path: '/unknown',
      headers: {},
    };

    const result = await handler(event as APIGatewayProxyEvent, mockContext as Context);

    expect(result.statusCode).toBe(404);
  });

  it('should return 400 for missing user ID', async () => {
    const event: Partial<APIGatewayProxyEvent> = {
      httpMethod: 'GET',
      path: '/users/{id}',
      pathParameters: {},
      headers: {},
    };

    const result = await handler(event as APIGatewayProxyEvent, mockContext as Context);

    expect(result.statusCode).toBe(400);
  });

  it('should return 400 for invalid JSON body', async () => {
    const event: Partial<APIGatewayProxyEvent> = {
      httpMethod: 'POST',
      path: '/users',
      body: 'invalid json',
      headers: {},
    };

    const result = await handler(event as APIGatewayProxyEvent, mockContext as Context);

    expect(result.statusCode).toBe(400);
  });
});

Service Test with Mocks

// tests/services/user.service.test.ts
import { UserService } from '../../src/services/user.service';
import { IUserRepository } from '../../src/config/container';

const mockRepository: jest.Mocked<IUserRepository> = {
  findById: jest.fn(),
  save: jest.fn(),
  delete: jest.fn(),
};

describe('UserService', () => {
  let service: UserService;

  beforeEach(() => {
    service = new UserService(mockRepository);
    jest.clearAllMocks();
  });

  describe('findById', () => {
    it('should return user when found', async () => {
      const user = { id: '123', name: 'John', email: 'john@example.com' };
      mockRepository.findById.mockResolvedValue(user);

      const result = await service.findById('123');

      expect(result).toEqual(user);
      expect(mockRepository.findById).toHaveBeenCalledWith('123');
    });

    it('should return null when user not found', async () => {
      mockRepository.findById.mockResolvedValue(null);

      const result = await service.findById('999');

      expect(result).toBeNull();
    });
  });

  describe('create', () => {
    it('should create and return user', async () => {
      const input = { name: 'John', email: 'john@example.com' };
      const created = { id: '123', ...input };
      mockRepository.save.mockResolvedValue(created);

      const result = await service.create(input);

      expect(result).toEqual(created);
      expect(mockRepository.save).toHaveBeenCalledWith(expect.objectContaining(input));
    });
  });
});

Deployment

Serverless Framework

# serverless.yml
service: raw-ts-lambda

provider:
  name: aws
  runtime: nodejs20.x
  stage: ${opt:stage, 'dev'}
  region: ${opt:region, 'us-east-1'}
  memorySize: 256
  timeout: 10
  environment:
    NODE_OPTIONS: '--enable-source-maps'
    STAGE: ${self:provider.stage}

  iam:
    role:
      statements:
        - Effect: Allow
          Action:
            - logs:CreateLogGroup
            - logs:CreateLogStream
            - logs:PutLogEvents
          Resource:
            - !Sub 'arn:aws:logs:${aws:region}:${aws:accountId}:log-group:/aws/lambda/${self:service}-*'
        - Effect: Allow
          Action:
            - dynamodb:GetItem
            - dynamodb:PutItem
            - dynamodb:UpdateItem
            - dynamodb:DeleteItem
            - dynamodb:Query
            - dynamodb:Scan
          Resource:
            - !GetAtt UsersTable.Arn

plugins:
  - serverless-esbuild

custom:
  esbuild:
    bundle: true
    minify: ${self:custom.isProduction}
    sourcemap: true
    target: node20
    platform: node
    format: esm
    splitting: true
    entryPoints:
      - src/handlers/api.handler.ts
      - src/handlers/s3.handler.ts
      - src/handlers/sqs.handler.ts
    external:
      - aws-sdk

  isProduction: !Equals ['${self:provider.stage}', 'prod']

functions:
  api:
    handler: dist/api.handler
    events:
      - http:
          path: /{proxy+}
          method: ANY
          cors: true

  s3Processor:
    handler: dist/s3.handler
    events:
      - s3:
          bucket: ${self:service}-input-${self:provider.stage}
          event: s3:ObjectCreated:*

  sqsProcessor:
    handler: dist/sqs.handler
    events:
      - sqs:
          arn:
            Fn::GetAtt:
              - ProcessingQueue
              - Arn
          batchSize: 10
          functionResponseType: ReportBatchItemFailures

resources:
  Resources:
    UsersTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: ${self:service}-users-${self:provider.stage}
        BillingMode: PAY_PER_REQUEST
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: S
        KeySchema:
          - AttributeName: id
            KeyType: HASH

    ProcessingQueue:
      Type: AWS::SQS::Queue
      Properties:
        QueueName: ${self:service}-processing-${self:provider.stage}
        VisibilityTimeout: 60

Best Practices Summary

  1. Use static/module-level caching for AWS clients and connections
  2. Implement lazy loading for expensive resources
  3. Keep dependencies minimal - only include what you need
  4. Use esbuild for fast bundling and smaller package size
  5. Implement proper error handling with structured responses
  6. Use TypeScript strict mode for better type safety
  7. Write unit tests with mocked dependencies
  8. Use environment variables for configuration
  9. Enable source maps for easier debugging
  10. Monitor cold starts and optimize initialization code

Cold Start Benchmarks

Configuration Cold Start Warm Start Memory
Minimal (no deps) ~50ms ~2ms 128MB
With DynamoDB client ~150ms ~5ms 256MB
With full AWS SDK ~300ms ~10ms 512MB

When to Use Raw TypeScript vs NestJS

Use Raw TypeScript when:

  • Maximum performance is critical
  • Minimal cold start is required
  • Simple handlers with minimal logic
  • Small team with no framework experience
  • Cost optimization is priority

Use NestJS when:

  • Complex application architecture
  • Team familiar with NestJS
  • Need dependency injection container
  • Multiple modules and services
  • Enterprise-grade requirements