AWS Lambda

Deploy your Vercube application to AWS Lambda with API Gateway

Deploy your Vercube application to AWS Lambda with full support for API Gateway v1 and v2, automatic binary content handling, and seamless cookie management.

Basic Setup

lambda.ts
import { createApp } from '@vercube/core';
import { toServerlessHandler } from '@vercube/serverless/aws-lambda';

const app = createApp();

// Your controllers are automatically registered
export const handler = toServerlessHandler(app);

Supported API Gateway Versions

The adapter automatically detects and supports both API Gateway versions:

API Gateway v1 (REST API)

Works with APIGatewayProxyEvent:

// API Gateway v1 Event Format
{
  httpMethod: 'GET',
  path: '/api/users',
  headers: {
    'content-type': 'application/json',
    'authorization': 'Bearer token...'
  },
  body: '{"name":"John"}',
  queryStringParameters: {
    page: '1',
    limit: '10'
  },
  pathParameters: {
    id: '123'
  }
}

Key Features:

  • Traditional REST API structure
  • Simple header handling
  • Cookie handling via Set-Cookie header
  • Binary content with isBase64Encoded flag

API Gateway v2 (HTTP API)

Works with APIGatewayProxyEventV2:

// API Gateway v2 Event Format
{
  requestContext: {
    http: {
      method: 'GET',
      path: '/api/users'
    }
  },
  headers: {
    'content-type': 'application/json',
    'authorization': 'Bearer token...'
  },
  body: '{"name":"John"}',
  queryStringParameters: {
    page: '1',
    limit: '10'
  },
  pathParameters: {
    id: '123'
  }
}

Key Features:

  • Improved performance and lower cost
  • Enhanced cookie handling with cookies array
  • Streamlined event structure
  • Better WebSocket support

Binary Content Handling

Binary responses are automatically encoded as base64:

@Controller('/files')
export class FileController {
  
  @Inject(FileService)
  private fileService!: FileService;
  
  @Get('/download/:id')
  async downloadFile(@Param('id') id: string) {
    const fileBuffer = await this.fileService.getFile(id);
    
    return new Response(fileBuffer, {
      headers: {
        'Content-Type': 'application/pdf',
        'Content-Disposition': `attachment; filename="file-${id}.pdf"`
      }
    });
  }
  
  @Get('/image/:id')
  async getImage(@Param('id') id: string) {
    const imageBuffer = await this.fileService.getImage(id);
    
    return new Response(imageBuffer, {
      headers: {
        'Content-Type': 'image/png'
      }
    });
  }
}

The adapter automatically:

  1. Detects binary content types
  2. Encodes the body as base64
  3. Sets isBase64Encoded: true in the response
  4. API Gateway decodes it for the client

Configure Binary Media Types

Tell API Gateway which content types should be treated as binary:

serverless.yml
functions:
  api:
    handler: lambda.handler
    events:
      - http:
          path: /{proxy+}
          method: ANY
          binaryMediaTypes:
            - 'image/*'
            - 'application/pdf'
            - 'application/zip'
            - 'application/octet-stream'

Cookies work seamlessly across both API Gateway versions:

@Controller('/auth')
export class AuthController {
  
  @Inject(AuthService)
  private authService!: AuthService;
  
  @Post('/login')
  async login(@Body({ validationSchema: LoginSchema }) credentials: LoginDto) {
    const token = await this.authService.generateToken(credentials);
    
    // Set authentication cookie
    return FastResponse.ok({ success: true })
      .cookie('auth_token', token, {
        httpOnly: true,
        secure: true,
        maxAge: 3600,
        sameSite: 'strict'
      });
  }
  
  @Post('/logout')
  async logout() {
    // Clear authentication cookie
    return FastResponse.ok({ success: true })
      .cookie('auth_token', '', {
        httpOnly: true,
        secure: true,
        maxAge: 0
      });
  }
  
  @Get('/session')
  async getSession(@Cookie('auth_token') token: string) {
    if (!token) {
      throw new UnauthorizedException('Not authenticated');
    }
    
    const session = await this.authService.validateToken(token);
    return { session };
  }
}

How it works:

  • API Gateway v1: Cookies set via Set-Cookie header
  • API Gateway v2: Cookies set via cookies array for better handling

Environment Variables

Access Lambda-specific environment variables:

import { RuntimeConfig } from '@vercube/core';

@Controller('/config')
export class ConfigController {
  
  @Get('/lambda-info')
  getLambdaInfo() {
    return {
      // AWS Lambda environment variables
      region: process.env.AWS_REGION,
      functionName: process.env.AWS_LAMBDA_FUNCTION_NAME,
      functionVersion: process.env.AWS_LAMBDA_FUNCTION_VERSION,
      memoryLimit: process.env.AWS_LAMBDA_FUNCTION_MEMORY_SIZE,
      logGroup: process.env.AWS_LAMBDA_LOG_GROUP_NAME,
      logStream: process.env.AWS_LAMBDA_LOG_STREAM_NAME
    };
  }
  
