DEV Community

Cover image for Implement middleware pattern in Azure Functions
Emanuel Casco
Emanuel Casco

Posted on • Edited on

Implement middleware pattern in Azure Functions

Introduction

I wrote this post to share my experience implementing middleware pattern on Azure Functions, a serverless compute service that enables you to run code on-demand without having to explicitly manage infrastructure.

Biggest advantage of serverless computing is that you can focus on building apps and don’t worry about provisioning or maintaining servers. You can write code only for what truly matters to your business.

However in real world applications you have to deal with some common technical concerns outside business logic, like input parsing and validation, output serialization, error handling, and more. Very often, all this necessary code ends up polluting the pure business logic code in your functions, making the code harder to read and to maintain.

Web frameworks, like Express or Hapi, has solved this problem using the middleware pattern. This pattern allows developers to isolate these common technical concerns into “steps” that decorate the main business logic code.

After deciding to implement this pattern on the project I were working on, I made a small search to check if someone had already implemented a similar solution. Unfortunately, the few solutions I found didn‘t meet my needs.

The solution

After checking that there was no already-implemented solution that meets my needs, I decided to create my own solution. That is how Azure-Middleware was born.

GitHub logo emanuelcasco / azure-middleware

Node.js middleware engine for Azure Functions 🔗

Azure Middleware Engine 🔗

Azure Middleware Engine is developed inspired in web framworks like express, fastify, hapi, etc. to provide an easy-to-use api to use middleware patter in Azure Functions.

But, less talk and let see some code.

For example:

// index.js
const { someFunctionHandler } = require('./handlers');
const schema = require('../schemas');
const ChainedFunction = new MiddlewareHandler()
   .validate(schema)
   .use(someFunctionHandler)
   .use(ctx => {
      Promise.resolve(1).then(() => {
         ctx.log.info('Im called second');
         ctx.next();
      });
   })
   .use(ctx => {
      ctx.log.info('Im called third');
      ctx.done(null, { status: 200 }

Implementation

Input validation

In serverless arquitectures is essential to be able to determine the correct behavior of each function as separate pieces of code. Therefore, in order to avoid unexpected behaviors, is important ensure that function inputs belong to its domain.

To accomplish this mission Azure-Middleware uses Joi. It allows us to define a schema and check if the input message is valid or not.

With the validate method you can define the scheme that will be used to validate the messages. If your function is called with an invalid message then an exception will be thrown and your function won’t be executed.

module.exports = new MiddlewareHandler()
   .validate(invalidJoiSchema)
   .use(functionHandler)
   .catch(errorHandler)
   .listen();

Function handlers chaining

use method is used to chain different function handlers, or middlewares, as “steps”. It expect a function handler as argument.

Each middleware is executed sequentially in the order in which the function was defined. Information flow passes to the next element of the chain when calling context.next.

module.exports = new MiddlewareHandler()
   .validate(schema)
   .use((ctx, msg) => {
      ctx.log.info('Print first');
      ctx.next();
   })
   .use((ctx, msg) => {
      ctx.log.info('Print second');
      ctx.done();
   })
   .catch(errorHandler)
   .listen();

next is a method injected into context. It is used to iterate the middlewares chain.

Error handling

Error handling is very similar as it works in web frameworks like Express. When an exception is thrown, the first error handler into the middlewares chain will be executed. While all function handlers before will be ignored.

Also, you can jump to the next error handler using next. If this method receives an argument as first argument then it will be handled as an error.

Also, you can jump to the next error handler using context.next. If this method receives a non-nil value as first argument, it will be handled as an error.

Unlike the function handlers, the error handlers receive an error as the first argument.

module.exports = new MiddlewareHandler()
   .use((ctx, msg) => {
      ctx.log.info('Hello world');
      ctx.next('ERROR!');
   })
   .use((ctx, msg) => {
      ctx.log.info('Not executed :(');
      ctx.next();
   })
   .catch((error, ctx, msg) => {
      ctx.log.info(errors); // ERROR!
      ctx.next();
   })
   .listen();

Wrap up

The package is still in development and I have some ideas to improve it. However, if you have any suggestion, please don’t doubt in contact me and let me know about it!

Thanks for reading. If you have thoughts on this, be sure to leave a comment.


You can follow me on Twitter, Github or LinkedIn.

Link to my original post on Medium.

Top comments (0)