AWS Serverless Development with Lambda
Build scalable serverless applications using AWS Lambda, API Gateway, and serverless framework
# AWS Serverless Development with Lambda
## 1. Serverless Framework Setup
### Project Structure and Configuration
```yaml
# serverless.yml
service: my-serverless-api
frameworkVersion: '3'
provider:
name: aws
runtime: nodejs18.x
region: ${opt:region, 'us-east-1'}
stage: ${opt:stage, 'dev'}
environment:
STAGE: ${self:provider.stage}
REGION: ${self:provider.region}
DYNAMODB_TABLE: ${self:service}-${self:provider.stage}-users
JWT_SECRET: ${ssm:/${self:service}/${self:provider.stage}/jwt-secret}
iam:
role:
statements:
- Effect: Allow
Action:
- dynamodb:Query
- dynamodb:Scan
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
Resource:
- "arn:aws:dynamodb:${self:provider.region}:*:table/${self:provider.environment.DYNAMODB_TABLE}"
- "arn:aws:dynamodb:${self:provider.region}:*:table/${self:provider.environment.DYNAMODB_TABLE}/index/*"
- Effect: Allow
Action:
- s3:GetObject
- s3:PutObject
- s3:DeleteObject
Resource:
- "arn:aws:s3:::my-app-uploads-${self:provider.stage}/*"
- Effect: Allow
Action:
- ses:SendEmail
- ses:SendRawEmail
Resource: "*"
plugins:
- serverless-webpack
- serverless-offline
- serverless-dynamodb-local
- serverless-dotenv-plugin
- serverless-prune-plugin
- serverless-plugin-tracing
custom:
webpack:
webpackConfig: 'webpack.config.js'
includeModules: true
packager: 'npm'
excludeFiles: src/**/*.test.js
dynamodb:
start:
port: 8000
inMemory: true
migrate: true
stages:
- dev
- test
prune:
automatic: true
number: 3
functions:
authorizer:
handler: src/handlers/auth.authorize
# User Management
createUser:
handler: src/handlers/users.create
events:
- http:
path: /users
method: post
cors: true
getUser:
handler: src/handlers/users.get
events:
- http:
path: /users/{id}
method: get
cors: true
authorizer: authorizer
updateUser:
handler: src/handlers/users.update
events:
- http:
path: /users/{id}
method: put
cors: true
authorizer: authorizer
listUsers:
handler: src/handlers/users.list
events:
- http:
path: /users
method: get
cors: true
authorizer: authorizer
request:
parameters:
querystrings:
limit: false
cursor: false
# File Upload
uploadFile:
handler: src/handlers/files.upload
timeout: 30
events:
- http:
path: /upload
method: post
cors: true
authorizer: authorizer
# Background Jobs
processImage:
handler: src/handlers/jobs.processImage
timeout: 300
memorySize: 1024
events:
- s3:
bucket: my-app-uploads-${self:provider.stage}
event: s3:ObjectCreated:*
rules:
- suffix: .jpg
- suffix: .png
sendNotification:
handler: src/handlers/notifications.send
events:
- sqs:
arn:
Fn::GetAtt:
- NotificationQueue
- Arn
# Scheduled Jobs
dailyCleanup:
handler: src/handlers/scheduled.cleanup
events:
- schedule: cron(0 2 * * ? *)
resources:
Resources:
UsersTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: ${self:provider.environment.DYNAMODB_TABLE}
AttributeDefinitions:
- AttributeName: id
AttributeType: S
- AttributeName: email
AttributeType: S
- AttributeName: createdAt
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
GlobalSecondaryIndexes:
- IndexName: EmailIndex
KeySchema:
- AttributeName: email
KeyType: HASH
Projection:
ProjectionType: ALL
BillingMode: PAY_PER_REQUEST
- IndexName: CreatedAtIndex
KeySchema:
- AttributeName: createdAt
KeyType: HASH
Projection:
ProjectionType: ALL
BillingMode: PAY_PER_REQUEST
BillingMode: PAY_PER_REQUEST
StreamSpecification:
StreamViewType: NEW_AND_OLD_IMAGES
S3Bucket:
Type: AWS::S3::Bucket
Properties:
BucketName: my-app-uploads-${self:provider.stage}
CorsConfiguration:
CorsRules:
- AllowedHeaders: ['*']
AllowedMethods: [GET, PUT, POST, DELETE]
AllowedOrigins: ['*']
NotificationQueue:
Type: AWS::SQS::Queue
Properties:
QueueName: ${self:service}-${self:provider.stage}-notifications
VisibilityTimeoutSeconds: 300
MessageRetentionPeriod: 1209600
```
## 2. Lambda Function Implementation
### User Management Handler
```typescript
// src/handlers/users.ts
import { APIGatewayProxyHandler, APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
import { DynamoDB } from 'aws-sdk';
import { v4 as uuidv4 } from 'uuid';
import { createResponse, parseJSON, validateEmail, hashPassword } from '../utils/helpers';
import { UserService } from '../services/userService';
import { ValidationError, NotFoundError } from '../utils/errors';
const dynamodb = new DynamoDB.DocumentClient();
const userService = new UserService(dynamodb);
export const create: APIGatewayProxyHandler = async (
event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
try {
console.log('Creating user:', JSON.stringify(event, null, 2));
const body = parseJSON(event.body);
// Validation
if (!body.email || !body.name || !body.password) {
throw new ValidationError('Email, name, and password are required');
}
if (!validateEmail(body.email)) {
throw new ValidationError('Invalid email format');
}
if (body.password.length < 8) {
throw new ValidationError('Password must be at least 8 characters');
}
// Check if user already exists
const existingUser = await userService.findByEmail(body.email);
if (existingUser) {
throw new ValidationError('User with this email already exists');
}
// Create user
const user = {
id: uuidv4(),
email: body.email.toLowerCase(),
name: body.name,
passwordHash: await hashPassword(body.password),
isActive: true,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
await userService.create(user);
// Return user without password hash
const { passwordHash, ...userResponse } = user;
return createResponse(201, {
message: 'User created successfully',
user: userResponse,
});
} catch (error) {
console.error('Error creating user:', error);
if (error instanceof ValidationError) {
return createResponse(400, { message: error.message });
}
return createResponse(500, { message: 'Internal server error' });
}
};
export const get: APIGatewayProxyHandler = async (
event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
try {
const userId = event.pathParameters?.id;
if (!userId) {
return createResponse(400, { message: 'User ID is required' });
}
const user = await userService.findById(userId);
if (!user) {
throw new NotFoundError('User not found');
}
// Remove sensitive data
const { passwordHash, ...userResponse } = user;
return createResponse(200, { user: userResponse });
} catch (error) {
console.error('Error getting user:', error);
if (error instanceof NotFoundError) {
return createResponse(404, { message: error.message });
}
return createResponse(500, { message: 'Internal server error' });
}
};
export const update: APIGatewayProxyHandler = async (
event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
try {
const userId = event.pathParameters?.id;
const currentUser = event.requestContext.authorizer?.user;
if (!userId) {
return createResponse(400, { message: 'User ID is required' });
}
// Check authorization
if (currentUser.id !== userId && currentUser.role !== 'admin') {
return createResponse(403, { message: 'Unauthorized to update this user' });
}
const body = parseJSON(event.body);
const updateData: any = {
updatedAt: new Date().toISOString(),
};
// Validate and add fields to update
if (body.name) {
updateData.name = body.name;
}
if (body.email) {
if (!validateEmail(body.email)) {
throw new ValidationError('Invalid email format');
}
updateData.email = body.email.toLowerCase();
}
if (body.password) {
if (body.password.length < 8) {
throw new ValidationError('Password must be at least 8 characters');
}
updateData.passwordHash = await hashPassword(body.password);
}
const updatedUser = await userService.update(userId, updateData);
if (!updatedUser) {
throw new NotFoundError('User not found');
}
// Remove sensitive data
const { passwordHash, ...userResponse } = updatedUser;
return createResponse(200, {
message: 'User updated successfully',
user: userResponse,
});
} catch (error) {
console.error('Error updating user:', error);
if (error instanceof ValidationError) {
return createResponse(400, { message: error.message });
}
if (error instanceof NotFoundError) {
return createResponse(404, { message: error.message });
}
return createResponse(500, { message: 'Internal server error' });
}
};
export const list: APIGatewayProxyHandler = async (
event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
try {
const limit = parseInt(event.queryStringParameters?.limit || '20');
const cursor = event.queryStringParameters?.cursor;
const result = await userService.list({
limit: Math.min(limit, 100), // Cap at 100
cursor,
});
return createResponse(200, result);
} catch (error) {
console.error('Error listing users:', error);
return createResponse(500, { message: 'Internal server error' });
}
};
```
### Authentication Handler
```typescript
// src/handlers/auth.ts
import { APIGatewayTokenAuthorizerHandler, APIGatewayAuthorizerResult } from 'aws-lambda';
import jwt from 'jsonwebtoken';
import { UserService } from '../services/userService';
import { DynamoDB } from 'aws-sdk';
const dynamodb = new DynamoDB.DocumentClient();
const userService = new UserService(dynamodb);
export const authorize: APIGatewayTokenAuthorizerHandler = async (event) => {
try {
const token = event.authorizationToken?.replace('Bearer ', '');
if (!token) {
throw new Error('No token provided');
}
// Verify JWT token
const decoded = jwt.verify(token, process.env.JWT_SECRET!) as any;
// Get user from database
const user = await userService.findById(decoded.userId);
if (!user || !user.isActive) {
throw new Error('User not found or inactive');
}
// Generate policy
const policy = generatePolicy(user.id, 'Allow', event.methodArn);
// Add user context
policy.context = {
userId: user.id,
email: user.email,
name: user.name,
role: user.role || 'user',
};
return policy;
} catch (error) {
console.error('Authorization error:', error);
throw new Error('Unauthorized');
}
};
const generatePolicy = (
principalId: string,
effect: 'Allow' | 'Deny',
resource: string
): APIGatewayAuthorizerResult => {
return {
principalId,
policyDocument: {
Version: '2012-10-17',
Statement: [
{
Action: 'execute-api:Invoke',
Effect: effect,
Resource: resource,
},
],
},
};
};
// Login handler
import { APIGatewayProxyHandler } from 'aws-lambda';
import bcrypt from 'bcryptjs';
import { createResponse, parseJSON, validateEmail } from '../utils/helpers';
import { ValidationError } from '../utils/errors';
export const login: APIGatewayProxyHandler = async (event) => {
try {
const body = parseJSON(event.body);
if (!body.email || !body.password) {
throw new ValidationError('Email and password are required');
}
if (!validateEmail(body.email)) {
throw new ValidationError('Invalid email format');
}
// Find user by email
const user = await userService.findByEmail(body.email.toLowerCase());
if (!user || !user.isActive) {
throw new ValidationError('Invalid credentials');
}
// Verify password
const isValidPassword = await bcrypt.compare(body.password, user.passwordHash);
if (!isValidPassword) {
throw new ValidationError('Invalid credentials');
}
// Generate JWT token
const token = jwt.sign(
{ userId: user.id, email: user.email },
process.env.JWT_SECRET!,
{ expiresIn: '24h' }
);
// Remove sensitive data
const { passwordHash, ...userResponse } = user;
return createResponse(200, {
message: 'Login successful',
token,
user: userResponse,
});
} catch (error) {
console.error('Login error:', error);
if (error instanceof ValidationError) {
return createResponse(400, { message: error.message });
}
return createResponse(500, { message: 'Internal server error' });
}
};
```
## 3. DynamoDB Service Layer
### User Service with DynamoDB
```typescript
// src/services/userService.ts
import { DynamoDB } from 'aws-sdk';
export interface User {
id: string;
email: string;
name: string;
passwordHash: string;
isActive: boolean;
role?: string;
createdAt: string;
updatedAt: string;
}
export interface ListOptions {
limit: number;
cursor?: string;
}
export interface ListResult {
users: Omit<User, 'passwordHash'>[];
nextCursor?: string;
hasMore: boolean;
}
export class UserService {
private tableName: string;
constructor(private dynamodb: DynamoDB.DocumentClient) {
this.tableName = process.env.DYNAMODB_TABLE!;
}
async create(user: User): Promise<User> {
const params = {
TableName: this.tableName,
Item: user,
ConditionExpression: 'attribute_not_exists(id)',
};
await this.dynamodb.put(params).promise();
return user;
}
async findById(id: string): Promise<User | null> {
const params = {
TableName: this.tableName,
Key: { id },
};
const result = await this.dynamodb.get(params).promise();
return result.Item as User || null;
}
async findByEmail(email: string): Promise<User | null> {
const params = {
TableName: this.tableName,
IndexName: 'EmailIndex',
KeyConditionExpression: 'email = :email',
ExpressionAttributeValues: {
':email': email,
},
};
const result = await this.dynamodb.query(params).promise();
return result.Items?.[0] as User || null;
}
async update(id: string, updateData: Partial<User>): Promise<User | null> {
// Build update expression
const updateExpressions: string[] = [];
const expressionAttributeNames: Record<string, string> = {};
const expressionAttributeValues: Record<string, any> = {};
Object.keys(updateData).forEach((key, index) => {
const attributeName = `#attr${index}`;
const attributeValue = `:val${index}`;
updateExpressions.push(`${attributeName} = ${attributeValue}`);
expressionAttributeNames[attributeName] = key;
expressionAttributeValues[attributeValue] = updateData[key as keyof User];
});
const params = {
TableName: this.tableName,
Key: { id },
UpdateExpression: `SET ${updateExpressions.join(', ')}`,
ExpressionAttributeNames: expressionAttributeNames,
ExpressionAttributeValues: expressionAttributeValues,
ReturnValues: 'ALL_NEW' as const,
ConditionExpression: 'attribute_exists(id)',
};
try {
const result = await this.dynamodb.update(params).promise();
return result.Attributes as User;
} catch (error: any) {
if (error.code === 'ConditionalCheckFailedException') {
return null;
}
throw error;
}
}
async delete(id: string): Promise<boolean> {
const params = {
TableName: this.tableName,
Key: { id },
ConditionExpression: 'attribute_exists(id)',
};
try {
await this.dynamodb.delete(params).promise();
return true;
} catch (error: any) {
if (error.code === 'ConditionalCheckFailedException') {
return false;
}
throw error;
}
}
async list(options: ListOptions): Promise<ListResult> {
const params: DynamoDB.DocumentClient.ScanInput = {
TableName: this.tableName,
Limit: options.limit,
ProjectionExpression: 'id, email, #name, isActive, #role, createdAt, updatedAt',
ExpressionAttributeNames: {
'#name': 'name',
'#role': 'role',
},
};
if (options.cursor) {
params.ExclusiveStartKey = JSON.parse(
Buffer.from(options.cursor, 'base64').toString()
);
}
const result = await this.dynamodb.scan(params).promise();
const users = result.Items as Omit<User, 'passwordHash'>[];
const hasMore = !!result.LastEvaluatedKey;
const nextCursor = hasMore
? Buffer.from(JSON.stringify(result.LastEvaluatedKey)).toString('base64')
: undefined;
return {
users,
nextCursor,
hasMore,
};
}
}
```
## 4. File Upload and Processing
### S3 File Upload Handler
```typescript
// src/handlers/files.ts
import { APIGatewayProxyHandler } from 'aws-lambda';
import { S3 } from 'aws-sdk';
import { v4 as uuidv4 } from 'uuid';
import { createResponse, parseJSON } from '../utils/helpers';
import { ValidationError } from '../utils/errors';
const s3 = new S3();
const bucketName = process.env.S3_BUCKET!;
export const upload: APIGatewayProxyHandler = async (event) => {
try {
const body = parseJSON(event.body);
const currentUser = event.requestContext.authorizer?.user;
if (!body.fileName || !body.fileType || !body.fileContent) {
throw new ValidationError('fileName, fileType, and fileContent are required');
}
// Validate file type
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf'];
if (!allowedTypes.includes(body.fileType)) {
throw new ValidationError('File type not allowed');
}
// Generate unique file key
const fileExtension = body.fileName.split('.').pop();
const fileKey = `uploads/${currentUser.userId}/${uuidv4()}.${fileExtension}`;
// Convert base64 to buffer
const fileBuffer = Buffer.from(body.fileContent, 'base64');
// Check file size (max 5MB)
if (fileBuffer.length > 5 * 1024 * 1024) {
throw new ValidationError('File size exceeds 5MB limit');
}
// Upload to S3
const uploadParams = {
Bucket: bucketName,
Key: fileKey,
Body: fileBuffer,
ContentType: body.fileType,
Metadata: {
originalName: body.fileName,
uploadedBy: currentUser.userId,
uploadedAt: new Date().toISOString(),
},
};
const result = await s3.upload(uploadParams).promise();
// Generate signed URL for download
const downloadUrl = s3.getSignedUrl('getObject', {
Bucket: bucketName,
Key: fileKey,
Expires: 3600, // 1 hour
});
return createResponse(200, {
message: 'File uploaded successfully',
fileId: fileKey,
downloadUrl,
location: result.Location,
});
} catch (error) {
console.error('Upload error:', error);
if (error instanceof ValidationError) {
return createResponse(400, { message: error.message });
}
return createResponse(500, { message: 'Internal server error' });
}
};
// Image processing handler
import sharp from 'sharp';
export const processImage = async (event: any) => {
try {
const records = event.Records || [];
for (const record of records) {
const bucket = record.s3.bucket.name;
const key = decodeURIComponent(record.s3.object.key.replace(/\+/g, ' '));
console.log(`Processing image: ${bucket}/${key}`);
// Download original image
const originalImage = await s3.getObject({ Bucket: bucket, Key: key }).promise();
if (!originalImage.Body) {
continue;
}
// Create thumbnails
const thumbnailSizes = [
{ width: 150, height: 150, suffix: 'thumb' },
{ width: 300, height: 300, suffix: 'small' },
{ width: 800, height: 600, suffix: 'medium' },
];
for (const size of thumbnailSizes) {
const resizedImage = await sharp(originalImage.Body as Buffer)
.resize(size.width, size.height, {
fit: 'cover',
position: 'center',
})
.jpeg({ quality: 80 })
.toBuffer();
const thumbnailKey = key.replace(
/\.(jpg|jpeg|png|gif)$/i,
`_${size.suffix}.jpg`
);
await s3.putObject({
Bucket: bucket,
Key: thumbnailKey,
Body: resizedImage,
ContentType: 'image/jpeg',
Metadata: {
originalKey: key,
thumbnailSize: `${size.width}x${size.height}`,
},
}).promise();
console.log(`Created thumbnail: ${thumbnailKey}`);
}
}
} catch (error) {
console.error('Image processing error:', error);
throw error;
}
};
```
## 5. Background Jobs and Queues
### SQS Message Processing
```typescript
// src/handlers/notifications.ts
import { SQSHandler } from 'aws-lambda';
import { SES } from 'aws-sdk';
import { UserService } from '../services/userService';
import { DynamoDB } from 'aws-sdk';
const ses = new SES();
const dynamodb = new DynamoDB.DocumentClient();
const userService = new UserService(dynamodb);
export interface NotificationMessage {
type: 'email' | 'sms' | 'push';
recipient: string;
subject: string;
message: string;
metadata?: Record<string, any>;
}
export const send: SQSHandler = async (event) => {
try {
for (const record of event.Records) {
const message: NotificationMessage = JSON.parse(record.body);
console.log('Processing notification:', message);
switch (message.type) {
case 'email':
await sendEmail(message);
break;
case 'sms':
await sendSMS(message);
break;
case 'push':
await sendPushNotification(message);
break;
default:
console.warn('Unknown notification type:', message.type);
}
}
} catch (error) {
console.error('Notification processing error:', error);
throw error; // This will put the message back in the queue for retry
}
};
async function sendEmail(message: NotificationMessage): Promise<void> {
const params = {
Source: 'noreply@example.com',
Destination: {
ToAddresses: [message.recipient],
},
Message: {
Subject: {
Data: message.subject,
Charset: 'UTF-8',
},
Body: {
Html: {
Data: message.message,
Charset: 'UTF-8',
},
},
},
};
await ses.sendEmail(params).promise();
console.log(`Email sent to ${message.recipient}`);
}
async function sendSMS(message: NotificationMessage): Promise<void> {
// Implement SMS sending logic (e.g., using SNS)
console.log(`SMS would be sent to ${message.recipient}: ${message.message}`);
}
async function sendPushNotification(message: NotificationMessage): Promise<void> {
// Implement push notification logic
console.log(`Push notification would be sent to ${message.recipient}: ${message.message}`);
}
// Utility function to queue notifications
import { SQS } from 'aws-sdk';
const sqs = new SQS();
const queueUrl = process.env.NOTIFICATION_QUEUE_URL!;
export async function queueNotification(message: NotificationMessage): Promise<void> {
const params = {
QueueUrl: queueUrl,
MessageBody: JSON.stringify(message),
DelaySeconds: 0,
};
await sqs.sendMessage(params).promise();
console.log('Notification queued:', message.type, message.recipient);
}
```
### Scheduled Tasks
```typescript
// src/handlers/scheduled.ts
import { ScheduledHandler } from 'aws-lambda';
import { DynamoDB, S3 } from 'aws-sdk';
import { UserService } from '../services/userService';
const dynamodb = new DynamoDB.DocumentClient();
const s3 = new S3();
const userService = new UserService(dynamodb);
export const cleanup: ScheduledHandler = async (event) => {
try {
console.log('Starting daily cleanup job');
// Clean up expired files
await cleanupExpiredFiles();
// Clean up inactive users
await cleanupInactiveUsers();
// Generate daily reports
await generateDailyReports();
console.log('Daily cleanup completed successfully');
} catch (error) {
console.error('Cleanup job failed:', error);
throw error;
}
};
async function cleanupExpiredFiles(): Promise<void> {
const bucketName = process.env.S3_BUCKET!;
const expiredDate = new Date();
expiredDate.setDate(expiredDate.getDate() - 30); // 30 days old
try {
const objects = await s3.listObjectsV2({ Bucket: bucketName }).promise();
const expiredObjects = objects.Contents?.filter(obj =>
obj.LastModified && obj.LastModified < expiredDate
) || [];
if (expiredObjects.length > 0) {
const deleteParams = {
Bucket: bucketName,
Delete: {
Objects: expiredObjects.map(obj => ({ Key: obj.Key! })),
},
};
await s3.deleteObjects(deleteParams).promise();
console.log(`Deleted ${expiredObjects.length} expired files`);
}
} catch (error) {
console.error('Error cleaning up files:', error);
}
}
async function cleanupInactiveUsers(): Promise<void> {
// Implementation for cleaning up inactive users
console.log('Cleaning up inactive users...');
}
async function generateDailyReports(): Promise<void> {
// Implementation for generating daily reports
console.log('Generating daily reports...');
}
```
## 6. Testing Serverless Functions
### Unit Tests
```typescript
// tests/handlers/users.test.ts
import { APIGatewayProxyEvent, Context } from 'aws-lambda';
import { create, get } from '../../src/handlers/users';
import { UserService } from '../../src/services/userService';
// Mock AWS SDK
jest.mock('aws-sdk');
// Mock UserService
jest.mock('../../src/services/userService');
const MockedUserService = UserService as jest.MockedClass<typeof UserService>;
describe('User Handlers', () => {
let mockUserService: jest.Mocked<UserService>;
beforeEach(() => {
mockUserService = new MockedUserService({} as any) as jest.Mocked<UserService>;
jest.clearAllMocks();
});
describe('create', () => {
it('should create a user successfully', async () => {
const event: Partial<APIGatewayProxyEvent> = {
body: JSON.stringify({
email: 'test@example.com',
name: 'Test User',
password: 'password123',
}),
};
const mockUser = {
id: 'user-123',
email: 'test@example.com',
name: 'Test User',
passwordHash: 'hashed-password',
isActive: true,
createdAt: '2023-01-01T00:00:00.000Z',
updatedAt: '2023-01-01T00:00:00.000Z',
};
mockUserService.findByEmail.mockResolvedValue(null);
mockUserService.create.mockResolvedValue(mockUser);
const result = await create(event as APIGatewayProxyEvent, {} as Context, () => {});
expect(result.statusCode).toBe(201);
expect(JSON.parse(result.body)).toEqual({
message: 'User created successfully',
user: expect.objectContaining({
id: 'user-123',
email: 'test@example.com',
name: 'Test User',
}),
});
expect(JSON.parse(result.body).user.passwordHash).toBeUndefined();
});
it('should return error for existing email', async () => {
const event: Partial<APIGatewayProxyEvent> = {
body: JSON.stringify({
email: 'existing@example.com',
name: 'Test User',
password: 'password123',
}),
};
const existingUser = {
id: 'user-456',
email: 'existing@example.com',
name: 'Existing User',
passwordHash: 'hashed-password',
isActive: true,
createdAt: '2023-01-01T00:00:00.000Z',
updatedAt: '2023-01-01T00:00:00.000Z',
};
mockUserService.findByEmail.mockResolvedValue(existingUser);
const result = await create(event as APIGatewayProxyEvent, {} as Context, () => {});
expect(result.statusCode).toBe(400);
expect(JSON.parse(result.body)).toEqual({
message: 'User with this email already exists',
});
});
it('should validate required fields', async () => {
const event: Partial<APIGatewayProxyEvent> = {
body: JSON.stringify({
email: 'test@example.com',
// Missing name and password
}),
};
const result = await create(event as APIGatewayProxyEvent, {} as Context, () => {});
expect(result.statusCode).toBe(400);
expect(JSON.parse(result.body)).toEqual({
message: 'Email, name, and password are required',
});
});
});
describe('get', () => {
it('should return user by ID', async () => {
const event: Partial<APIGatewayProxyEvent> = {
pathParameters: { id: 'user-123' },
};
const mockUser = {
id: 'user-123',
email: 'test@example.com',
name: 'Test User',
passwordHash: 'hashed-password',
isActive: true,
createdAt: '2023-01-01T00:00:00.000Z',
updatedAt: '2023-01-01T00:00:00.000Z',
};
mockUserService.findById.mockResolvedValue(mockUser);
const result = await get(event as APIGatewayProxyEvent, {} as Context, () => {});
expect(result.statusCode).toBe(200);
expect(JSON.parse(result.body)).toEqual({
user: expect.objectContaining({
id: 'user-123',
email: 'test@example.com',
name: 'Test User',
}),
});
expect(JSON.parse(result.body).user.passwordHash).toBeUndefined();
});
it('should return 404 for non-existent user', async () => {
const event: Partial<APIGatewayProxyEvent> = {
pathParameters: { id: 'non-existent' },
};
mockUserService.findById.mockResolvedValue(null);
const result = await get(event as APIGatewayProxyEvent, {} as Context, () => {});
expect(result.statusCode).toBe(404);
expect(JSON.parse(result.body)).toEqual({
message: 'User not found',
});
});
});
});
```
### Integration Tests
```typescript
// tests/integration/api.test.ts
import { handler } from '../../src/handlers/users';
import { DynamoDB } from 'aws-sdk';
// Use DynamoDB Local for integration tests
const dynamodb = new DynamoDB.DocumentClient({
region: 'local',
endpoint: 'http://localhost:8000',
});
describe('User API Integration Tests', () => {
beforeAll(async () => {
// Set up test database
await setupTestDatabase();
});
afterAll(async () => {
// Clean up test database
await cleanupTestDatabase();
});
beforeEach(async () => {
// Clear test data before each test
await clearTestData();
});
it('should create and retrieve a user', async () => {
// Create user
const createEvent = {
httpMethod: 'POST',
path: '/users',
body: JSON.stringify({
email: 'integration@example.com',
name: 'Integration Test',
password: 'password123',
}),
headers: {},
pathParameters: null,
queryStringParameters: null,
requestContext: {} as any,
resource: '',
stageVariables: null,
isBase64Encoded: false,
multiValueHeaders: {},
multiValueQueryStringParameters: null,
};
const createResult = await handler(createEvent, {} as any, () => {});
expect(createResult.statusCode).toBe(201);
const createdUser = JSON.parse(createResult.body).user;
// Retrieve user
const getEvent = {
...createEvent,
httpMethod: 'GET',
path: `/users/${createdUser.id}`,
pathParameters: { id: createdUser.id },
body: null,
};
const getResult = await handler(getEvent, {} as any, () => {});
expect(getResult.statusCode).toBe(200);
const retrievedUser = JSON.parse(getResult.body).user;
expect(retrievedUser.email).toBe('integration@example.com');
expect(retrievedUser.name).toBe('Integration Test');
});
});
async function setupTestDatabase() {
// Create test table
// Implementation depends on your test setup
}
async function cleanupTestDatabase() {
// Delete test table
// Implementation depends on your test setup
}
async function clearTestData() {
// Clear all items from test table
// Implementation depends on your test setup
}
```
## Checklist for AWS Serverless Development
- [ ] Set up Serverless Framework with proper configuration
- [ ] Implement Lambda functions with proper error handling
- [ ] Configure DynamoDB tables with appropriate indexes
- [ ] Set up API Gateway with authentication and CORS
- [ ] Implement file upload and processing with S3
- [ ] Create background job processing with SQS
- [ ] Add scheduled tasks for maintenance operations
- [ ] Configure proper IAM roles and permissions
- [ ] Implement comprehensive logging and monitoring
- [ ] Set up local development environment
- [ ] Add unit and integration tests
- [ ] Configure environment-specific deployments
- [ ] Implement proper secret management
- [ ] Add performance monitoring and alerting
- [ ] Set up CI/CD pipeline for automated deployments