  @Get('/runtime-info')
  getRuntimeInfo() {
    return {
      // Runtime execution details
      requestId: process.env.AWS_REQUEST_ID,
      executionEnv: process.env.AWS_EXECUTION_ENV,
      runtime: process.env.AWS_LAMBDA_RUNTIME_API
    };
  }
}

Serverless Framework Configuration

Basic Configuration

serverless.yml
service: vercube-api

provider:
  name: aws
  runtime: nodejs22.x
  region: us-east-1
  memorySize: 512
  timeout: 30
  
  # Environment variables
  environment:
    NODE_ENV: production
    DATABASE_URL: ${env:DATABASE_URL}
    JWT_SECRET: ${env:JWT_SECRET}

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

Advanced Configuration

serverless.yml
service: vercube-api

provider:
  name: aws
  runtime: nodejs22.x
  region: us-east-1
  
  # Performance settings
  memorySize: 1024
  timeout: 60
  
  # VPC configuration (for database access)
  vpc:
    securityGroupIds:
      - sg-xxxxxxxxx
    subnetIds:
      - subnet-xxxxxxxxx
      - subnet-yyyyyyyyy
  
  # IAM permissions
  iam:
    role:
      statements:
        - Effect: Allow
          Action:
            - s3:GetObject
            - s3:PutObject
          Resource: 'arn:aws:s3:::my-bucket/*'
        - Effect: Allow
          Action:
            - dynamodb:Query
            - dynamodb:Scan
            - dynamodb:GetItem
            - dynamodb:PutItem
          Resource: 'arn:aws:dynamodb:${aws:region}:*:table/my-table'
  
  # Environment variables
  environment:
    NODE_ENV: ${opt:stage, 'dev'}
    DATABASE_URL: ${env:DATABASE_URL}
    REDIS_URL: ${env:REDIS_URL}
    JWT_SECRET: ${env:JWT_SECRET}
    S3_BUCKET: ${env:S3_BUCKET}

functions:
  api:
    handler: lambda.handler
    
    # Provisioned concurrency for predictable performance
    provisionedConcurrency: 2
    
    # Reserved concurrent executions
    reservedConcurrency: 100
    
    events:
      - http:
          path: /{proxy+}
          method: ANY
          cors:
            origin: '*'
            headers:
              - Content-Type
              - Authorization
              - X-Api-Key
            allowCredentials: true
          
          # Binary media types
          binaryMediaTypes:
            - 'image/*'
            - 'application/pdf'
            - 'application/zip'
    
    # Layer for dependencies
    layers:
      - arn:aws:lambda:us-east-1:xxxxx:layer:my-dependencies:1
    
    # Tags
    tags:
      Environment: ${opt:stage, 'dev'}
      Service: vercube-api

# Plugins
plugins:
  - serverless-offline
  - serverless-plugin-typescript

# Custom configuration
custom:
  serverless-offline:
    httpPort: 3000

Cold Start Optimization

Minimize cold start times for better performance:

Module-Level Initialization

lambda.ts
import { createApp } from '@vercube/core';
import { toServerlessHandler } from '@vercube/serverless/aws-lambda';
import { DatabaseService } from './services/Database';

// Initialize app at module level (happens once per container)
const app = createApp({
  setup: async (app) => {
    // Bind services
    app.container.bind(DatabaseService);
    
    // Initialize database connection (reused across invocations)
    const db = app.container.get(DatabaseService);
    await db.connect();
  }
});

// Export handler (execution is fast)
export const handler = toServerlessHandler(app);

Lazy Loading Heavy Dependencies

export class HeavyServiceFactory {
  private static instance: HeavyService | null = null;
  
  static async getInstance() {
    if (!this.instance) {
      // Only import when needed
      const { HeavyService } = await import('./HeavyService');
      this.instance = new HeavyService();
    }
    return this.instance;
  }
}

@Controller('/heavy')
export class HeavyController {
  
  @Get('/process')
  async process() {
    // Load heavy service only when this endpoint is called
    const service = await HeavyServiceFactory.getInstance();
    return await service.process();
  }
}

Provisioned Concurrency

Keep functions warm to eliminate cold starts:

serverless.yml
functions:
  api:
    handler: lambda.handler
    provisionedConcurrency: 5  # Keep 5 instances always warm
    
    # Or use auto-scaling
    provisionedConcurrency:
      minCapacity: 2
      maxCapacity: 10
      targetUtilizationPercent: 0.75

Database Connections

Handle database connections efficiently in Lambda:

Connection Pooling

export class DatabaseService {
  private static pool: Pool | null = null;
  
  async getPool() {
    // Reuse connection pool across invocations
    if (DatabaseService.pool) {
      return DatabaseService.pool;
    }
    
    DatabaseService.pool = new Pool({
      host: process.env.DB_HOST,
      port: Number(process.env.DB_PORT),
      user: process.env.DB_USER,
      password: process.env.DB_PASSWORD,
      database: process.env.DB_NAME,
      
      // Lambda-optimized settings
      max: 1,  // Single connection per Lambda instance
      idleTimeoutMillis: 30000,
      connectionTimeoutMillis: 2000
    });
    
    return DatabaseService.pool;
  }
  
