DRY Principle in Your AWS SAM Application with Middlewares

Mohammad Faisal - Nov 28 '23 - - Dev Community

To read more articles like this, visit my blog

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. 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 of the 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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
    }
};
Enter fullscreen mode Exit fullscreen mode

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;

}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
    );
};
Enter fullscreen mode Exit fullscreen mode

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 
});
Enter fullscreen mode Exit fullscreen mode

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 or Personal Website

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .