Declarative Decorating in AWS Lambdas

Sean McDowell
3 min readSep 17, 2020

Recently I set about creating my first API Gateway in AWS backed by Node lambda functions coming from spending the last few years developing Spring Boot APIs in Java. With a basic “hello world” project working I had two non functional requirements on my checklist before the API was production ready:

1. Authenticate requests with a JWT token
2. Implement tracking IDs for log aggregation

Both of these are requirements which in the OOP world fit nicely into a declarative style of programming. This allows separation of repeating boiler plate code from business logic. It’s worth noting AWS provide out of the box solutions for log aggregation and authentication but, for reasons I won’t delve into here, the out of the box solutions weren’t fit for purpose. I decided to leverage the functional style of programming Typescript allows and use composable higher order functions.

Here’s how the code progressed…

First off, the original “hello world” lambda


const handler: APIGatewayProxyHandler = async () => {
//business logic here
return { statusCode: 200, body: ‘hello world!’ };
};

Time to add authentication. First I added a “decorator” function type to reuse for any decorator implementations


export type CustomLambdaHandlerDecorator<THandler> = (
handlerToDecorate: THandler
) => THandler;

Next I created an authentication implementation with tests

export const authenticationDecorator: CustomLambdaHandlerDecorator<APIGatewayProxyHandler> = (
handlerToDecorate
) => {
return (
event: APIGatewayProxyEvent,
context: Context,
callback: Callback<APIGatewayProxyResult>
) => {

//code simplified for example
verifyToken(event.headers.Authorization);
return handlerToDecorate(event, context, callback);
};
};

And finally added the decorator to my handler


const helloWorldFunction: APIGatewayProxyHandler = async () => {
//business logic here
return { statusCode: 200, body: ‘hello world!’ };
};
const handler: APIGatewayProxyHandler = authenticationDecorator(
helloWorldFunction
);

Notice how the “aspect” like code is now separate from the business logic a real world example would include in the helloWorldFunction. This allows us to easily test the business logic separately in our unit tests.

Next I moved on to tracking IDs for our log aggregation. I created a decorator which added functionality to:

  • Parse a tracking id from a request header if found or generate a new guid as a tracking ID
  • Add this to a global context for use in logging and for subsequent requests, the global context allows us to grab the value where needed avoiding verbose parameter passing throughout the code
  • Return the tracking ID as a header on the response

Here’s the abbreviated code

export const trackingIdDecorator: CustomLambdaHandlerDecorator<APIGatewayProxyHandler> = (
handlerToDecorate
) => {
return async (
event: APIGatewayProxyEvent,
context: Context,
callback: Callback<APIGatewayProxyResult>
) => {
const trackingId = fetchOrGenerateId(event);
addToContext(trackingId);const response = await handlerToDecorate(event, context, callback) as APIGatewayProxyResult;response.headers = response.headers || {};
response.headers.trackingId = trackingId;
return response;
};
};

An interesting feature of this pattern is the ability to modify the lambda handlers response before returning it, demonstrated here by the addition of the trackingId header.

Now adding it to my hello word handler


const handler: APIGatewayProxyHandler = trackingIdDecorator(
authenticationDecorator(
helloWorldFunction
)
);

At this point you can see the functions compose nicely but I found nesting the functions wasn’t the nicest syntax, you can imagine how it might descend into a “flock of geese” type scenario. After some digging around online I found a nice solution, a “pipe” function which takes an array of decorators as a parameter, chains them together from left to right and returns the resulting decorator.

export const pipe = <THandler>(...decorators: CustomLambdaHandlerDecorator<THandler>[]): CustomLambdaHandlerDecorator<THandler> =>
(handler) => decorators.reduce((previousDecorator, currentDecorator) => currentDecorator(previousDecorator), handler);

Now I can decorate my lambda function like so


const handler: APIGatewayProxyHandler = pipe(
trackingIdDecorator,
authenticationDecorator
)(helloWorldFunction);

Overall I’ve found this pattern allows a declarative way to untangle repeating boiler plate code from business logic, allowing for enhanced readability and testability.

--

--