  async query(sql: string, params?: any[]) {
    const pool = await this.getPool();
    return await pool.query(sql, params);
  }
}

RDS Proxy

Use RDS Proxy to manage database connections:

serverless.yml
provider:
  environment:
    DB_HOST: my-rds-proxy.proxy-xxxxxxxxx.us-east-1.rds.amazonaws.com
    DB_PORT: 5432
  
  iam:
    role:
      statements:
        - Effect: Allow
          Action:
            - rds-db:connect
          Resource: 'arn:aws:rds-db:us-east-1:xxxxx:dbuser:prx-xxxxx/*'

Advanced Response Handling

Custom Headers

@Controller('/api')
export class ApiController {
  
  @Get('/cached-data')
  getCachedData() {
    return FastResponse.ok({ data: 'cached' })
      .header('Cache-Control', 'public, max-age=3600')
      .header('X-Custom-Header', 'custom-value');
  }
  
  @Get('/streaming-data')
  getStreamingData() {
    const stream = this.createDataStream();
    
    return new Response(stream, {
      headers: {
        'Content-Type': 'application/json',
        'Transfer-Encoding': 'chunked'
      }
    });
  }
}

Error Handling

import { NotFoundException, BadRequestException, InternalServerErrorException } from '@vercube/core';

@Controller('/users')
export class UserController {
  
  @Get('/:id')
  async getUser(@Param('id') id: string) {
    try {
      const user = await this.userService.findById(id);
      
      if (!user) {
        throw new NotFoundException(`User with ID ${id} not found`);
      }
      
      return user;
    } catch (error) {
      if (error instanceof NotFoundException) {
        throw error;
      }
      
      // Log unexpected errors
      console.error('Error fetching user:', error);
      throw new InternalServerErrorException('Failed to fetch user');
    }
  }
}

// Automatically returns proper AWS response:
// {
//   statusCode: 404,
//   headers: { 'Content-Type': 'application/json' },
//   body: '{"statusCode":404,"message":"User with ID ... not found"}'
// }

Deployment

Deploy to AWS

# Deploy to default stage (dev)
serverless deploy

# Deploy to production
serverless deploy --stage prod

# Deploy specific function
serverless deploy function -f api

# Deploy with verbose output
serverless deploy --verbose

Environment-Specific Deployments

serverless.yml
service: vercube-api

provider:
  name: aws
  runtime: nodejs22.x
  region: ${opt:region, 'us-east-1'}
  stage: ${opt:stage, 'dev'}
  
  environment:
    NODE_ENV: ${self:provider.stage}
    DATABASE_URL: ${env:DATABASE_URL_${self:provider.stage}}

functions:
  api:
    handler: lambda.handler
# Deploy to development
serverless deploy --stage dev

# Deploy to production
serverless deploy --stage prod --region us-west-2

Monitoring and Debugging

CloudWatch Metrics

Monitor your Lambda function performance:

serverless.yml
functions:
  api:
    handler: lambda.handler
    
    # Enable detailed CloudWatch metrics
    tracing:
      lambda: true
      apiGateway: true

Key Metrics to Monitor:

  • Invocations - Number of times function is invoked
  • Duration - Execution time per invocation
  • Errors - Number of failed invocations
  • Throttles - Number of throttled invocations
  • ConcurrentExecutions - Number of concurrent invocations
  • IteratorAge - For stream-based invocations

X-Ray Tracing

Enable AWS X-Ray for detailed tracing:

serverless.yml
provider:
  tracing:
    lambda: true
    apiGateway: true

functions:
  api:
    handler: lambda.handler
import AWSXRay from 'aws-xray-sdk-core';
import AWS from 'aws-sdk';

// Wrap AWS SDK
const XAWS = AWSXRay.captureAWS(AWS);

@Controller('/traced')
export class TracedController {
  
  @Get('/data')
  async getData() {
    // This will appear in X-Ray traces
    const segment = AWSXRay.getSegment();
    const subsegment = segment.addNewSubsegment('custom-operation');
    
    try {
      const data = await this.processData();
      subsegment.close();
      return data;
    } catch (error) {
      subsegment.addError(error);
      subsegment.close();
      throw error;
    }
  }
}

Troubleshooting

Common Issues

Handler not found

Error: Cannot find module 'lambda'

Solution: Ensure handler path matches your file structure:

functions:
  api:
    handler: lambda.handler  # <filename>.<export name>

Request timeout

Task timed out after 30.00 seconds

Solution: Increase timeout:

functions:
  api:
    timeout: 60

Binary content corrupted

Response body appears corrupted or truncated

Solution: Configure binary media types:

functions:
  api:
    events:
      - http:
          binaryMediaTypes:
            - 'image/*'
            - 'application/pdf'

Cold start too slow

Duration: 3000ms (Cold Start: 2500ms)

Solutions:

  • Reduce deployment package size
  • Use Lambda layers for dependencies
  • Enable provisioned concurrency
  • Lazy load heavy modules

Memory limit exceeded

Process exited before completing request

Solution: Increase memory:

functions:
  api:
    memorySize: 1024  # or higher
Previous

Azure Functions

Deploy your Vercube application to Azure Functions with HTTP triggers

Next