Skip to main content
The CGIAR Risk Intelligence API is a NestJS application deployed to AWS Lambda. A single deployment package is used for both the API Lambda (HTTP requests) and Worker Lambda (background jobs).

Prerequisites

  • AWS CLI configured with valid credentials
  • pnpm installed
  • jq, zip installed
  • CloudFormation stack AllianceRiskStack already deployed
  • Deployment bucket exists (alliance-risk-deploy-{env})

Quick Start

From the project root:
This runs scripts/deploy-api.sh, which handles the full deployment pipeline.

Deployment Pipeline

The deploy-api.sh script performs the following steps:

1. Build TypeScript with tsc

Why tsc first? NestJS uses TypeScript decorators extensively (@Injectable(), @Controller(), etc.). Decorator metadata must be preserved, so we compile with tsc before bundling.

2. Bundle with esbuild

The deployment package is created using esbuild to inline all dependencies into single-file bundles:
Result: All 1,400+ NestJS node_modules packages are inlined into two single-file bundles:
  • dist/src/lambda.js (~4 MB)
  • dist/src/worker.js (~4 MB)

3. Copy External Packages

Some packages cannot be bundled and must be copied separately: The script uses pnpm symlink resolution to locate these packages in the .pnpm store and copies them to node_modules/ in the deployment package. Optimization: Only PostgreSQL WASM binaries are copied (other DB engines excluded), and all .d.ts, .map, README.md, test directories are removed.

4. Copy Prisma Schema

The Prisma schema is required at runtime for Prisma Client.

5. Create Deployment Package

Package Size:
  • Before esbuild: ~80 MB (all node_modules)
  • After esbuild: ~8-10 MB (bundles + external packages only)
Deployment Time:
  • Before esbuild: 10+ minutes
  • After esbuild: Less than 2 minutes

6. Upload to S3

The deployment bucket is created during infrastructure deployment.

7. Update Lambda Functions

Both Lambda functions are updated with the same deployment package (different entry points):

Lambda Configuration

API Lambda

Function Name: alliance-risk-api Handler: dist/src/lambda.handler Entry Point (packages/api/src/lambda.ts):
Key Details:
  • context.callbackWaitsForEmptyEventLoop = false — Required to prevent Lambda timeout due to Prisma connection pool
  • Caching: NestJS app instance is cached across Lambda warm starts for performance
  • @codegenie/serverless-express: Adapts NestJS to AWS Lambda proxy events (API Gateway HTTP API payload format 2.0)
Timeout: 29 seconds (< API Gateway 30s limit) Memory: 1024 MB VPC: Attached to private subnets for RDS access

Worker Lambda

Function Name: alliance-risk-worker Handler: dist/src/worker.handler Entry Point (packages/api/src/worker.ts):
Timeout: 900 seconds (15 minutes) Memory: 1024 MB VPC: Attached to private subnets for RDS access Special Action: run-sql — Used by migrate-remote.sh to run database migrations (see Database Migrations)

Environment Variables

Both Lambda functions receive these environment variables (defined in the CloudFormation template):

API Lambda

Worker Lambda

DATABASE_URL Construction

The DATABASE_URL is constructed dynamically in the CloudFormation template using Secrets Manager and RDS endpoint:
Critical: Without this variable, Prisma falls back to localhost:5432 and all database operations fail.

Critical TypeScript Configuration

The API package must have esModuleInterop: true in packages/api/tsconfig.json:
Why? Without esModuleInterop, imports like import express from 'express' compile to express_1.default() without the __importDefault helper, causing runtime crashes in Lambda:

Deployment Order

When deploying from scratch:
For routine code changes (no infrastructure or schema changes), only step 3 is needed.

Troubleshooting

Lambda returns HTTP 500 (often masked as CORS error on frontend)

Check CloudWatch Logs:
Common Causes:

Lambda timeout during cold start

Cause: Prisma connection pool keeps event loop alive Solution: Ensure context.callbackWaitsForEmptyEventLoop = false in both lambda.ts and worker.ts

Deployment package too large (>50 MB)

Cause: External packages not optimized Solution: Check cleanup logic in deploy-api.sh (removes .d.ts, .map, README.md, test directories)

esbuild warnings about dynamic requires

Expected. Packages like Prisma use dynamic require() and must be external. Warnings can be ignored.

Performance Optimization

  • ARM64 Architecture: 20% cost reduction
  • Lambda Caching: NestJS app instance cached across warm starts
  • Tree Shaking: Unused code eliminated by esbuild
  • Minification: Syntax and whitespace minified (not identifier mangling)
  • Selective Prisma Binaries: Only PostgreSQL WASM copied (SQLite, MySQL excluded)

Security Best Practices

  • Secrets Manager: Database credentials never in environment variables (dynamically resolved)
  • VPC Isolation: Lambdas in private VPC, no public internet access (except via VPC endpoints)
  • Least Privilege IAM: Lambda roles grant only required permissions
  • No Hardcoded Credentials: All AWS services accessed via IAM roles

Rollback

If a deployment fails, the previous deployment package is still available in S3:
Consider versioning S3 keys with timestamps for easier rollback: