Using middleware for cross-cutting concerns in your Lambda functions
When you’re building an API backed by Lambda functions, you may find yourself implementing similar logic across multiple functions.
In order to minimise code duplication, you might decide to separate out these shared pieces of logic into their own modules and invoke them where appropriate inside your handler. While this is certainly an improvement, you may still end up with a lot of boilerplate control flow code inside your handler body. And the more boilerplate code you have, the higher the risk of unintended copy and paste errors.
Consider the following cross-cutting concerns that are common to many API applications:
- Deserialising request object from a JSON string
- Loading configuration data that the handler function depends upon (e.g. from environment variables, SSM Parameter Store or Secrets Manager)
- Performing request-based authorisation logic
- Validating request payloads
- Sending correct CORS headers in the response
- Logging unhandled errors and the context in which they occurred (by including event payload, etc)
Each of the above would likely require at least a few lines of code (including messy try/catch blocks) before you even start implementing your route-specific business logic.
A more effective way to address such cross-cutting concerns is to use a mechanism called “middleware”. If you’ve used web frameworks such as Express.js, you’re probably familiar with this. If not, here’s a quick explanation of how it works…
The entire API request-response process is treated as a pipeline of JavaScript functions. Multiple functions are chained together in an ordered sequence such that:
- Each middleware function receives the output of the previous function as its input.
- Each middleware function has the ability to continue the flow by passing on its output to the next function or to terminate the flow and have the response sent immediately to the client.
- Each middleware function generally has no knowledge of other functions in the chain, it just implements a common interface.
- The handler function containing your route-specific business logic can be positioned at any point in the sequence.
A top-level handler function acts as the orchestrator of this flow, and this function is configured as the entrypoint of the Lambda function. It receives the event payload that triggered the Lambda function and passes it as the input to the first configured handler in the sequence.
Writing your own middleware orchestrator and individual handler plugins can be a mind melter as you have to write functions that return other functions (an advanced functional programming technique known as currying).
To avoid this, you could instead use a dedicated middleware framework. Middy is probably the most popular one in the AWS Lambda Node.js space. It provides several plugins for common cross-cutting concerns like those I listed above.
The example below shows how you can use 2 Middy plugins to deserialise the JSON event body and read configuration settings from SSM in a declarative manner:
import { APIGatewayProxyEvent } from 'aws-lambda';
import middy from '@middy/core';
import jsonBodyParser from '@middy/http-json-body-parser';
import ssm from '@middy/ssm';
exports.handler = middy((event: APIGatewayProxyEvent) => {
console.log('Received body object', event.body);
console.log('API key is', process.env.APP_API_KEY);
// Add route-specific business logic here...
return {
statusCode: 204,
body: '',
};
}).use([
ssm({
cache: true,
paths: {
APP_: `/myapp/${process.env.STAGE || 'dev'}`,
},
}),
jsonBodyParser(),
]);
Have a great weekend!
— Paul.
Other articles you might enjoy:
Free Email Course
How to transition your team to a serverless-first mindset
In this 5-day email course, you’ll learn:
- Lesson 1: Why serverless is inevitable
- Lesson 2: How to identify a candidate project for your first serverless application
- Lesson 3: How to compose the building blocks that AWS provides
- Lesson 4: Common mistakes to avoid when building your first serverless application
- Lesson 5: How to break ground on your first serverless project