DEV Community

Cover image for Elevating Your AWS Lambda Game With Middy And TypeScript
Matteo Depascale for AWS Community Builders

Posted on • Originally published at cloudnature.net

Elevating Your AWS Lambda Game With Middy And TypeScript

Introduction

In the ever-evolving landscape of AWS Lambda development, efficiency is key. Picture this: you're building serverless functions, but the process feels a bit like assembling a puzzle without all the pieces. That's where Middy steps in as the missing link.

AWS Lambda, while revolutionary, can pose challenges in terms of code organization, reusability, and overall development simplicity. Without the right tools, navigating these challenges might feel like traversing uncharted territory.

Enter Middy, the middleware framework designed to streamline and enhance AWS Lambda functions. Middy acts as your development ally, addressing the pain points of Lambda development with its middleware architecture. In this blogpost you'll discover how Middy turns these challenges into opportunities for a more efficient and enjoyable serverless development experience.

⚠️ This is the second part of the series focused on AWS Lambda and serverless. You don't need the first part, but if you are here you will probably like it. Here is the link:https://cloudnature.net/blog/nextlevel-serverless-development-with-sst-and-typescript.


Setting the Stage

Lambda execution environment lifecycle

AWS Lambda functions, the backbone of serverless computing, offer scalable and cost-effective execution. Before diving into the power of Middy, it's crucial to grasp the fundamentals of Lambda functions.

AWS Lambda allows you to run code without provisioning or managing servers, responding to events while automatically managing compute resources. You write the code, AWS handles the rest. Fantastic, right? However, like any tech, there are a few aspects to be mindful of.

Here's a simple handler code snippet:

export const handler = async (event, context) => {
  console.log("EVENT: \n" + JSON.stringify(event, null, 2));
  return context.logStreamName;
};
Enter fullscreen mode Exit fullscreen mode

Lambda functions respond to various events: S3, SNS, SQS, HTTP from API Gateway, and more. In this code we are printing the event that the AWS Lambda function handled.
Lambda development isn't always a smooth sail. Finding and addressing these challenges is crucial for achieving a robust and efficient serverless architecture. Let's explore challenges:

  • Input Logging: debugging based on received events
  • Output Logging: ensuring the response is correct
  • Body Parsing: yep, JSON.parse(event.body) for HTTP events
  • HTTP Body Response: parsing responses with statusCode and message
  • CORS Headers: adding them in HTTP response
  • Error Handling: ah, the inevitable pain point (I can feel you pain)
  • Event Validation: ensuring events meet expected criteria
  • Event Body Typing: especially in TypeScript, right?

Solving these challenges often involves developing your Lambda handler wrapper: wrapping in try...catch, logging events, handling errors, and reshaping responses. Well, I did all of this until I stumbled upon THE middleware solution: Middy.


Introducing Middy

How Middy works

Middy, a game-changer in Lambda development, introduces a middleware architecture that streamlines your serverless journey. Think of middleware as a series of functions that intercept the flow of data between the initial request and the final response. In the context of AWS Lambda, Middy seamlessly integrates into the execution flow, allowing you to augment, modify, or validate both the incoming events and outgoing responses.
Each middleware handles a specific concern, contributing to a modular and maintainable codebase. This design facilitates the addition or removal of middleware based on your specific requirements.

Let's revisit some of the challenges we identified earlier and discover how Middy rises to the occasion:

  • Input Logging: Middy has a dedicated middleware for that.
  • Output Logging: There's a middleware designed to handle that seamlessly.
  • Body Parsing: Yes, you guessed it, there's a middleware for parsing HTTP event bodies.
  • HTTP Body Response: Once again, Middy steps in with a dedicated middleware.

Now that we understand the workings of AWS Lambda and the capabilities Middy brings to the table, let's explore how we can combine them to overcome real-world challenges in serverless development.


Getting Started with Middy and TypeScript

Getting Middy up and running is a breeze. Follow these simple steps to integrate Middy into your AWS Lambda environment:

npm install --save @middy/core
npm install --save-dev @types/aws-lambda
Enter fullscreen mode Exit fullscreen mode

For typescript I found this issue, which is literally what we need (after a few twiks πŸ”¨)
πŸ”— https://github.com/middyjs/middy/issues/316

Our Middyfy wrapper sets the stage, providing a structured approach to typing events and handling requests. Now, let's extend it to accommodate essential Middy middleware:

