Skip to main content

Basic Lambda Example

A complete, production-ready example of building and deploying a Lambda function with blue-green deployment using sdlcs-aws-cdk-lib.

Overview

This example demonstrates:

  • Setting up a Lambda function with TypeScript
  • Using the BlueGreenLambda construct
  • Environment-specific configuration
  • API Gateway integration
  • DynamoDB table integration
  • Testing and deployment

Project Structure

my-app/
├── bin/
│ └── app.ts # CDK app entry point
├── lib/
│ ├── MyStack.ts # CDK stack definition
│ └── lambda/
│ └── BlueGreenLambda.ts # Blue-green construct
├── lib-src/
│ └── lambda/
│ ├── package.json # Lambda workspace config
│ └── myApi/
│ ├── package.json # Function dependencies
│ ├── tsconfig.json # TypeScript config
│ ├── src/
│ │ └── index.ts # Lambda handler
│ └── test/
│ └── index.test.ts # Unit tests
├── package.json
├── tsconfig.json
└── cdk.json

Step 1: Create the CDK Stack

// lib/MyStack.ts
import { Stack, StackProps } from '@root/aws-cdk-lib';
import { Construct } from 'constructs';
import { BlueGreenLambda } from './lambda/BlueGreenLambda';
import { RestApi, LambdaIntegration, Cors } from '@root/aws-cdk-lib';
import { Table, AttributeType, BillingMode } from 'aws-cdk-lib/aws-dynamodb';
import { StringParameter } from '@root/aws-cdk-lib';
import { Sdlc } from '@root/types/global/ContextStrategy';
import { Duration } from 'aws-cdk-lib';

export interface MyStackProps extends StackProps {
readonly sdlc: Sdlc;
}

export class MyStack extends Stack {
constructor(scope: Construct, id: string, props: MyStackProps) {
super(scope, id, props);

const { sdlc } = props;

// Create DynamoDB table
const table = new Table(this, 'ItemsTable', {
tableName: `items-${sdlc}`,
partitionKey: {
name: 'id',
type: AttributeType.STRING,
},
billingMode: BillingMode.PAY_PER_REQUEST,
});

// Create SSM parameter for configuration
const configParameter = new StringParameter(this, 'ConfigParameter', {
parameterName: `/myapp/${sdlc}/config`,
stringValue: JSON.stringify({
environment: sdlc,
features: {
featureA: true,
featureB: sdlc !== 'prod',
},
}),
});

// Create Lambda with blue-green deployment
const api = new BlueGreenLambda(this, 'ApiFunction', {
sdlc,
functionName: 'MyAPI',
entry: './lib-src/lambda/myApi/src/index.ts',
timeout: Duration.seconds(30),
memorySize: 256,
environment: {
TABLE_NAME: table.tableName,
CONFIG_PARAMETER: configParameter.parameterName,
},
});

// Grant permissions
table.grantReadWriteData(api.lambda);
configParameter.grantRead(api.lambda);

// Create API Gateway
const restApi = new RestApi(this, 'MyApi', {
restApiName: `my-api-${sdlc}`,
description: `My API - ${sdlc} environment`,
defaultCorsPreflightOptions: {
allowOrigins: Cors.ALL_ORIGINS,
allowMethods: Cors.ALL_METHODS,
},
});

// Add routes
const apiFunction = api.getFunction();
const integration = new LambdaIntegration(apiFunction);

// GET /items
const items = restApi.root.addResource('items');
items.addMethod('GET', integration);

// POST /items
items.addMethod('POST', integration);

// GET /items/{id}
const item = items.addResource('{id}');
item.addMethod('GET', integration);

// PUT /items/{id}
item.addMethod('PUT', integration);

// DELETE /items/{id}
item.addMethod('DELETE', integration);
}
}

Step 2: Lambda Handler Implementation

// lib-src/lambda/myApi/src/index.ts
import {
APIGatewayProxyEvent,
APIGatewayProxyResult,
Context,
} from 'aws-lambda';
import {
DynamoDBClient,
GetItemCommand,
PutItemCommand,
DeleteItemCommand,
ScanCommand,
} from '@aws-sdk/client-dynamodb';
import { marshall, unmarshall } from '@aws-sdk/util-dynamodb';
import { randomUUID } from 'crypto';

// Initialize DynamoDB client (reused across invocations)
const dynamodb = new DynamoDBClient({});
const tableName = process.env.TABLE_NAME!;

