This document describes how to create a serverless event handler component leveraging
shared components from the serverless-common
package.
By applying a standard suite of configurable middleware to your AWS Lambda functions, you can skip the boilerplate code and focus on the business logic of the function.
All AWS Lambda functions share the same handler function shape...
export type Handler<TEvent = any, TResult = any> = (
event: TEvent,
context: Context,
callback: Callback<TResult>,
) => void | Promise<TResult>;
Middy provides the ability to wrap AWS Lambda handler functions in middleware. Each middleware provides the ability to act on the event before the handler, act on the result after the handler, or to act on unhandled errors thrown from the handler or middleware.
Want to know more? Read the official Middy guide for how middleware works.
To create a simplistic API Gateway event handler without event validation, you will need to create two modules: the handler function and the middyfy wrapper.
Create the AWS Lambda Handler function with the usual signature...
NOTE: Each handler function is located in a dedicated directory to allow the handler function and related middleware components to be bundled and exported in a single
index.ts
.
/handlers/task-find/handler.ts
import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';
import { NotFoundError } from '@leanstacks/serverless-common';
...
export const handler = async (
event: APIGatewayProxyEvent,
context: Context,
): Promise<APIGatewayProxyResult> => {
// parse request
const taskListId: string = event.pathParameters?.taskListId ?? '';
const requestor: string = event.requestContext.authorizer?.user_id ?? '';
// perform business logic
const taskDataService = new TaskDataService();
const taskList = await taskDataService.find(taskListId, requestor);
// create response
if (taskList) {
return {
statusCode: 200,
headers: {
'Access-Control-Allow-Origin': config.ALLOW_ORIGIN,
},
body: JSON.stringify(taskList),
};
} else {
throw new NotFoundError();
}
};
Next, we need to "middyfy" the handler function. This is just a fancy way of saying we are wrapping the handler function with middleware.
/handlers/task-find/index.ts
import { middyfyAPIGateway } from '@leanstacks/serverless-common';
import { handler } from './handler';
export const handle = middyfyAPIGateway({ handler });
The handler function appears much the same as what you may be writing already. So what is advantage? The magic happens in the middyfyAPIGateway
function. This simple function wraps the handler in several middlewares.
The logger-initializer
middleware adds AWS Lambda Context metadata to the logger so that all messages may be correlated to a specific Lambda function invocation.
The http-event-normalizer
official Middy middleware ensures that all of the optional elements of an APIGatewayProxyEvent are defined even when empty.
The http-json-body-parser
official Middy middleware parses the HTTP request body converting it from a string of JSON into an object.
The validator-joi
middleware validates the APIGatewayProxyEvent with a Joi schema, when a schema is provided in the middleware options.
The http-error-handler
middleware processes errors thrown from the handler function, creating a standardized response based upon the error type.
In the section above, we discussed how to create an API Gateway event handler function in the most simplistic form. Let's add event validation to ensure that the request contains the required information and throw an error if it does not.
Building on the example from the previous section, simply create a Joi schema.
/handlers/task-find/schema.ts
import { APIGatewayProxyEvent } from 'aws-lambda';
import * as Joi from 'joi';
export const eventSchema = Joi.object<APIGatewayProxyEvent>({
pathParameters: Joi.object({
taskListId: Joi.string().required(),
}),
requestContext: Joi.object({
authorizer: Joi.object({
user_id: Joi.string().required(),
}),
}),
});
So that our middleware will validate the API Gateway event, we must pass the schema in the middleware options in index.ts
as illustrated below.
import { middyfyAPIGateway } from '@leanstacks/serverless-common';
import { handler } from './handler';
import { eventSchema } from './schema';
export const handle = middyfyAPIGateway({ handler, eventSchema });
When an eventSchema
is present in the middleware options, the Joi Validator middleware ensures that the event is valid. The middleware throws a ValidationError if it is invalid. The HTTP Error Handler knows how to handle a ValidationError, returning a response with status code 400 and an informative message.
Notice that nothing changed with the handler function itself!
Use a Scheduled event handler when you need to run a Lambda function on a cron schedule.
To create a Scheduled event handler, you will need to create two modules: the handler function and the middyfy wrapper.
Create the AWS Lambda Handler function with the usual signature...
/handlers/task-scheduled/handler.ts
import { Context, ScheduledEvent } from 'aws-lambda';
...
export const handler = async (event: ScheduledEvent, context: Context): Promise<void> => {
try {
// perform business logic
await TaskService.sendReminders();
} catch (error) {
Logger.error('Failed to send task reminders. Detail:', error);
throw new ServiceError(error);
}
};
Next, we need to "middyfy" the handler function. This is just a fancy way of saying we are wrapping the handler function with middleware.
/handlers/task-scheduled/index.ts
import { middyfyScheduled } from '@leanstacks/serverless-common';
import { handler } from './handler';
export const handle = middyfyScheduled({ handler });
Use a SNS event handler to process events from an AWS Simple Notification Service (SNS) topic.
To create a SNS event handler, you will need to create two modules: the handler function and the middyfy wrapper.
Create the AWS Lambda Handler function with the usual signature...
/handlers/task-sns/handler.ts
import { Context, SNSEvent } from 'aws-lambda';
...
export const handler = async (event: SNSEvent, context: Context): Promise<void> => {
try {
// perform business logic
const promises = map(event.Records, (record) => {
const taskToCreate = record.Sns.Message as Task;
return TaskService.create(taskToCreate);
});
const tasks:Task[] = await Promise.all(promises);
} catch (error) {
Logger.error('Failed to create tasks. Detail:', error);
throw new ServiceError(error);
}
};
Next, we need to "middyfy" the handler function. This is just a fancy way of saying we are wrapping the handler function with middleware.
/handlers/task-sns/index.ts
import { middyfySNS } from '@leanstacks/serverless-common';
import { handler } from './handler';
export const handle = middyfySNS({ handler });
The handler is wrapped with two middlewares.
-
The
logger-initializer
middleware adds AWS Lambda Context metadata to the logger so that all messages may be correlated to a specific Lambda function invocation. -
The
event-normalizer
middleware performs a JSON parse on theMessage
. -
The
validator
middleware will validate the event when aneventSchema
is provided in the options.
Use a SQS event handler to process events from an AWS Simple Queue Service (SQS) topic.
To create a SQS event handler, you will need to create two modules: the handler function and the middyfy wrapper. You may optionally create a schema.ts
module which provides a Joi validation schema for the SQSEvent
.
Create the AWS Lambda Handler function with the usual signature...
/handlers/task-sqs/handler.ts
import { Context, SQSEvent, SQSBatchResponse } from 'aws-lambda';
...
export const handler = async (event: SNSEvent, context: Context): Promise<SQSBatchResponse | void> => {
try {
// perform business logic
const promises = map(event.Records, (record) => {
const taskToCreate = record.body as Task;
return TaskService.create(taskToCreate);
});
const response:SQSBatchResponse = {};
Promise.allSettled(promises)
.then((values) => {
response.batchItemFailures = map(values, (value) => {
if (value.status === 'rejected') {
return { itemIdentifier: value.reason }
}
})
});
return response;
} catch (error) {
Logger.error('Failed to create tasks. Detail:', error);
throw new ServiceError(error);
}
};
Next, we need to "middyfy" the handler function. This is just a fancy way of saying we are wrapping the handler function with middleware.
/handlers/task-sqs/index.ts
import { middyfySQS } from '@leanstacks/serverless-common';
import { handler } from './handler';
export const handle = middyfySQS({ handler });
The handler is wrapped with two middlewares.
- The
logger-initializer
middleware adds AWS Lambda Context metadata to the logger so that all messages may be correlated to a specific Lambda function invocation. - The
event-normalizer
middleware performs a JSON parse on thebody
. - The
validator
middleware will validate the event when aneventSchema
is provided in the options.
Use a Lambda event handler to process events from direct invocations from another Lambda function.
NOTE: This is generally considered to be an anti-pattern in serverless application design. See Lambda functions calling Lambda functions in the AWS Lambda guide. Consider SQS queues or Step Functions as an alternative.
To create a Lambda event handler, you will need to create two modules: the handler function and the middyfy wrapper. You may optionally create a schema.ts
module which provides a Joi validation schema for the LambdaEvent
.
Create the AWS Lambda Handler function with the usual signature...
/handlers/task-lambda/handler.ts
import { Context } from 'aws-lambda';
import { LambdaEvent, LambdaResult, middyfyLambda } from '@leanstacks/serverless-common';
...
type FindTaskEvent = {
taskId: string;
};
type FindTaskResult = {
task?: Task;
};
export const handler = async (
event: LambdaEvent<FindTaskEvent>,
context: Context,
): Promise<LambdaResult<FindTaskResult>> => {
try {
// handle request
const { taskId } = event;
const task = await TaskService.findById(taskId);
// format and return response
if (task) {
return {
status: 200,
statusText: 'OK',
data: { task },
};
} else {
return {
status: 404,
statusText: 'Not Found',
data: {},
};
}
} catch (error) {
Logger.warn(`Failed to find Task. Detail: ${error}`);
return {
status: 500,
statusText: `${error}`,
data: {},
};
}
};
Next, we need to "middyfy" the handler function. This is just a fancy way of saying we are wrapping the handler function with middleware.
/handlers/task-lambda/index.ts
import { middyfyLambda } from '@leanstacks/serverless-common';
import { handler } from './handler';
export const handle = middyfyLambda({ handler });
The handler is wrapped with one middleware.
- The
logger-initializer
middleware adds AWS Lambda Context metadata to the logger so that all messages may be correlated to a specific Lambda function invocation. - The
validator
middleware will validate the event when aneventSchema
is provided in the options.