import middy, { type MiddyfiedHandler } from '@middy/core'
import type { APIGatewayProxyEvent, APIGatewayProxyResult, Context, Handler as AWSHandler, SNSEvent } from 'aws-lambda'
import { type Entity } from 'dynamodb-onetable'
import { type OneField, type Paged } from 'dynamodb-onetable/dist/mjs/Model'

// Event is an APIGatewayProxyEvent with a typed body, pathParameters and queryStringParameters which depends on http-json-body-parser & json-schema-to-ts
// queryStringParameters and multiValueQueryStringParameters is non-nullable as we use http-event-normalizer
export interface Event<TBody, TPathParameters, TQueryStringParameters>
  extends Omit<APIGatewayProxyEvent, 'body' | 'pathParameters' | 'queryStringParameters'> {
  waitTimestamp: string | number | Date
  body: TBody
  pathParameters: TPathParameters
  queryStringParameters: TQueryStringParameters
  multiValueQueryStringParameters: NonNullable<APIGatewayProxyEvent['multiValueQueryStringParameters']>
}

// We are making use of http-response-serializer, so our body type can either be an Entity, an Array<Entity> or a string
interface Result extends Omit<APIGatewayProxyResult, 'body'> {
  body:
  | Entity<Record<string, OneField>>
  | Paged<Entity<Record<string, OneField>>>
  | string
  | Record<string, unknown>
}

// Handler type which gives us proper types on our event based on TBody and TPathParameters which are JSON schemas
export type Handler<TBody = void, TPathParameters = void, TQueryStringParameters = void> = AWSHandler<
Event<TBody, TPathParameters, TQueryStringParameters>,
Result
>

interface RequestSchema {
  properties?: {
    body?: Record<string, unknown> | null
    pathParameters?: Record<string, unknown> | null
    queryStringParameters?: Record<string, unknown> | null
  }
}

export const middyfy = (
  handler: Handler<never, never, never>,
  requestSchema: RequestSchema | null = null
): MiddyfiedHandler<Event<never, never, never>, Result, Error, Context> => {
  const wrapper = middy(handler)

    // Attach desired Middy middlewares here
  wrapper.use(/* middleware 1 */);
  wrapper.use(/* middleware 2 */);
  // ... add more as needed

  return wrapper
}
Enter fullscreen mode Exit fullscreen mode

Dive into the Middy middleware ecosystem and choose the ones that align with your Lambda function's requirements. Whether it's input logging, output logging, handling HTTP responses... Middy has you covered.

And this is our sample lambda function:

import { middyfy, type Handler } from '@core/libs/middyWrapper'
import type { FromSchema } from 'json-schema-to-ts'

export const bodySchema = {
  type: 'object',
  properties: {
    subject: { type: 'string', maxLength: 100 },
    content: { type: 'string', maxLength: 100 }
  },
  required: ['subject', 'content'],
  additionalProperties: false
} as const

export const schema = {
  type: 'object',
  properties: {
    body: bodySchema
  }
} as const

const main: Handler<FromSchema<typeof bodySchema>, void, void> = async (event) => {
  return {
    statusCode: 200,
    body: { ...event }
  }
}

export const handler = middyfy(main, schema)
Enter fullscreen mode Exit fullscreen mode

Did you copy everything? Perfect, because now we are going to add middlewares and get our hands dirty 😈


Leveraging Middy's Middleware

Ready for the exciting part? Follow these simple steps to introduce essential middlewares (not all of them, there are a lot) and witness the transformation.

Let's start by installing some middlewares. Open your terminal and run:

npm install @middy/http-error-handler
npm install @middy/http-json-body-parser
npm install @middy/http-response-serializer
npm install @middy/validator
npm install @middy/validator/transpile
Enter fullscreen mode Exit fullscreen mode

Now, import them into your Middyfy wrapper code:

import httpErrorHandlerMiddleware from '@middy/http-error-handler'
import httpJsonBodyParserMiddleware from '@middy/http-json-body-parser'
import httpResponseSerializerMiddleware from '@middy/http-response-serializer'
import validatorMiddleware from '@middy/validator'
import { transpileSchema } from '@middy/validator/transpile'
Enter fullscreen mode Exit fullscreen mode

Let's break it down one middleware at the time:

  1. http-json-body-parser
wrapper
      .use(httpJsonBodyParserMiddleware())