interface Item {
id: string;
name: string;
description?: string;
createdAt: string;
updatedAt: string;
}

class Logger {
constructor(private requestId: string) {}

info(message: string, data?: Record<string, unknown>) {
console.log(JSON.stringify({
level: 'INFO',
message,
requestId: this.requestId,
timestamp: new Date().toISOString(),
...data,
}));
}

error(message: string, error?: unknown) {
console.error(JSON.stringify({
level: 'ERROR',
message,
requestId: this.requestId,
timestamp: new Date().toISOString(),
error: error instanceof Error ? {
message: error.message,
stack: error.stack,
} : error,
}));
}
}

export async function handler(
event: APIGatewayProxyEvent,
context: Context
): Promise<APIGatewayProxyResult> {
const logger = new Logger(context.awsRequestId);

logger.info('Request received', {
method: event.httpMethod,
path: event.path,
pathParameters: event.pathParameters,
});

try {
const route = `${event.httpMethod} ${event.resource}`;

switch (route) {
case 'GET /items':
return await listItems(logger);

case 'POST /items':
return await createItem(event, logger);

case 'GET /items/{id}':
return await getItem(event, logger);

case 'PUT /items/{id}':
return await updateItem(event, logger);

case 'DELETE /items/{id}':
return await deleteItem(event, logger);

default:
return response(404, { error: 'Not found' });
}
} catch (error) {
logger.error('Request failed', error);
return response(500, {
error: 'Internal server error',
message: error instanceof Error ? error.message : 'Unknown error',
});
}
}

async function listItems(logger: Logger): Promise<APIGatewayProxyResult> {
const command = new ScanCommand({ TableName: tableName });
const result = await dynamodb.send(command);

const items = result.Items?.map(item => unmarshall(item)) || [];

logger.info('Items listed', { count: items.length });
return response(200, { items });
}

async function getItem(
event: APIGatewayProxyEvent,
logger: Logger
): Promise<APIGatewayProxyResult> {
const id = event.pathParameters?.id;

if (!id) {
return response(400, { error: 'Missing id parameter' });
}

const command = new GetItemCommand({
TableName: tableName,
Key: marshall({ id }),
});

const result = await dynamodb.send(command);

if (!result.Item) {
return response(404, { error: 'Item not found' });
}

const item = unmarshall(result.Item);
logger.info('Item retrieved', { id });

return response(200, { item });
}

async function createItem(
event: APIGatewayProxyEvent,
logger: Logger
): Promise<APIGatewayProxyResult> {
if (!event.body) {
return response(400, { error: 'Missing request body' });
}

const { name, description } = JSON.parse(event.body);

if (!name) {
return response(400, { error: 'Missing required field: name' });
}

const now = new Date().toISOString();
const item: Item = {
id: randomUUID(),
name,
description,
createdAt: now,
updatedAt: now,
};

const command = new PutItemCommand({
TableName: tableName,
Item: marshall(item),
});

await dynamodb.send(command);

logger.info('Item created', { id: item.id });
return response(201, { item });
}

async function updateItem(
event: APIGatewayProxyEvent,
logger: Logger
): Promise<APIGatewayProxyResult> {
const id = event.pathParameters?.id;

if (!id) {
return response(400, { error: 'Missing id parameter' });
}

if (!event.body) {
return response(400, { error: 'Missing request body' });
}

// First check if item exists
const getCommand = new GetItemCommand({
TableName: tableName,
Key: marshall({ id }),
});

const existing = await dynamodb.send(getCommand);

if (!existing.Item) {
return response(404, { error: 'Item not found' });
}

const { name, description } = JSON.parse(event.body);
const existingItem = unmarshall(existing.Item) as Item;

const updatedItem: Item = {
...existingItem,
name: name || existingItem.name,
description: description !== undefined ? description : existingItem.description,
updatedAt: new Date().toISOString(),
};

const putCommand = new PutItemCommand({
TableName: tableName,
Item: marshall(updatedItem),
});

await dynamodb.send(putCommand);

logger.info('Item updated', { id });
return response(200, { item: updatedItem });
}

async function deleteItem(
event: APIGatewayProxyEvent,
logger: Logger
): Promise<APIGatewayProxyResult> {
const id = event.pathParameters?.id;

if (!id) {
return response(400, { error: 'Missing id parameter' });
}

const command = new DeleteItemCommand({
TableName: tableName,
Key: marshall({ id }),
});

await dynamodb.send(command);

logger.info('Item deleted', { id });
return response(204, {});
}

