Middy.js

A powerful Node.js middleware framework for your lambdas 

Luciano Mammino (@loige)

29/01/2020, Dublin

👋 Hello, I am Luciano!

Fullstack Dev & Cloud Architect

Blog: loige.co

Twitter: @loige

GitHub: @lmammino 

What is serverless

Compute as functions (FaaS)

Event-based model

Managed resources

Why serverless is good

Focus on business logic

Scalability

Pay-per-use model (compute time * memory)

Forces you to think micro-services

My Serverless experience

Open source

Enterprise

  • Trading, Billing engine, Market data aggregator solutions (ESB)
  • Big data pipeline to make network metadata searchable (Vectra.ai)
  • Organised various workshops around Europe (Serverlesslab.com)

💁‍♀️ DISCLAIMER

 

Serverless is more than just FaaS

 

... but here we will focus mostly on AWS Lamda

👩‍🏫 Quickest intro to Lambda (ever)!

📄

code

⚡️

event

💰

profit

=

* real life coding with Lambda is a little bit more complicated™️ than this...

exports.myLambda = async function (
    event,
    context
) {

  // get input from event and context

  // return output or throw an error
}

Anatomy of a Node.js lambda on AWS

exports.myLambda = async function (event, context) {
  const name = event.queryStringParameters.name
  const message = `Hello ${name || 'World'}!`
  
  return ({
    statusCode: 200,
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ message })
  })
}

Hello World!

exports.MyLambda = async function (event, context) {
  // decrypt environment variables with KMS
  // deserialize the content of the event
  // validate input, authentication, authorization
  
  // REAL BUSINESS LOGIC
  // (process input, generate output)
  
  // validate output
  // serialize response
  // handle errors
}

Real World  vs Hello World

LOTS of BOILERPLATE 😓

🙇‍♀️

In a real project with many lambdas,
we might end up with a lot of code duplication!

🤔

Can we avoid a bit of clutter by separating "pure business logic" from the "boilerplate"?

A solution...

npm install @middy/core

Note: using version 1.0.0 (beta)

Give it moar love 😍

Usage

const middy = require('@middy/core')
const { middleware1, middleware2, middleware3 } = require('some-middlewares')

const originalHandler = async (event, context) => {
  /* your business logic */
}

const handler = middy(originalHandler)

handler
  .use(middleware1())
  .use(middleware2())
  .use(middleware3())

module.exports = { handler }

1. define handler

2. "middify"  the handler

3. attach middlewares

4. export "middyfied" handler

Usage

const middy = require('@middy/core')
const { middleware1, middleware2, middleware3 } = require('some-middlewares')

const originalHandler = async (event, context) => {
  /* your business logic */
}

const handler = middy(originalHandler)

handler
  .use(middleware1())
  .use(middleware2())
  .use(middleware3())

module.exports = { handler }

Business logic

Boilerplate

const middy = require('@middy/core')
const urlEncodedBodyParser = require('@middy/http-urlencode-body-parser')
const validator = require('@middy/validator')
const httpErrorHandler = require('@middy/http-error-handler')

const processPaymentHandler = async (event, context) => {
  const { 
    creditCardNumber, expiryMonth, expiryYear, cvc, nameOnCard, amount
  } = event.body
  
  // do stuff with this data ...
  
  return ({
    statusCode: 200,
    body: 'payment processed correctly'
  })
}

const inputSchema = {
 // define validation schema here ...
}

const handler = middy(processPaymentHandler)
  .use(urlEncodedBodyParser())
  .use(validator(inputSchema))
  .use(httpErrorHandler())

module.exports = { handler }

Handler
(business logic)

Attach middlewares

Export enhanced handler

Dependencies

Define the input schema
(uses JSON-schema)

Why?

  • Simplify code
  • Reusability
    • input parsing
    • input & output validation
    • output serialization
    • error handling...
  • Focus (even) MORE on business logic
  • Testability

How it works

Execution order

  1. middleware1 (before)
  2. middleware2 (before)
  3. middleware3 (before)
  4. handler
  5. middleware3 (after)
  6. middleware2 (after)
  7. middleware1 (after)

When an error happens...

  • Flow is stopped
  • First middleware implementing `onError` gets control
  • It can choose to handle the error, or delegate it to the next handler
  • If the error is handler a response is returned
  • If the error is not handled the execution fails reporting the unhandled error

Writing a middleware

const myMiddleware = (config) => {
  
  // might set default options in config
  
  return ({
    before: (handler, next) => {
      // might read options from `config`
    },
    after: (handler, next) => {
      // might read options from `config`
    },
    onError: (handler, next) => {
      // might read options from `config`
    }
  })
}

module.exports = myMiddleware

Inline middlewares

const middy = require('@middy/core')

const handler = middy((event, context, callback) => {
  // do stuff
})

handler.before((handler, next) => {
  // do something in the before phase
  next()
})

handler.after((handler, next) => {
  // do something in the after phase
  next()
})

handler.onError((handler, next) => {
  // do something in the on error phase
  next()
})

module.exports = { handler }

In summary

  • Serverless is cool, it helps you to build apps quickly and with a greater focus on business logic, rather than on infrastructure!

  • Middy helps you to keep focusing on your business logic first

  • You can add extra behaviours with very minimal changes to your core logic by introducing dedicated middlewares

  • You can easily share common functionality through middlewares

If you like Middy

Your turn...

THANKS!

A special thank you to all the amazing Middy users and contributors!