Skip to content

Lambda Deployment

Glasswork is optimized for AWS Lambda with small bundle sizes, fast cold starts, and native compatibility. This guide covers building and deploying your application to Lambda.

Supported Versions

  • Node.js 20, 22 or 24 runtimes
  • ESM bundles with top-level await
  • esbuild (v0.19+) for examples shown here

Lambda-Ready by Default

Glasswork applications are Lambda-ready out of the box:

typescript
// src/server.ts
import { serve } from '@hono/node-server';
import { bootstrap, isLambda } from 'glasswork';
import { handle } from 'hono/aws-lambda';
import { AppModule } from './app.module';

const { app } = await bootstrap(AppModule, {
  openapi: {
    enabled: true,
    serveSpecs: !isLambda(), // Only serve locally
    serveUI: !isLambda(),
  },
});

// Export handler for Lambda
export const handler = handle(app);

// Start local server if not in Lambda
if (!isLambda()) {
  const port = Number(process.env.PORT) || 3000;
  console.log(`Server running on http://localhost:${port}`);
  serve({ fetch: app.fetch, port });
}

The same code runs locally and in Lambda without changes.

Project Configuration

TypeScript Configuration

Glasswork uses async bootstrap() which requires top-level await. Configure TypeScript for ESM:

json
// tsconfig.json
{
  "compilerOptions": {
    "lib": ["es2024"],
    "module": "ESNext",
    "target": "es2022",
    "moduleResolution": "bundler",
    "esModuleInterop": true,
    "strict": true,
    "skipLibCheck": true,
    "outDir": "dist",
    "declaration": true
  },
  "include": ["src/**/*"]
}

Package Configuration

Enable ESM in your package.json:

json
// package.json
{
  "type": "module",
  "scripts": {
    "build": "tsx build.ts",
    "dev": "tsx watch src/server.ts"
  }
}

Building for Lambda

esbuild Configuration

Use esbuild to create optimized ESM Lambda bundles:

typescript
// build.ts
import * as esbuild from 'esbuild';
import { analyzeMetafile } from 'esbuild';

const sharedConfig: esbuild.BuildOptions = {
  platform: 'node',
  target: 'node22',
  format: 'esm',
  bundle: true,
  minify: true,
  keepNames: true, // Required for Awilix PROXY mode
  sourcemap: false,
  metafile: true,
  external: ['@aws-sdk/*'], // AWS SDK available in Lambda runtime
  treeShaking: true,
  drop: ['debugger'],
  // Create require() shim for ESM bundles that include CJS dependencies
  banner: {
    js: `import { createRequire } from 'module'; const require = createRequire(import.meta.url);`,
  },
};

