Functional middleware

How to use Maybe Monad to make your server middleware a little bit easier to reason about.

I had an Express server that was processing events from a payment system. For example, if a subscription was changed by the user, the payment system would send an event, which my server would process. The first step in every controller function was to take apart the request object to extract and check input parameters. If a parameter was missing or incorrect, the middleware would print a console message and return an error HTTP code. Otherwise everything would be ok, and the server would do something.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const R = require('ramda')
const onSubscriptionCreated = (req, res) => {
// use Ramda to safely get deep property
const customer = R.path(['body', 'content', 'customer'], req)
if (!customer) {
console.error('Missing customer in content', req.body)
return res.sendStatus(400)
}
// ... more checks like that

// parameters are good
// implement actual internal logic using Customer model ORM object
Customer.update(customer)
.then(() => res.sendStatus(200)) // all good
// something is wrong, send error
.catch(() => res.sendStatus(500))
}

The same parameter logic was everywhere - checking the request object to have body.content.customer object (and all other checks) was in every middleware function. Unless I called every controller path with every combination of valid and invalid parameters, I could not get close to 100% of code coverage in my middleware tests.

Usually I would extract common code like that into its own function. But in this case the parameter check also does control actions. If a parameter is invalid, the code

  • sends response status to the caller using res.sendStatus(400)
  • returns from the middleware function early

I could not simply factor this code out

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const R = require('ramda')
const getArguments = (req, res) => {
// use Ramda to safely get deep property
const customer = R.path(['body', 'content', 'customer'], req)
if (!customer) {
console.error('Missing customer in content', req.body)
// hmm, this just returns from this function
// not from the caller
return res.sendStatus(400)
}
// ... more checks like that
return {customer}
}
const onSubscriptionCreated = (req, res) => {
// hmm, this does not work!
const {customer} = getArguments(req, res)

// parameters are good
// implement actual internal logic using Customer model ORM object
Customer.update(customer)
.then(() => res.sendStatus(200)) // all good
// something is wrong, send error
.catch(() => res.sendStatus(500))
}

My utility function getArguments needs to do both things: it should extract parameter, and "flag" the status. Then the caller would know - where all parameters good or not? I could implement the check in the caller using my own check, for example, by checking for null

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const getArguments = (req, res) => {
// use Ramda to safely get deep property
const customer = R.path(['body', 'content', 'customer'], req)
if (!customer) {
console.error('Missing customer in content', req.body)
return null
}
// ... more checks like that
return {customer}
}
const onSubscriptionCreated = (req, res) => {
const params = getArguments(req, res)
if (!params) {
return res.sendStatus(400)
}
const {customer} = params
// all good
}

Nice, but what if null is an allowed value? We need something "standard" to avoid reinventing this wheel again and again. In functional programming encoding additional information around a value means we should return some "box" type that keeps the value and allows to use the value in a standard way. What do we need in our case?

  • if parameters are good, then we need to call our Customer.update(customer)... code
  • if parameters are bad, we need to call res.sendStatus(400)

There are only two possibilities and this type is commonly called Maybe. I can use an implementation from folktale. My getArguments function will signal that all parameters are good by returning Maybe.Just object. If something is invalid, it will return Maybe.Nothing

1
2
3
4
5
6
7
8
9
10
11
const Maybe = require('folktale/maybe')
const getArguments = (req, res) => {
// use Ramda to safely get deep property
const customer = R.path(['body', 'content', 'customer'], req)
if (!customer) {
console.error('Missing customer in content', req.body)
return Maybe.Nothing()
}
// ... more checks like that
return Maybe.Just({customer})
}

Super, the caller now "knows" that it should handle both cases, and there is an easy way to do this - by matching the returned type, almost like a switch statement using Maybe.matchWith method

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const onSubscriptionCreated = (req, res) => {
// little callback function that is only called
// with good parameters
const handleSub = ({ customer }) => {
Customer.update(customer)
.then(() => res.sendStatus(200)) // all good
// something is wrong, send error
.catch(() => res.sendStatus(500))
}
// attach handlers to each type that is in Maybe
// Maybe.Nothing and Maybe.Just
getArguments(req).matchWith({
Nothing: () => res.sendStatus(400),
Just: handleSub
})
}

Perfect, all our input checking logic is refactored into a single function, and we are using "standard" Maybe logic to continue processing based on the returned result. Our middleware is a little bit cleaner now, with each function doing only its job and not mixing input checks with sending a response for example.

Other libs

I like using folktale, but similar Maybe implementation can be found in pretty much every functional JavaScript (and other languages) library

More work

We have only split parameter extraction from acting on them. But we could do more work to make code cleaner and more reliable

  • instead of Maybe return actual results of input argument validation (which could have multiple error messages), usually called Result
  • separate parameter checks (which is a pure operation) from printing error messages (which is a side effect)
  • use Task instead of Promise in Customer.update(customer) to make implementation pure