Enter fullscreen mode Exit fullscreen mode
  1. validator: which checks if the event is compliant with the schema we specified in out lambda function
  if (requestSchema != null) {
    wrapper.use(validatorMiddleware({ eventSchema: transpileSchema(requestSchema) }))
      .use({
        onError: (request) => {
          const response = request.response
          const error = request.error as any
          if (response.statusCode === 400) {
            response.headers['Content-Type'] = 'application/json'
            response.body = JSON.stringify({ message: response.body, validationErrors: error.cause })
          }
        }
      })
  }
Enter fullscreen mode Exit fullscreen mode
  1. http-error-handler
wrapper
      .use(httpErrorHandlerMiddleware({}))
Enter fullscreen mode Exit fullscreen mode
  1. http-response-serializer
wrapper
      .use(
      httpResponseSerializerMiddleware({
        serializers: [
          {
            regex: /^application\/json$/,
            serializer: ({ body }) => JSON.stringify(body)
          }
        ],
        defaultContentType: 'application/json'
      })
    )
Enter fullscreen mode Exit fullscreen mode

With less than 100 lines of code, your Lambda functions are now robust and production-ready.


Considerations

Logs for input and output Middy middleware
Various middlewares showcased in the image above illustrates the power of Middy in action. From logging input and output to enhancing security headers and serializing responses, Middy simplifies complex processes into an elegant solution.

Having incorporated Middy into the production environment for my newsletter, I've witnessed a noticeable improvement in debugging and monitoring. The once complex task has become more streamlined and less prone to messy complications.

A particularly noteworthy aspect is the ability to use a unified interface for input validation and TypeScript typing. This seemingly simple feature significantly elevates the developer experience.

I really like the idea, and I plan to use Middy more in my future projects. Now that I have a new and ready-to-use library for my Middy wrapper, it's even more exciting! 🀩


Future Trends

The future of AWS Lambda development with Middy holds more and more possibilities as the Serverless adoption goes on. The increasing adoption of serverless architecture has attracted attention, leading to AWS and FourTheorem sponsorship which signifies the growing significance of Middy in the serverless ecosystem.

As serverless computing continues to gain traction, more developers are embracing the simplicity and efficiency it offers. This surge in popularity implies a broader user base for Middy, making it a go-to choice for Lambda development.

The growing user base is likely to result in increased community contributions. This influx of developers brings diverse perspectives and ideas, which may lead to create new and innovative middlewares. Expect a richer ecosystem with a wide array of middleware options for various use cases.

While there is already a lot of HTTP-related middlewares, the future holds the promise of an expanded selection for different AWS services.

Hopefully Middy becomes an integral part of the serverless development landscape, it really deserve it ✌️


Conclusion

In closing, Middy emerges as a game-changer in the world of AWS Lambda development. As we explored Lambda, we encountered challenges like logging and error handling. Middy steps in to effortlessly tackle these hurdles with its modular middleware approach.

Middy isn't just a middleware, it's a tool that simplifies AWS Lambda complexities, taking serverless development to a whole new level.

And there you have it, folks! Do I have your attention now? Get ready because the next article will delve into how I developed my Serverless newsletter infrastructure with SST and Middy (plus a really big Step Function) πŸ’ͺ

If you enjoyed this article, please let me know in the comment section or send me a DM. I'm always happy to chat! ✌️

Thank you so much for reading! πŸ™ Keep an eye out for more AWS-related posts, and feel free to connect with me on LinkedIn πŸ‘‰ https://www.linkedin.com/in/matteo-depascale/.


References

Disclaimer: opinions expressed are solely my own and do not express the views or opinions of my employer.

Top comments (3)

Collapse
 
dvddpl profile image
Davide de Paolis

middy is awesome. why reinventing the wheel and write over and over again the same code in every lambda? by using those Middleware our code is lean and simple and we can focus only on what matters.

Collapse
 
mdrijwan profile image
Md Rijwan Razzaq Matin

Whatever you’re doing with middy here are easily achievable without using middy which eliminates installing so many external modules that unnecessarily makes the package heavier. If your lambda doesn’t have lot of handlers, using any external modules seems useless in my opinion.

Collapse
 
depaa profile image
Matteo Depascale

That's a really great opinion, I think it's the eternal dilemma right? If you want to bring your own utilities, you can do just that. Sometime people don't have time to create their utilities, and they want something they can use to enhance their Lambda development experience. And I think Middy does just that.

For example, one small thing that comes to my mind is the ability to validate the event body and have an interface to use, both while defining the schema in one single point

Image description

Other than that, you are importing less than 250KB (235KB are for the validator because it uses Ajv).
So yeah, I agree with you that you can use your own utilities, and sometimes, other options, like Middy, may come handy too