async function build() {
  try {
    const result = await esbuild.build({
      ...sharedConfig,
      entryPoints: ['src/server.ts'],
      outfile: 'dist/api.mjs', // Use .mjs extension for ESM
    });

    if (result.metafile) {
      const analysis = await analyzeMetafile(result.metafile);
      console.log('Bundle analysis:', analysis);
    }

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

build();

Key settings:

  • format: 'esm' - ESM format for top-level await support
  • keepNames: true - Critical for Awilix dependency injection (preserves class/property names)
  • external: ['@aws-sdk/*'] - Excludes AWS SDK (included in Lambda runtime)
  • minify: true - Reduces bundle size for faster cold starts
  • treeShaking: true - Removes unused code
  • banner.js - Creates require() shim for CJS dependencies (like Prisma)
  • outfile: 'dist/api.mjs' - Use .mjs extension so Lambda recognizes ESM

Install esbuild:

bash
npm install -D esbuild tsx
bash
pnpm add -D esbuild tsx
bash
yarn add -D esbuild tsx

Bundle Size

Expect bundle sizes under 1MB:

  • Glasswork + Hono + Valibot: ~200-300KB
  • With Prisma: ~800KB-1MB
  • Cold start: 100-300ms

Deployment Options

Choose the infrastructure-as-code tool that fits your workflow:

AWS SAM

AWS Serverless Application Model (SAM) provides a simple deployment experience.

template.yaml:

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

Resources:
  ApiFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: dist/
      Handler: api.handler
      Runtime: nodejs22.x
      Architectures:
        - arm64  # Graviton2: Better price/performance
      MemorySize: 256
      Timeout: 10
      Environment:
        Variables:
          NODE_OPTIONS: '--max-old-space-size=256'
          DATABASE_URL: !Ref DatabaseUrl
      FunctionUrlConfig:
        AuthType: NONE
        Cors:
          AllowOrigins:
            - 'https://example.com'
          AllowMethods:
            - GET
            - POST
            - PUT
            - PATCH
            - DELETE
          AllowHeaders:
            - '*'

Parameters:
  DatabaseUrl:
    Type: String
    Description: Database connection string

Deploy:

bash
npm run build
sam build
sam deploy --guided

AWS CDK

Use TypeScript for infrastructure:

typescript
// lib/api-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as lambdaNodejs from 'aws-cdk-lib/aws-lambda-nodejs';

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

    const apiFunction = new lambdaNodejs.NodejsFunction(this, 'ApiFunction', {
      entry: 'src/server.ts',
      handler: 'handler',
      runtime: lambda.Runtime.NODEJS_22_X,
      memorySize: 256,
      timeout: cdk.Duration.seconds(10),
      bundling: {
        minify: true,
        sourceMap: false,
        target: 'node22',
        keepNames: true, // Required for Awilix
        externalModules: ['@aws-sdk/*'],
      },
      environment: {
        NODE_OPTIONS: '--max-old-space-size=256',
        DATABASE_URL: process.env.DATABASE_URL!,
      },
    });

    // Add Function URL
    const functionUrl = apiFunction.addFunctionUrl({
      authType: lambda.FunctionUrlAuthType.NONE,
      cors: {
        allowedOrigins: ['https://example.com'],
        allowedMethods: [lambda.HttpMethod.ALL],
        allowedHeaders: ['*'],
      },
    });

    new cdk.CfnOutput(this, 'ApiUrl', {
      value: functionUrl.url,
    });
  }
}

Deploy:

bash
cdk deploy

Serverless Framework

Configuration-driven deployment:

yaml
# serverless.yml
service: glasswork-api

provider:
  name: aws
  runtime: nodejs22.x
  memorySize: 256
  timeout: 10
  environment:
    NODE_OPTIONS: '--max-old-space-size=256'
    DATABASE_URL: ${env:DATABASE_URL}

functions:
  api:
    handler: dist/api.handler
    architecture: arm64  # Graviton2: Better price/performance
    url:
      cors:
        allowedOrigins:
          - https://example.com
        allowedHeaders:
          - '*'
        allowedMethods:
          - GET
          - POST
          - PUT
          - PATCH
          - DELETE

