AWS Lambda TypeScript: Serverless Function Development
AWS Lambda with TypeScript enables building scalable, cost-effective serverless applications. This comprehensive guide covers function development, API Gateway integration, database operations, monitoring, and automated deployment using AWS CDK.
Why Choose AWS Lambda with TypeScript?
- Serverless Architecture: No server management, pay only for execution time
- Type Safety: Full TypeScript support with AWS SDK v3
- Auto Scaling: Automatic scaling based on demand
- Event-Driven: Integrates with 200+ AWS services
- Cost Effective: Pay per request and execution time
Step 1: Development Environment Setup
Set up a comprehensive AWS Lambda TypeScript development environment:
# Create project directory
mkdir aws-lambda-typescript-app
cd aws-lambda-typescript-app
# Initialize package.json
npm init -y
# Install AWS SDK v3 and Lambda dependencies
npm install @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
npm install @aws-sdk/client-s3 @aws-sdk/client-ses
npm install @aws-sdk/client-secrets-manager
npm install aws-lambda
# Install TypeScript and development tools
npm install -D typescript @types/node
npm install -D @types/aws-lambda
npm install -D esbuild serverless
npm install -D jest @types/jest ts-jest
npm install -D eslint @typescript-eslint/eslint-plugin
npm install -D prettier husky lint-staged
# Install AWS CDK for infrastructure
npm install -D aws-cdk-lib constructs
npm install -D @aws-cdk/aws-lambda-nodejs
# Install utility libraries
npm install zod middy
npm install uuid @types/uuid
npm install jsonwebtoken @types/jsonwebtoken
Configure TypeScript for AWS Lambda development:
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022"],
"module": "CommonJS",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"allowJs": true,
"outDir": "./dist",
"rootDir": "./src",
"removeComments": true,
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitThis": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@/types/*": ["src/types/*"],
"@/utils/*": ["src/utils/*"],
"@/services/*": ["src/services/*"],
"@/middleware/*": ["src/middleware/*"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "cdk.out"]
}
tsconfig.json
Step 2: Type Definitions and Interfaces
Create comprehensive type definitions for Lambda functions:
import {
APIGatewayProxyEvent,
APIGatewayProxyResult,
Context,
DynamoDBStreamEvent,
S3Event,
SQSEvent,
EventBridgeEvent,
} from "aws-lambda";
// Base API response structure
export interface ApiResponse<T = any> {
success: boolean;
data?: T;
message?: string;
errors?: Record<string, string[]>;
meta?: {
timestamp: string;
requestId: string;
version: string;
};
}
// Enhanced Lambda context with custom properties
export interface LambdaContext extends Context {
correlationId?: string;
userId?: string;
tenantId?: string;
}
// User types
export interface User {
id: string;
email: string;
username: string;
firstName: string;
lastName: string;
avatar?: string;
role: UserRole;
isActive: boolean;
emailVerified: boolean;
preferences: UserPreferences;
metadata: Record<string, any>;
createdAt: string;
updatedAt: string;
}
export type UserRole = "admin" | "user" | "moderator";
export interface UserPreferences {
theme: "light" | "dark" | "system";
language: string;
timezone: string;
notifications: {
email: boolean;
push: boolean;
sms: boolean;
};
}
// Authentication types
export interface AuthTokenPayload {
userId: string;
email: string;
role: UserRole;
iat: number;
exp: number;
}
export interface LoginRequest {
email: string;
password: string;
rememberMe?: boolean;
}
export interface RegisterRequest {
email: string;
username: string;
password: string;
firstName: string;
lastName: string;
}
// Product types
export interface Product {
id: string;
name: string;
description: string;
price: number;
currency: string;
category: string;
images: string[];
specifications: Record<string, string>;
inventory: {
quantity: number;
inStock: boolean;
reservedQuantity: number;
};
seo: {
title?: string;
description?: string;
keywords?: string[];
};
isActive: boolean;
createdAt: string;
updatedAt: string;
}
// Order types
export interface Order {
id: string;
userId: string;
items: OrderItem[];
status: OrderStatus;
totalAmount: number;
currency: string;
shippingAddress: Address;
billingAddress: Address;
paymentMethod: PaymentMethod;
shippingMethod: ShippingMethod;
notes?: string;
metadata: Record<string, any>;
createdAt: string;
updatedAt: string;
}
export interface OrderItem {
productId: string;
name: string;
price: number;
quantity: number;
totalPrice: number;
}
export type OrderStatus =
| "pending"
| "confirmed"
| "processing"
| "shipped"
| "delivered"
| "cancelled"
| "refunded";
export interface Address {
street: string;
city: string;
state: string;
zipCode: string;
country: string;
}
export interface PaymentMethod {
type: "credit_card" | "debit_card" | "paypal" | "stripe";
last4?: string;
brand?: string;
}
export interface ShippingMethod {
id: string;
name: string;
cost: number;
estimatedDays: number;
}
// DynamoDB types
export interface DynamoDBItem {
pk: string; // Partition key
sk: string; // Sort key
gsi1pk?: string; // Global secondary index 1 partition key
gsi1sk?: string; // Global secondary index 1 sort key
entityType: string;
ttl?: number;
createdAt: string;
updatedAt: string;
}
export interface UserDynamoDBItem extends DynamoDBItem {
entityType: "USER";
email: string;
username: string;
passwordHash: string;
profile: {
firstName: string;
lastName: string;
avatar?: string;
};
role: UserRole;
status: "active" | "inactive" | "suspended";
emailVerified: boolean;
preferences: UserPreferences;
metadata: Record<string, any>;
}
export interface ProductDynamoDBItem extends DynamoDBItem {
entityType: "PRODUCT";
name: string;
description: string;
price: number;
currency: string;
category: string;
images: string[];
specifications: Record<string, string>;
inventory: {
quantity: number;
inStock: boolean;
reservedQuantity: number;
};
seo: {
title?: string;
description?: string;
keywords?: string[];
};
isActive: boolean;
}
// Event types
export interface UserRegisteredEvent {
eventType: "USER_REGISTERED";
userId: string;
email: string;
timestamp: string;
metadata: Record<string, any>;
}
export interface OrderCreatedEvent {
eventType: "ORDER_CREATED";
orderId: string;
userId: string;
totalAmount: number;
currency: string;
items: OrderItem[];
timestamp: string;
metadata: Record<string, any>;
}
export interface ProductUpdatedEvent {
eventType: "PRODUCT_UPDATED";
productId: string;
changes: Partial<Product>;
timestamp: string;
metadata: Record<string, any>;
}
// Lambda function types
export type LambdaHandler<TEvent = any, TResult = any> = (
event: TEvent,
context: LambdaContext
) => Promise<TResult>;
export type ApiHandler = LambdaHandler<
APIGatewayProxyEvent,
APIGatewayProxyResult
>;
export type DynamoDBHandler = LambdaHandler<DynamoDBStreamEvent, void>;
export type S3Handler = LambdaHandler<S3Event, void>;
export type SQSHandler = LambdaHandler<SQSEvent, void>;
export type EventBridgeHandler<T = any> = LambdaHandler<
EventBridgeEvent<string, T>,
void
>;
// Utility types
export type CreateRequest<T> = Omit<T, "id" | "createdAt" | "updatedAt">;
export type UpdateRequest<T> = Partial<
Omit<T, "id" | "createdAt" | "updatedAt">
>;
export type PaginationParams = {
limit?: number;
nextToken?: string;
};
export type PaginatedResponse<T> = {
items: T[];
nextToken?: string;
count: number;
};
// Error types
export interface LambdaError extends Error {
statusCode?: number;
code?: string;
details?: any;
}
// Configuration types
export interface LambdaConfig {
region: string;
stage: string;
tableName: string;
bucketName: string;
jwtSecret: string;
corsOrigin: string;
logLevel: "debug" | "info" | "warn" | "error";
}
// Middleware types
export interface MiddlewareContext {
event: APIGatewayProxyEvent;
context: LambdaContext;
config: LambdaConfig;
user?: User;
correlationId: string;
}
src/types/index.ts
Step 3: Core Utilities and Services
Create comprehensive utility functions and services:
import { APIGatewayProxyResult } from "aws-lambda";
import { ApiResponse } from "@/types";
export class ResponseBuilder {
static success<T>(
data?: T,
message?: string,
statusCode = 200
): APIGatewayProxyResult {
const response: ApiResponse<T> = {
success: true,
data,
message,
meta: {
timestamp: new Date().toISOString(),
requestId: "",
version: process.env.API_VERSION || "1.0.0",
},
};
return {
statusCode,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": process.env.CORS_ORIGIN || "*",
"Access-Control-Allow-Headers":
"Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token",
"Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS",
},
body: JSON.stringify(response),
};
}
static error(
message: string,
statusCode = 400,
errors?: Record<string, string[]>
): APIGatewayProxyResult {
const response: ApiResponse = {
success: false,
message,
errors,
meta: {
timestamp: new Date().toISOString(),
requestId: "",
version: process.env.API_VERSION || "1.0.0",
},
};
return {
statusCode,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": process.env.CORS_ORIGIN || "*",
"Access-Control-Allow-Headers":
"Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token",
"Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS",
},
body: JSON.stringify(response),
};
}
static notFound(message = "Resource not found"): APIGatewayProxyResult {
return this.error(message, 404);
}
static unauthorized(message = "Unauthorized"): APIGatewayProxyResult {
return this.error(message, 401);
}
static forbidden(message = "Forbidden"): APIGatewayProxyResult {
return this.error(message, 403);
}
static validation(errors: Record<string, string[]>): APIGatewayProxyResult {
return this.error("Validation failed", 422, errors);
}
static internal(message = "Internal server error"): APIGatewayProxyResult {
return this.error(message, 500);
}
static created<T>(
data: T,
message = "Resource created successfully"
): APIGatewayProxyResult {
return this.success(data, message, 201);
}
static noContent(): APIGatewayProxyResult {
return {
statusCode: 204,
headers: {
"Access-Control-Allow-Origin": process.env.CORS_ORIGIN || "*",
"Access-Control-Allow-Headers":
"Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token",
"Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS",
},
body: "",
};
}
}
export const createResponse = ResponseBuilder;
src/utils/response.ts
import { z, ZodSchema } from "zod";
import { APIGatewayProxyEvent } from "aws-lambda";
export class ValidationError extends Error {
constructor(
public errors: Record<string, string[]>,
message = "Validation failed"
) {
super(message);
this.name = "ValidationError";
}
}
export class Validator {
static validateBody<T>(event: APIGatewayProxyEvent, schema: ZodSchema<T>): T {
if (!event.body) {
throw new ValidationError({ body: ["Request body is required"] });
}
try {
const parsedBody = JSON.parse(event.body);
return schema.parse(parsedBody);
} catch (error) {
if (error instanceof z.ZodError) {
const errors: Record<string, string[]> = {};
error.errors.forEach(err => {
const path = err.path.join(".");
if (!errors[path]) {
errors[path] = [];
}
errors[path].push(err.message);
});
throw new ValidationError(errors);
}
throw new ValidationError({ body: ["Invalid JSON format"] });
}
}
static validatePathParameters<T>(
event: APIGatewayProxyEvent,
schema: ZodSchema<T>
): T {
if (!event.pathParameters) {
throw new ValidationError({ path: ["Path parameters are required"] });
}
try {
return schema.parse(event.pathParameters);
} catch (error) {
if (error instanceof z.ZodError) {
const errors: Record<string, string[]> = {};
error.errors.forEach(err => {
const path = `path.${err.path.join(".")}`;
if (!errors[path]) {
errors[path] = [];
}
errors[path].push(err.message);
});
throw new ValidationError(errors);
}
throw new ValidationError({ path: ["Invalid path parameters"] });
}
}
static validateQueryParameters<T>(
event: APIGatewayProxyEvent,
schema: ZodSchema<T>
): T {
const queryParams = event.queryStringParameters || {};
try {
return schema.parse(queryParams);
} catch (error) {
if (error instanceof z.ZodError) {
const errors: Record<string, string[]> = {};
error.errors.forEach(err => {
const path = `query.${err.path.join(".")}`;
if (!errors[path]) {
errors[path] = [];
}
errors[path].push(err.message);
});
throw new ValidationError(errors);
}
throw new ValidationError({ query: ["Invalid query parameters"] });
}
}
static validateHeaders<T>(
event: APIGatewayProxyEvent,
schema: ZodSchema<T>
): T {
const headers = event.headers || {};
try {
return schema.parse(headers);
} catch (error) {
if (error instanceof z.ZodError) {
const errors: Record<string, string[]> = {};
error.errors.forEach(err => {
const path = `headers.${err.path.join(".")}`;
if (!errors[path]) {
errors[path] = [];
}
errors[path].push(err.message);
});
throw new ValidationError(errors);
}
throw new ValidationError({ headers: ["Invalid headers"] });
}
}
}
// Common validation schemas
export const CommonSchemas = {
id: z.string().uuid("Invalid ID format"),
email: z.string().email("Invalid email format"),
pagination: z.object({
limit: z
.string()
.transform(val => parseInt(val))
.pipe(z.number().min(1).max(100))
.optional(),
nextToken: z.string().optional(),
}),
pathId: z.object({
id: z.string().uuid("Invalid ID format"),
}),
authHeaders: z.object({
authorization: z.string().min(1, "Authorization header is required"),
}),
};
src/utils/validation.ts
import {
DynamoDBClient,
GetItemCommand,
PutItemCommand,
UpdateItemCommand,
DeleteItemCommand,
QueryCommand,
ScanCommand,
TransactWriteItemsCommand,
} from "@aws-sdk/client-dynamodb";
import {
DynamoDBDocumentClient,
GetCommand,
PutCommand,
UpdateCommand,
DeleteCommand,
QueryCommand as DocQueryCommand,
ScanCommand as DocScanCommand,
TransactWriteCommand,
} from "@aws-sdk/lib-dynamodb";
import { DynamoDBItem, PaginatedResponse } from "@/types";
export class DynamoDBService {
private client: DynamoDBDocumentClient;
private tableName: string;
constructor(tableName: string, region = "us-east-1") {
const dynamoClient = new DynamoDBClient({ region });
this.client = DynamoDBDocumentClient.from(dynamoClient);
this.tableName = tableName;
}
// Get single item by primary key
async getItem<T extends DynamoDBItem>(
pk: string,
sk: string
): Promise<T | null> {
try {
const command = new GetCommand({
TableName: this.tableName,
Key: { pk, sk },
});
const response = await this.client.send(command);
return response.Item as T | null;
} catch (error) {
console.error("DynamoDB getItem error:", error);
throw new Error("Failed to get item from database");
}
}
// Put single item
async putItem<T extends DynamoDBItem>(item: T): Promise<void> {
try {
const now = new Date().toISOString();
const itemWithTimestamps = {
...item,
createdAt: item.createdAt || now,
updatedAt: now,
};
const command = new PutCommand({
TableName: this.tableName,
Item: itemWithTimestamps,
});
await this.client.send(command);
} catch (error) {
console.error("DynamoDB putItem error:", error);
throw new Error("Failed to save item to database");
}
}
// Update item
async updateItem<T extends DynamoDBItem>(
pk: string,
sk: string,
updates: Partial<Omit<T, "pk" | "sk" | "createdAt" | "updatedAt">>
): Promise<T | null> {
try {
const updateExpressions: string[] = [];
const expressionAttributeNames: Record<string, string> = {};
const expressionAttributeValues: Record<string, any> = {};
// Add updatedAt timestamp
updates.updatedAt = new Date().toISOString() as any;
Object.entries(updates).forEach(([key, value]) => {
if (value !== undefined) {
updateExpressions.push(`#${key} = :${key}`);
expressionAttributeNames[`#${key}`] = key;
expressionAttributeValues[`:${key}`] = value;
}
});
if (updateExpressions.length === 0) {
throw new Error("No valid updates provided");
}
const command = new UpdateCommand({
TableName: this.tableName,
Key: { pk, sk },
UpdateExpression: `SET ${updateExpressions.join(", ")}`,
ExpressionAttributeNames: expressionAttributeNames,
ExpressionAttributeValues: expressionAttributeValues,
ReturnValues: "ALL_NEW",
});
const response = await this.client.send(command);
return response.Attributes as T | null;
} catch (error) {
console.error("DynamoDB updateItem error:", error);
throw new Error("Failed to update item in database");
}
}
// Delete item
async deleteItem(pk: string, sk: string): Promise<void> {
try {
const command = new DeleteCommand({
TableName: this.tableName,
Key: { pk, sk },
});
await this.client.send(command);
} catch (error) {
console.error("DynamoDB deleteItem error:", error);
throw new Error("Failed to delete item from database");
}
}
// Query items by partition key
async queryItems<T extends DynamoDBItem>(
pk: string,
options: {
sk?: string;
skCondition?: "begins_with" | "between";
skValue2?: string;
limit?: number;
nextToken?: string;
sortDescending?: boolean;
indexName?: string;
} = {}
): Promise<PaginatedResponse<T>> {
try {
let keyConditionExpression = "pk = :pk";
const expressionAttributeValues: Record<string, any> = { ":pk": pk };
if (options.sk) {
if (options.skCondition === "begins_with") {
keyConditionExpression += " AND begins_with(sk, :sk)";
expressionAttributeValues[":sk"] = options.sk;
} else if (options.skCondition === "between" && options.skValue2) {
keyConditionExpression += " AND sk BETWEEN :sk1 AND :sk2";
expressionAttributeValues[":sk1"] = options.sk;
expressionAttributeValues[":sk2"] = options.skValue2;
} else {
keyConditionExpression += " AND sk = :sk";
expressionAttributeValues[":sk"] = options.sk;
}
}
const command = new DocQueryCommand({
TableName: this.tableName,
IndexName: options.indexName,
KeyConditionExpression: keyConditionExpression,
ExpressionAttributeValues: expressionAttributeValues,
Limit: options.limit || 50,
ExclusiveStartKey: options.nextToken
? JSON.parse(options.nextToken)
: undefined,
ScanIndexForward: !options.sortDescending,
});
const response = await this.client.send(command);
return {
items: response.Items as T[],
nextToken: response.LastEvaluatedKey
? JSON.stringify(response.LastEvaluatedKey)
: undefined,
count: response.Count || 0,
};
} catch (error) {
console.error("DynamoDB queryItems error:", error);
throw new Error("Failed to query items from database");
}
}
// Scan table with filters
async scanItems<T extends DynamoDBItem>(
options: {
filterExpression?: string;
expressionAttributeNames?: Record<string, string>;
expressionAttributeValues?: Record<string, any>;
limit?: number;
nextToken?: string;
indexName?: string;
} = {}
): Promise<PaginatedResponse<T>> {
try {
const command = new DocScanCommand({
TableName: this.tableName,
IndexName: options.indexName,
FilterExpression: options.filterExpression,
ExpressionAttributeNames: options.expressionAttributeNames,
ExpressionAttributeValues: options.expressionAttributeValues,
Limit: options.limit || 50,
ExclusiveStartKey: options.nextToken
? JSON.parse(options.nextToken)
: undefined,
});
const response = await this.client.send(command);
return {
items: response.Items as T[],
nextToken: response.LastEvaluatedKey
? JSON.stringify(response.LastEvaluatedKey)
: undefined,
count: response.Count || 0,
};
} catch (error) {
console.error("DynamoDB scanItems error:", error);
throw new Error("Failed to scan items from database");
}
}
// Transaction write
async transactWrite(
operations: Array<{
operation: "put" | "update" | "delete";
item?: DynamoDBItem;
pk?: string;
sk?: string;
updates?: Record<string, any>;
}>
): Promise<void> {
try {
const transactItems = operations.map(op => {
const now = new Date().toISOString();
switch (op.operation) {
case "put":
if (!op.item) throw new Error("Item is required for put operation");
return {
Put: {
TableName: this.tableName,
Item: {
...op.item,
createdAt: op.item.createdAt || now,
updatedAt: now,
},
},
};
case "update":
if (!op.pk || !op.sk || !op.updates) {
throw new Error(
"pk, sk, and updates are required for update operation"
);
}
const updateExpressions: string[] = [];
const expressionAttributeNames: Record<string, string> = {};
const expressionAttributeValues: Record<string, any> = {};
op.updates.updatedAt = now;
Object.entries(op.updates).forEach(([key, value]) => {
if (value !== undefined) {
updateExpressions.push(`#${key} = :${key}`);
expressionAttributeNames[`#${key}`] = key;
expressionAttributeValues[`:${key}`] = value;
}
});
return {
Update: {
TableName: this.tableName,
Key: { pk: op.pk, sk: op.sk },
UpdateExpression: `SET ${updateExpressions.join(", ")}`,
ExpressionAttributeNames: expressionAttributeNames,
ExpressionAttributeValues: expressionAttributeValues,
},
};
case "delete":
if (!op.pk || !op.sk) {
throw new Error("pk and sk are required for delete operation");
}
return {
Delete: {
TableName: this.tableName,
Key: { pk: op.pk, sk: op.sk },
},
};
default:
throw new Error(`Unknown operation: ${op.operation}`);
}
});
const command = new TransactWriteCommand({
TransactItems: transactItems,
});
await this.client.send(command);
} catch (error) {
console.error("DynamoDB transactWrite error:", error);
throw new Error("Failed to execute transaction");
}
}
}
src/services/dynamodbService.ts
Step 4: Lambda Function Handlers
Create comprehensive Lambda function handlers:
import { APIGatewayProxyEvent, Context } from "aws-lambda";
import { z } from "zod";
import { v4 as uuidv4 } from "uuid";
import bcrypt from "bcryptjs";
import jwt from "jsonwebtoken";
import {
ApiHandler,
User,
UserDynamoDBItem,
CreateRequest,
UpdateRequest,
} from "@/types";
import { createResponse } from "@/utils/response";
import { Validator, ValidationError, CommonSchemas } from "@/utils/validation";
import { DynamoDBService } from "@/services/dynamodbService";
import { withMiddleware } from "@/middleware";
// Initialize DynamoDB service
const dynamoService = new DynamoDBService(process.env.TABLE_NAME!);
// Validation schemas
const CreateUserSchema = z.object({
email: z.string().email(),
username: z.string().min(3).max(50),
password: z.string().min(8),
firstName: z.string().min(1).max(100),
lastName: z.string().min(1).max(100),
});
const UpdateUserSchema = z
.object({
username: z.string().min(3).max(50).optional(),
firstName: z.string().min(1).max(100).optional(),
lastName: z.string().min(1).max(100).optional(),
avatar: z.string().url().optional(),
})
.strict();
const LoginSchema = z.object({
email: z.string().email(),
password: z.string().min(1),
});
// Helper functions
const hashPassword = async (password: string): Promise<string> => {
return bcrypt.hash(password, 12);
};
const verifyPassword = async (
password: string,
hash: string
): Promise<boolean> => {
return bcrypt.compare(password, hash);
};
const generateToken = (user: User): string => {
return jwt.sign(
{
userId: user.id,
email: user.email,
role: user.role,
},
process.env.JWT_SECRET!,
{ expiresIn: "24h" }
);
};
const mapDynamoToUser = (item: UserDynamoDBItem): User => ({
id: item.pk.replace("USER#", ""),
email: item.email,
username: item.username,
firstName: item.profile.firstName,
lastName: item.profile.lastName,
avatar: item.profile.avatar,
role: item.role,
isActive: item.status === "active",
emailVerified: item.emailVerified,
preferences: item.preferences,
metadata: item.metadata,
createdAt: item.createdAt,
updatedAt: item.updatedAt,
});
// Create user handler
export const createUser: ApiHandler = async (
event: APIGatewayProxyEvent,
context: Context
) => {
try {
// Validate request body
const userData = Validator.validateBody(event, CreateUserSchema);
// Check if user already exists
const existingUser = await dynamoService.queryItems<UserDynamoDBItem>(
"USER",
{
sk: `EMAIL#${userData.email.toLowerCase()}`,
indexName: "GSI1",
}
);
if (existingUser.items.length > 0) {
return createResponse.error("User with this email already exists", 409);
}
// Check if username is taken
const existingUsername = await dynamoService.queryItems<UserDynamoDBItem>(
"USER",
{
sk: `USERNAME#${userData.username.toLowerCase()}`,
indexName: "GSI1",
}
);
if (existingUsername.items.length > 0) {
return createResponse.error("Username is already taken", 409);
}
// Hash password
const passwordHash = await hashPassword(userData.password);
// Create user ID
const userId = uuidv4();
// Create DynamoDB item
const userItem: UserDynamoDBItem = {
pk: `USER#${userId}`,
sk: `USER#${userId}`,
gsi1pk: "USER",
gsi1sk: `EMAIL#${userData.email.toLowerCase()}`,
entityType: "USER",
email: userData.email.toLowerCase(),
username: userData.username.toLowerCase(),
passwordHash,
profile: {
firstName: userData.firstName,
lastName: userData.lastName,
},
role: "user",
status: "active",
emailVerified: false,
preferences: {
theme: "system",
language: "en",
timezone: "UTC",
notifications: {
email: true,
push: true,
sms: false,
},
},
metadata: {},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
// Create username index item
const usernameItem: UserDynamoDBItem = {
...userItem,
pk: `USERNAME#${userData.username.toLowerCase()}`,
sk: `USER#${userId}`,
gsi1pk: "USER",
gsi1sk: `USERNAME#${userData.username.toLowerCase()}`,
};
// Save both items in transaction
await dynamoService.transactWrite([
{ operation: "put", item: userItem },
{ operation: "put", item: usernameItem },
]);
// Convert to user object and remove sensitive data
const user = mapDynamoToUser(userItem);
const { metadata, ...safeUser } = user;
return createResponse.created(safeUser, "User created successfully");
} catch (error) {
console.error("Create user error:", error);
if (error instanceof ValidationError) {
return createResponse.validation(error.errors);
}
return createResponse.internal("Failed to create user");
}
};
// Login handler
export const login: ApiHandler = async (
event: APIGatewayProxyEvent,
context: Context
) => {
try {
// Validate request body
const credentials = Validator.validateBody(event, LoginSchema);
// Find user by email
const userResult = await dynamoService.queryItems<UserDynamoDBItem>(
"USER",
{
sk: `EMAIL#${credentials.email.toLowerCase()}`,
indexName: "GSI1",
}
);
if (userResult.items.length === 0) {
return createResponse.unauthorized("Invalid credentials");
}
const userItem = userResult.items[0];
// Verify password
const isPasswordValid = await verifyPassword(
credentials.password,
userItem.passwordHash
);
if (!isPasswordValid) {
return createResponse.unauthorized("Invalid credentials");
}
// Check if user is active
if (userItem.status !== "active") {
return createResponse.forbidden("Account is not active");
}
// Generate JWT token
const user = mapDynamoToUser(userItem);
const token = generateToken(user);
// Remove sensitive data
const { metadata, ...safeUser } = user;
return createResponse.success(
{ user: safeUser, token },
"Login successful"
);
} catch (error) {
console.error("Login error:", error);
if (error instanceof ValidationError) {
return createResponse.validation(error.errors);
}
return createResponse.internal("Login failed");
}
};
// Get user by ID handler
export const getUserById: ApiHandler = withMiddleware(["auth"])(async (
event: APIGatewayProxyEvent,
context: Context
) => {
try {
// Validate path parameters
const { id } = Validator.validatePathParameters(
event,
CommonSchemas.pathId
);
// Get user from database
const userItem = await dynamoService.getItem<UserDynamoDBItem>(
`USER#${id}`,
`USER#${id}`
);
if (!userItem) {
return createResponse.notFound("User not found");
}
// Convert to user object
const user = mapDynamoToUser(userItem);
const { metadata, ...safeUser } = user;
return createResponse.success(safeUser);
} catch (error) {
console.error("Get user error:", error);
if (error instanceof ValidationError) {
return createResponse.validation(error.errors);
}
return createResponse.internal("Failed to get user");
}
});
// Update user handler
export const updateUser: ApiHandler = withMiddleware(["auth"])(async (
event: APIGatewayProxyEvent,
context: Context
) => {
try {
// Validate path parameters and body
const { id } = Validator.validatePathParameters(
event,
CommonSchemas.pathId
);
const updates = Validator.validateBody(event, UpdateUserSchema);
// Check if user exists
const existingItem = await dynamoService.getItem<UserDynamoDBItem>(
`USER#${id}`,
`USER#${id}`
);
if (!existingItem) {
return createResponse.notFound("User not found");
}
// Build update object
const updateData: Partial<UserDynamoDBItem> = {};
if (updates.username) {
// Check if new username is available
const existingUsername = await dynamoService.queryItems<UserDynamoDBItem>(
"USER",
{
sk: `USERNAME#${updates.username.toLowerCase()}`,
indexName: "GSI1",
}
);
if (
existingUsername.items.length > 0 &&
existingUsername.items[0].pk !== `USER#${id}`
) {
return createResponse.error("Username is already taken", 409);
}
updateData.username = updates.username.toLowerCase();
}
if (updates.firstName || updates.lastName || updates.avatar) {
updateData.profile = {
...existingItem.profile,
...(updates.firstName && { firstName: updates.firstName }),
...(updates.lastName && { lastName: updates.lastName }),
...(updates.avatar && { avatar: updates.avatar }),
};
}
// Update user
const updatedItem = await dynamoService.updateItem<UserDynamoDBItem>(
`USER#${id}`,
`USER#${id}`,
updateData
);
if (!updatedItem) {
return createResponse.internal("Failed to update user");
}
// Convert to user object
const user = mapDynamoToUser(updatedItem);
const { metadata, ...safeUser } = user;
return createResponse.success(safeUser, "User updated successfully");
} catch (error) {
console.error("Update user error:", error);
if (error instanceof ValidationError) {
return createResponse.validation(error.errors);
}
return createResponse.internal("Failed to update user");
}
});
// Delete user handler
export const deleteUser: ApiHandler = withMiddleware(["auth", "admin"])(async (
event: APIGatewayProxyEvent,
context: Context
) => {
try {
// Validate path parameters
const { id } = Validator.validatePathParameters(
event,
CommonSchemas.pathId
);
// Check if user exists
const existingItem = await dynamoService.getItem<UserDynamoDBItem>(
`USER#${id}`,
`USER#${id}`
);
if (!existingItem) {
return createResponse.notFound("User not found");
}
// Delete user and username index
await dynamoService.transactWrite([
{ operation: "delete", pk: `USER#${id}`, sk: `USER#${id}` },
{
operation: "delete",
pk: `USERNAME#${existingItem.username}`,
sk: `USER#${id}`,
},
]);
return createResponse.noContent();
} catch (error) {
console.error("Delete user error:", error);
if (error instanceof ValidationError) {
return createResponse.validation(error.errors);
}
return createResponse.internal("Failed to delete user");
}
});
// List users handler
export const listUsers: ApiHandler = withMiddleware(["auth", "admin"])(async (
event: APIGatewayProxyEvent,
context: Context
) => {
try {
// Validate query parameters
const queryParams = Validator.validateQueryParameters(
event,
CommonSchemas.pagination
);
// Query users
const result = await dynamoService.queryItems<UserDynamoDBItem>("USER", {
limit: queryParams.limit,
nextToken: queryParams.nextToken,
indexName: "GSI1",
});
// Convert to user objects
const users = result.items.map(item => {
const user = mapDynamoToUser(item);
const { metadata, ...safeUser } = user;
return safeUser;
});
return createResponse.success({
users,
nextToken: result.nextToken,
count: result.count,
});
} catch (error) {
console.error("List users error:", error);
if (error instanceof ValidationError) {
return createResponse.validation(error.errors);
}
return createResponse.internal("Failed to list users");
}
});
src/handlers/users.ts
Step 5: Middleware System
Create a comprehensive middleware system:
import {
APIGatewayProxyEvent,
Context,
APIGatewayProxyResult,
} from "aws-lambda";
import jwt from "jsonwebtoken";
import { ApiHandler, AuthTokenPayload, User, LambdaContext } from "@/types";
import { createResponse } from "@/utils/response";
import { DynamoDBService } from "@/services/dynamodbService";
// Middleware types
type MiddlewareFunction = (
event: APIGatewayProxyEvent,
context: LambdaContext,
next: () => Promise<APIGatewayProxyResult>
) => Promise<APIGatewayProxyResult>;
type MiddlewareOptions = Array<
"cors" | "auth" | "admin" | "logging" | "validation" | "rateLimit"
>;
// Enhanced context with middleware data
interface EnhancedContext extends LambdaContext {
user?: User;
correlationId: string;
}
// CORS middleware
const corsMiddleware: MiddlewareFunction = async (event, context, next) => {
// Handle preflight requests
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 204,
headers: {
"Access-Control-Allow-Origin": process.env.CORS_ORIGIN || "*",
"Access-Control-Allow-Headers":
"Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Correlation-Id",
"Access-Control-Allow-Methods": "GET,POST,PUT,DELETE,OPTIONS",
"Access-Control-Max-Age": "86400",
},
body: "",
};
}
// Continue to next middleware
const result = await next();
// Add CORS headers to response
if (result.headers) {
result.headers["Access-Control-Allow-Origin"] =
process.env.CORS_ORIGIN || "*";
result.headers["Access-Control-Allow-Headers"] =
"Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Correlation-Id";
result.headers["Access-Control-Allow-Methods"] =
"GET,POST,PUT,DELETE,OPTIONS";
}
return result;
};
// Authentication middleware
const authMiddleware: MiddlewareFunction = async (event, context, next) => {
try {
const authHeader =
event.headers.authorization || event.headers.Authorization;
if (!authHeader) {
return createResponse.unauthorized("Authorization header is required");
}
const token = authHeader.replace("Bearer ", "");
if (!token) {
return createResponse.unauthorized("Invalid authorization format");
}
// Verify JWT token
let decoded: AuthTokenPayload;
try {
decoded = jwt.verify(token, process.env.JWT_SECRET!) as AuthTokenPayload;
} catch (error) {
return createResponse.unauthorized("Invalid or expired token");
}
// Get user from database
const dynamoService = new DynamoDBService(process.env.TABLE_NAME!);
const userItem = await dynamoService.getItem(
`USER#${decoded.userId}`,
`USER#${decoded.userId}`
);
if (!userItem) {
return createResponse.unauthorized("User not found");
}
// Add user to context
const enhancedContext = context as EnhancedContext;
enhancedContext.user = {
id: decoded.userId,
email: decoded.email,
role: decoded.role,
} as User;
enhancedContext.userId = decoded.userId;
return await next();
} catch (error) {
console.error("Auth middleware error:", error);
return createResponse.unauthorized("Authentication failed");
}
};
// Admin authorization middleware
const adminMiddleware: MiddlewareFunction = async (event, context, next) => {
const enhancedContext = context as EnhancedContext;
if (!enhancedContext.user) {
return createResponse.unauthorized("Authentication required");
}
if (enhancedContext.user.role !== "admin") {
return createResponse.forbidden("Admin access required");
}
return await next();
};
// Logging middleware
const loggingMiddleware: MiddlewareFunction = async (event, context, next) => {
const startTime = Date.now();
const correlationId =
event.headers["x-correlation-id"] ||
event.headers["X-Correlation-Id"] ||
context.awsRequestId;
// Add correlation ID to context
const enhancedContext = context as EnhancedContext;
enhancedContext.correlationId = correlationId;
// Log request
console.log("Request started:", {
correlationId,
method: event.httpMethod,
path: event.path,
queryParams: event.queryStringParameters,
userAgent: event.headers["user-agent"],
sourceIp: event.requestContext.identity.sourceIp,
userId: enhancedContext.userId,
});
try {
const result = await next();
// Log successful response
const duration = Date.now() - startTime;
console.log("Request completed:", {
correlationId,
statusCode: result.statusCode,
duration: `${duration}ms`,
});
// Add correlation ID to response headers
if (result.headers) {
result.headers["X-Correlation-Id"] = correlationId;
}
return result;
} catch (error) {
// Log error
const duration = Date.now() - startTime;
console.error("Request failed:", {
correlationId,
error: error instanceof Error ? error.message : "Unknown error",
duration: `${duration}ms`,
});
throw error;
}
};
// Rate limiting middleware (simple implementation)
const rateLimitMiddleware: MiddlewareFunction = async (
event,
context,
next
) => {
// This is a simplified rate limiting implementation
// In production, you would use DynamoDB or Redis to track rates
const clientIp = event.requestContext.identity.sourceIp;
const key = `rate_limit:${clientIp}`;
// For demo purposes, we'll allow all requests
// In production, implement proper rate limiting logic
return await next();
};
// Validation middleware (generic error handling)
const validationMiddleware: MiddlewareFunction = async (
event,
context,
next
) => {
try {
return await next();
} catch (error) {
console.error("Validation middleware error:", error);
if (error instanceof Error && error.name === "ValidationError") {
return createResponse.validation({
validation: [error.message],
});
}
throw error;
}
};
// Middleware registry
const middlewareRegistry: Record<string, MiddlewareFunction> = {
cors: corsMiddleware,
auth: authMiddleware,
admin: adminMiddleware,
logging: loggingMiddleware,
validation: validationMiddleware,
rateLimit: rateLimitMiddleware,
};
// Main middleware composer
export function withMiddleware(
options: MiddlewareOptions = ["cors", "logging"]
) {
return (handler: ApiHandler): ApiHandler => {
return async (event: APIGatewayProxyEvent, context: Context) => {
// Get middleware functions
const middlewares = options
.map(name => middlewareRegistry[name])
.filter(Boolean);
// Create middleware chain
let index = 0;
const next = async (): Promise<APIGatewayProxyResult> => {
if (index >= middlewares.length) {
// All middleware processed, call the actual handler
return await handler(event, context as LambdaContext);
}
const middleware = middlewares[index++];
return await middleware(event, context as LambdaContext, next);
};
try {
return await next();
} catch (error) {
console.error("Middleware chain error:", error);
// Return generic error response
return createResponse.internal(
error instanceof Error ? error.message : "Internal server error"
);
}
};
};
}
// Shorthand for common middleware combinations
export const withAuth = withMiddleware(["cors", "logging", "auth"]);
export const withAdmin = withMiddleware(["cors", "logging", "auth", "admin"]);
export const withPublic = withMiddleware(["cors", "logging"]);
src/middleware/index.ts
Step 6: AWS CDK Infrastructure
Create infrastructure as code using AWS CDK:
import * as cdk from "aws-cdk-lib";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as apigateway from "aws-cdk-lib/aws-apigateway";
import * as dynamodb from "aws-cdk-lib/aws-dynamodb";
import * as s3 from "aws-cdk-lib/aws-s3";
import * as iam from "aws-cdk-lib/aws-iam";
import * as logs from "aws-cdk-lib/aws-logs";
import * as events from "aws-cdk-lib/aws-events";
import * as targets from "aws-cdk-lib/aws-events-targets";
import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs";
import { Construct } from "constructs";
export class ServerlessAppStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Environment variables
const stage = this.node.tryGetContext("stage") || "dev";
const region = cdk.Stack.of(this).region;
// DynamoDB Table
const table = new dynamodb.Table(this, "AppTable", {
tableName: `serverless-app-${stage}`,
partitionKey: {
name: "pk",
type: dynamodb.AttributeType.STRING,
},
sortKey: {
name: "sk",
type: dynamodb.AttributeType.STRING,
},
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
encryption: dynamodb.TableEncryption.AWS_MANAGED,
pointInTimeRecovery: stage === "prod",
removalPolicy:
stage === "prod" ? cdk.RemovalPolicy.RETAIN : cdk.RemovalPolicy.DESTROY,
stream: dynamodb.StreamViewType.NEW_AND_OLD_IMAGES,
});
// Add Global Secondary Indexes
table.addGlobalSecondaryIndex({
indexName: "GSI1",
partitionKey: {
name: "gsi1pk",
type: dynamodb.AttributeType.STRING,
},
sortKey: {
name: "gsi1sk",
type: dynamodb.AttributeType.STRING,
},
});
// S3 Bucket for file storage
const bucket = new s3.Bucket(this, "AppBucket", {
bucketName: `serverless-app-${stage}-${region}`,
encryption: s3.BucketEncryption.S3_MANAGED,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
enforceSSL: true,
removalPolicy:
stage === "prod" ? cdk.RemovalPolicy.RETAIN : cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: stage !== "prod",
});
// Lambda execution role
const lambdaRole = new iam.Role(this, "LambdaExecutionRole", {
assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName(
"service-role/AWSLambdaBasicExecutionRole"
),
],
inlinePolicies: {
DynamoDBAccess: new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:UpdateItem",
"dynamodb:DeleteItem",
"dynamodb:Query",
"dynamodb:Scan",
"dynamodb:BatchGetItem",
"dynamodb:BatchWriteItem",
"dynamodb:TransactWriteItems",
"dynamodb:TransactGetItems",
],
resources: [table.tableArn, `${table.tableArn}/*`],
}),
],
}),
S3Access: new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
resources: [`${bucket.bucketArn}/*`],
}),
],
}),
},
});
// Common Lambda function configuration
const lambdaDefaults = {
runtime: lambda.Runtime.NODEJS_18_X,
architecture: lambda.Architecture.ARM_64,
timeout: cdk.Duration.seconds(30),
memorySize: 512,
role: lambdaRole,
environment: {
NODE_ENV: stage,
STAGE: stage,
REGION: region,
TABLE_NAME: table.tableName,
BUCKET_NAME: bucket.bucketName,
JWT_SECRET: "your-jwt-secret", // Use AWS Secrets Manager in production
CORS_ORIGIN: stage === "prod" ? "https://yourdomain.com" : "*",
LOG_LEVEL: stage === "prod" ? "info" : "debug",
},
bundling: {
minify: true,
sourceMap: true,
target: "es2020",
keepNames: true,
externalModules: ["aws-sdk"],
},
};
// User Management Functions
const createUserFunction = new NodejsFunction(this, "CreateUserFunction", {
...lambdaDefaults,
functionName: `serverless-app-${stage}-create-user`,
entry: "src/handlers/users.ts",
handler: "createUser",
description: "Create a new user",
});
const loginFunction = new NodejsFunction(this, "LoginFunction", {
...lambdaDefaults,
functionName: `serverless-app-${stage}-login`,
entry: "src/handlers/users.ts",
handler: "login",
description: "User login",
});
const getUserFunction = new NodejsFunction(this, "GetUserFunction", {
...lambdaDefaults,
functionName: `serverless-app-${stage}-get-user`,
entry: "src/handlers/users.ts",
handler: "getUserById",
description: "Get user by ID",
});
const updateUserFunction = new NodejsFunction(this, "UpdateUserFunction", {
...lambdaDefaults,
functionName: `serverless-app-${stage}-update-user`,
entry: "src/handlers/users.ts",
handler: "updateUser",
description: "Update user",
});
const deleteUserFunction = new NodejsFunction(this, "DeleteUserFunction", {
...lambdaDefaults,
functionName: `serverless-app-${stage}-delete-user`,
entry: "src/handlers/users.ts",
handler: "deleteUser",
description: "Delete user",
});
const listUsersFunction = new NodejsFunction(this, "ListUsersFunction", {
...lambdaDefaults,
functionName: `serverless-app-${stage}-list-users`,
entry: "src/handlers/users.ts",
handler: "listUsers",
description: "List users",
});
// API Gateway
const api = new apigateway.RestApi(this, "ServerlessApi", {
restApiName: `serverless-app-${stage}`,
description: "Serverless application API",
deployOptions: {
stageName: stage,
throttlingRateLimit: 1000,
throttlingBurstLimit: 2000,
loggingLevel: apigateway.MethodLoggingLevel.INFO,
dataTraceEnabled: stage !== "prod",
metricsEnabled: true,
},
defaultCorsPreflightOptions: {
allowOrigins:
stage === "prod"
? ["https://yourdomain.com"]
: apigateway.Cors.ALL_ORIGINS,
allowMethods: apigateway.Cors.ALL_METHODS,
allowHeaders: [
"Content-Type",
"X-Amz-Date",
"Authorization",
"X-Api-Key",
"X-Amz-Security-Token",
"X-Correlation-Id",
],
},
});
// API Gateway Resources and Methods
const usersResource = api.root.addResource("users");
const userResource = usersResource.addResource("{id}");
// User endpoints
usersResource.addMethod(
"POST",
new apigateway.LambdaIntegration(createUserFunction)
);
usersResource.addMethod(
"GET",
new apigateway.LambdaIntegration(listUsersFunction)
);
userResource.addMethod(
"GET",
new apigateway.LambdaIntegration(getUserFunction)
);
userResource.addMethod(
"PUT",
new apigateway.LambdaIntegration(updateUserFunction)
);
userResource.addMethod(
"DELETE",
new apigateway.LambdaIntegration(deleteUserFunction)
);
// Auth endpoints
const authResource = api.root.addResource("auth");
authResource
.addResource("login")
.addMethod("POST", new apigateway.LambdaIntegration(loginFunction));
// CloudWatch Log Groups
new logs.LogGroup(this, "ApiGatewayLogGroup", {
logGroupName: `/aws/apigateway/serverless-app-${stage}`,
retention:
stage === "prod"
? logs.RetentionDays.ONE_MONTH
: logs.RetentionDays.ONE_WEEK,
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
// EventBridge for event-driven architecture
const eventBus = new events.EventBus(this, "AppEventBus", {
eventBusName: `serverless-app-${stage}`,
});
// DynamoDB Stream processor
const streamProcessorFunction = new NodejsFunction(
this,
"StreamProcessorFunction",
{
...lambdaDefaults,
functionName: `serverless-app-${stage}-stream-processor`,
entry: "src/handlers/streamProcessor.ts",
handler: "processStream",
description: "Process DynamoDB stream events",
environment: {
...lambdaDefaults.environment,
EVENT_BUS_NAME: eventBus.eventBusName,
},
}
);
// Grant permissions for EventBridge
eventBus.grantPutEventsTo(streamProcessorFunction);
// Connect DynamoDB Stream to Lambda
streamProcessorFunction.addEventSource(
new lambda.DynamoEventSource(table, {
startingPosition: lambda.StartingPosition.TRIM_HORIZON,
batchSize: 10,
maxBatchingWindow: cdk.Duration.seconds(5),
retryAttempts: 3,
})
);
// CloudFormation Outputs
new cdk.CfnOutput(this, "ApiGatewayUrl", {
value: api.url,
description: "API Gateway URL",
exportName: `serverless-app-${stage}-api-url`,
});
new cdk.CfnOutput(this, "TableName", {
value: table.tableName,
description: "DynamoDB Table Name",
exportName: `serverless-app-${stage}-table-name`,
});
new cdk.CfnOutput(this, "BucketName", {
value: bucket.bucketName,
description: "S3 Bucket Name",
exportName: `serverless-app-${stage}-bucket-name`,
});
new cdk.CfnOutput(this, "EventBusName", {
value: eventBus.eventBusName,
description: "EventBridge Bus Name",
exportName: `serverless-app-${stage}-event-bus-name`,
});
}
}
// CDK App
const app = new cdk.App();
const stage = app.node.tryGetContext("stage") || "dev";
new ServerlessAppStack(app, `ServerlessAppStack-${stage}`, {
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION || "us-east-1",
},
stackName: `serverless-app-${stage}`,
description: `Serverless application infrastructure for ${stage} environment`,
tags: {
Environment: stage,
Project: "serverless-app",
Owner: "development-team",
},
});
infrastructure/main.ts
Best Practices Summary
- Type Safety: Use comprehensive TypeScript types throughout the application
- Error Handling: Implement proper error handling with custom error types
- Validation: Use Zod for request validation and data sanitization
- Security: Implement JWT authentication and authorization middleware
- Database Design: Use single-table design pattern with DynamoDB
- Monitoring: Add comprehensive logging and monitoring
- Infrastructure as Code: Use AWS CDK for reproducible deployments
- Performance: Optimize bundle size and cold start times
- Testing: Write unit and integration tests for critical functions
- Documentation: Maintain comprehensive API documentation
Development Commands
# Install dependencies
npm install
# Run local development
npm run dev
# Build functions
npm run build
# Deploy infrastructure
npm run deploy
# Run tests
npm test
# Lint code
npm run lint
Your AWS Lambda TypeScript serverless application is now ready for production with comprehensive infrastructure, type safety, middleware, and monitoring capabilities!