function response(statusCode: number, body: unknown): APIGatewayProxyResult {
return {
statusCode,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify(body),
};
}

Step 3: Package Configuration

// lib-src/lambda/myApi/package.json
{
"name": "my-api",
"version": "1.0.0",
"private": true,
"scripts": {
"test": "jest",
"test:watch": "jest --watch"
},
"dependencies": {
"@aws-sdk/client-dynamodb": "^3.710.0",
"@aws-sdk/util-dynamodb": "^3.710.0"
},
"devDependencies": {
"@types/aws-lambda": "^8.10.145",
"@types/jest": "^29.5.12",
"jest": "^29.7.0",
"ts-jest": "^29.2.5"
}
}

Step 4: Unit Tests

// lib-src/lambda/myApi/test/index.test.ts
import { handler } from '../src/index';
import { APIGatewayProxyEvent, Context } from 'aws-lambda';
import { mockClient } from 'aws-sdk-client-mock';
import { DynamoDBClient, ScanCommand, GetItemCommand } from '@aws-sdk/client-dynamodb';
import { marshall } from '@aws-sdk/util-dynamodb';

const dynamoMock = mockClient(DynamoDBClient);

function createEvent(overrides?: Partial<APIGatewayProxyEvent>): APIGatewayProxyEvent {
return {
body: null,
headers: {},
multiValueHeaders: {},
httpMethod: 'GET',
isBase64Encoded: false,
path: '/items',
pathParameters: null,
queryStringParameters: null,
multiValueQueryStringParameters: null,
stageVariables: null,
requestContext: {} as any,
resource: '/items',
...overrides,
};
}

function createContext(): Context {
return {
callbackWaitsForEmptyEventLoop: false,
functionName: 'test',
functionVersion: '1',
invokedFunctionArn: 'arn:aws:lambda:us-east-1:123456:function:test',
memoryLimitInMB: '128',
awsRequestId: 'test-id',
logGroupName: '/aws/lambda/test',
logStreamName: '2024/01/01/test',
getRemainingTimeInMillis: () => 30000,
done: () => {},
fail: () => {},
succeed: () => {},
};
}

describe('Lambda Handler', () => {
beforeEach(() => {
dynamoMock.reset();
process.env.TABLE_NAME = 'test-table';
});

describe('GET /items', () => {
it('should list all items', async () => {
const items = [
{ id: '1', name: 'Item 1' },
{ id: '2', name: 'Item 2' },
];

dynamoMock.on(ScanCommand).resolves({
Items: items.map(item => marshall(item)),
});

const event = createEvent();
const context = createContext();
const result = await handler(event, context);

expect(result.statusCode).toBe(200);
const body = JSON.parse(result.body);
expect(body.items).toHaveLength(2);
});
});

describe('GET /items/{id}', () => {
it('should return a single item', async () => {
const item = { id: '123', name: 'Test Item' };

dynamoMock.on(GetItemCommand).resolves({
Item: marshall(item),
});

const event = createEvent({
httpMethod: 'GET',
resource: '/items/{id}',
pathParameters: { id: '123' },
});
const context = createContext();
const result = await handler(event, context);

expect(result.statusCode).toBe(200);
const body = JSON.parse(result.body);
expect(body.item.id).toBe('123');
});

it('should return 404 for non-existent item', async () => {
dynamoMock.on(GetItemCommand).resolves({});

const event = createEvent({
httpMethod: 'GET',
resource: '/items/{id}',
pathParameters: { id: '999' },
});
const context = createContext();
const result = await handler(event, context);

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

Step5: Deploy

# Install dependencies
npm install

# Deploy to dev
npm run deploy:dev

# Deploy to staging
npm run deploy:staging

# Deploy to production
npm run deploy:prod

Step 6: Test the API

# Get API endpoint
API_URL=$(aws cloudformation describe-stacks \
--stack-name MyStack-dev \
--query 'Stacks[0].Outputs[?OutputKey==`ApiUrl`].OutputValue' \
--output text)

# Create an item
curl -X POST $API_URL/items \
-H "Content-Type: application/json" \
-d '{"name": "Test Item", "description": "A test item"}'

# List items
curl $API_URL/items

# Get item
curl $API_URL/items/{item-id}

# Update item
curl -X PUT $API_URL/items/{item-id} \
-H "Content-Type: application/json" \
-d '{"name": "Updated Item"}'

# Delete item
curl -X DELETE $API_URL/items/{item-id}