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.
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 intocontext
. 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)