package:
  individually: true
  patterns:
    - dist/**
    - '!node_modules/**'

Deploy:

bash
npm run build
serverless deploy

Terraform

Infrastructure as code with Terraform:

hcl
# main.tf
resource "aws_lambda_function" "api" {
  filename         = "dist/api.zip"
  function_name    = "glasswork-api"
  role            = aws_iam_role.lambda_role.arn
  handler         = "api.handler"
  runtime         = "nodejs22.x"
  architectures    = ["arm64"]  # Graviton2: Better price/performance
  memory_size     = 256
  timeout         = 10
  source_code_hash = filebase64sha256("dist/api.zip")

  environment {
    variables = {
      NODE_OPTIONS  = "--max-old-space-size=256"
      DATABASE_URL  = var.database_url
    }
  }
}

resource "aws_lambda_function_url" "api_url" {
  function_name      = aws_lambda_function.api.function_name
  authorization_type = "NONE"

  cors {
    allow_origins = ["https://example.com"]
    allow_methods = ["GET", "POST", "PUT", "PATCH", "DELETE"]
    allow_headers = ["*"]
  }
}

output "api_url" {
  value = aws_lambda_function_url.api_url.function_url
}

Deploy:

bash
npm run build
zip -r dist/api.zip dist/api.mjs
terraform apply

CloudFront Integration

For production, place Lambda behind CloudFront for:

  • Custom domains
  • Caching
  • WAF protection
  • Global edge locations

Example CloudFront distribution:

yaml
# SAM template.yaml
  CloudFrontDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        Enabled: true
        Origins:
          - Id: ApiOrigin
            DomainName: !Select [2, !Split ['/', !GetAtt ApiFunctionUrl.FunctionUrl]]
            CustomOriginConfig:
              OriginProtocolPolicy: https-only
        DefaultCacheBehavior:
          TargetOriginId: ApiOrigin
          ViewerProtocolPolicy: redirect-to-https
          AllowedMethods: [GET, HEAD, OPTIONS, PUT, POST, PATCH, DELETE]
          CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad # CachingDisabled
          OriginRequestPolicyId: b689b0a8-53d0-40ab-baf2-68738e2966ac # AllViewerExceptHostHeader

Environment Variables

Pass configuration through environment variables:

yaml
Environment:
  Variables:
    NODE_ENV: production
    DATABASE_URL: !Ref DatabaseUrl
    API_KEY: !Ref ApiKey
    # Add more as needed

Access in your application:

typescript
import { createConfig, envProvider } from 'glasswork';

const config = await createConfig({
  schema: ConfigSchema,
  providers: [envProvider()],
});

Alternative: SSM Parameter Store

Instead of environment variables, use the ssmProvider to load configuration from AWS Systems Manager Parameter Store. This is especially useful for sensitive values or when you want centralized configuration management.

See the Environment Config - AWS SSM Provider for details.

Performance Optimization

Memory Configuration

Lambda bills by GB-second. Test different memory sizes:

  • 256MB: Good for simple APIs (< 100 req/s)
  • 512MB: Better for database queries
  • 1024MB: High throughput, complex operations

Higher memory = more CPU, often cheaper due to faster execution.

Cold Start Optimization

Glasswork is already optimized:

  • Small bundle size (< 1MB)
  • Lazy loading with tree shaking
  • Minimal dependencies
  • PROXY mode DI (no reflection)

Additional optimizations:

  • Provisioned concurrency (for critical paths)
  • Lambda SnapStart (Node.js 20+)
  • Connection pooling (see Prisma section)

Database Connections

Prisma with Lambda

Use Prisma Data Proxy or connection pooling:

typescript
// src/database/prisma.service.ts
import { PrismaClient } from '@prisma/client';

export class PrismaService extends PrismaClient {
  constructor() {
    super({
      datasourceUrl: process.env.DATABASE_URL,
    });
  }

  async onModuleDestroy() {
    await this.$disconnect();
  }
}

Connection pooling (using Prisma Accelerate or PgBouncer):

bash
DATABASE_URL="postgresql://user:pass@host:5432/db?pgbouncer=true&connection_limit=1"

Connection Limits

Lambda can scale to thousands of concurrent instances. Limit connections:

typescript
const prisma = new PrismaClient({
  datasourceUrl: process.env.DATABASE_URL,
  // Lambda: 1 connection per instance
  // RDS Proxy handles pooling
});

Monitoring

CloudWatch Logs

Lambda automatically logs to CloudWatch:

typescript
import { createLogger } from 'glasswork';

const logger = createLogger('UserService');

logger.info('User created', { userId: user.id });
logger.error('Failed to create user', error);

Metrics

Track cold starts, duration, and errors in CloudWatch:

yaml
# SAM template.yaml
  ApiFunction:
    Properties:
      # Enable X-Ray tracing
      Tracing: Active

Testing Lambda

Test locally with SAM CLI:

bash
# Start local API
sam local start-api

# Invoke function
sam local invoke ApiFunction --event event.json

Or use the AWS Lambda Runtime Interface Emulator:

bash
npm install -D @aws-sdk/client-lambda
bash
pnpm add -D @aws-sdk/client-lambda
bash
yarn add -D @aws-sdk/client-lambda
bash
# Run locally
node --import=@aws-sdk/client-lambda dist/api.js

Troubleshooting

Bundle Too Large

Check what's included:

typescript
const result = await esbuild.build({
  metafile: true,
  // ...
});

console.log(await analyzeMetafile(result.metafile));

Common culprits:

  • Prisma client (~800KB) - expected
  • Multiple Valibot imports - use single import
  • Unused dependencies - check tree shaking

Cold Starts

Profile with CloudWatch Insights:

sql
fields @timestamp, @duration
| filter @type = "REPORT"
| stats avg(@duration), max(@duration), min(@duration)

Memory Issues

Increase memory or optimize:

yaml
MemorySize: 512 # MB

Monitor with:

sql
fields @timestamp, @maxMemoryUsed / 1000000 as maxMemoryUsedMB
| filter @type = "REPORT"
| stats avg(maxMemoryUsedMB), max(maxMemoryUsedMB)

Learn More

Released under the MIT License.