DRY Principle in Your AWS SAM Application with Middlewares
AWS Lambda is very popular in the serverless world. The ability to write code and not to worry about any infrastructure is great.
AWS SAM (Serverless application model) is a great way to write a complete backend application only with lambda. The idea is you just write the functions and define the infrastructure in the code.
But this creates a lot of duplication of logic as each function is written separately. But can we improve our architecture? Let’s find out!
The Issue with Lambda
Nothing is perfect in this world. And lambda’s are no different. When we use some backend nodejs framework like express, we can take care of some common tasks in a single place. Like
Incoming request validation
Error handling
Logging
But in aws lambda, we don’t have that privilege. Every lambda we use have the following structure
try{
// write business logic
}catch(err){
// handle error
}
This is repetitive and boring. How about we find out a way to handle this problem and improve our lambda architecture?
Here Comes Lambda Middleware
Middleware is a concept mainly familiar to nodejs programmers. Basically, it can intercept every request that comes in. Which is perfect for tackling common issues.
lambda-middleware is a collection of middleware that can be used with your existing lambda architecture.
Use a single middleware
Error handling is a crucial part of any application. There is an error handler middleware already written that we can take advantage of.
First, install the dependency.
npm i @lambda-middleware/errorHandler
Then use this middleware like the following
import { errorHandler } from "@lambda-middleware/errorHandler";
import { APIGatewayEvent, APIGatewayProxyResult } from "aws-lambda";
async function helloWorld(event: APIGatewayEvent): Promise<APIGatewayProxyResult> {
// throw error without try catch
throw new Error("Search is not implemented yet");
}
export const handler = errorHandler()(helloWorld); // <-- see here
Using this middleware relieves you from using any kind of try/catch block inside your application. This is huge!
Use Multiple Middlewares
Now as you can see using just one middleware we have improved our code. Let’s say we want to introduce request validation.
Normally in AWS Lambda, the request body comes inside the event.body
. We normally parse the request and then check manually if our desired parameters have arrived or not like the following
import { APIGatewayEvent } from 'aws-lambda';
export const handler = async (event: APIGatewayEvent) => {
try {
const request = JSON.parse(event.body)
if(!request.name) throw Error('Name not found!')
} catch (err: any) {
return errorResposnse(err);
}
};
But what if we can do the same thing but in a more declarative way?
Let’s first create a new request class
import { IsNotEmpty } from "class-validator";
class NameRequest {
@IsNotEmpty()
public name: string;
}
In this class, we are using the famous class-validator to decorate our request parameters. This way it is more clear how we are going to use our request object and everything.
Then install the following middleware dependency
npm i @lambda-middleware/class-validator
Then use it in the lambda like the following
import { classValidator } from '@lambda-middleware/class-validator'
import { APIGatewayEvent, APIGatewayProxyResult } from "aws-lambda";
import NameRequest from './NameRequest'
async function helloWorld(event: { body: NameRequest }): Promise<APIGatewayProxyResult> {
// body will have the name property here.
// No need to check
}
export const handler = classValidator()(helloWorld); // <-- see here
Using Multiple Middlewares
Now we have seen how our code can be improved with the introduction of middleware. What if we want to use 2 middleware at the same time?
Well, there is another package named compose just to do that. The idea is we get a function named composeHandler
and use that to aggregate multiple lambda middleware.
import { classValidator } from '@lambda-middleware/class-validator'
import { compose } from "@lambda-middleware/compose";
import { errorHandler } from "@lambda-middleware/errorHandler";
import { PromiseHandler } from "@lambda-middleware/utils";
import { APIGatewayEvent, APIGatewayProxyResult } from "aws-lambda";
import NameRequest from './NameRequest'
async function helloWorld(event: { body: NameRequest }): Promise<APIGatewayProxyResult> {
// body will have the name property here.
// No need to check
// also the errors will be handled automatically!
}
export const handler: ProxyHandler = compose(
errorHandler(),
classValidator({
bodyType: NameRequest
}))
)(helloWorld);
So this is how we can use multiple middlewares.
Improving this even more!
Now let’s say you are writing hundreds of lambda functions. and from the code, we can still see that we are calling the same compose
function inside every middleware.
can we reduce that? Let’s first create a utility function that will create a lambda function.
import { composeHandler } from '@lambda-middleware/compose';
import { errorHandler } from './error-handler-middleware';
import { classValidator } from './incoming-request-validator';
import { ClassType } from 'class-transformer-validator';
import {
APIGatewayEventDefaultAuthorizerContext,
APIGatewayEventRequestContext,
APIGatewayProxyEventStageVariables,
APIGatewayProxyResult,
Context
} from 'aws-lambda';
type LambdaRequestContext = {
environment: string;
authorizer: APIGatewayEventDefaultAuthorizerContext;
};
export const createLambdaHandler = <TRequest extends object, TResponse>(
requestModel: ClassType<TRequest>, // the request model
executeBusinessLogic: (request: TRequest, context: LambdaRequestContext) => Promise<TResponse> // the business logic
) => {
// here we are creating the wrapper for our actual business logic
const handlerWrapper = async (
event: {
body: TRequest;
stageVariables: APIGatewayProxyEventStageVariables | null;
requestContext: APIGatewayEventRequestContext;
},
context: Context
): Promise<APIGatewayProxyResult> => {
const response = await executeBusinessLogic( event.body, {
environment: event?.stageVariables?.Environment ,
authorizer: event?.requestContext?.authorizer
});
return {
headers: {},
isBase64Encoded: false,
multiValueHeaders: {},
statusCode: 200,
body: JSON.stringify(response)
};
};
return composeHandler(
errorHandler(),
classValidator({
bodyType: requestModel
}),
handlerWrapper
);
};
In this function, we are passing 2 things. The first parameter is the type of the request body for the lambda to be used by class-validator
and the second parameter is the actual business logic for the lambda.
Then we are calling the business logic inside the executeBusinessLogic
to call the actual logic and return the response.
Now we can use this function to create any lambda we want without the need to duplicate code.
import { createLambdaHandler } from './createLambdaHandler';
import { NameRequest } from './ NameRequest ';
export const handler = createLambdaHandler(NameRequest, async (request, context) => {
// only care about the business logic here
});
Final thoughts
Honestly finding out that we can use middleware just changed the game for me. It has improved the code quality to another level.
There are so many things that you can do with this concept. For example, you can write your own middleware too. Because at the end of the day they are just functions. So go ahead!
That’s it for today. Have a Great Day! :D
Have something to say? Get in touch with me via LinkedIn