Reusable Express Handlers

To handle HTTP requests, Express routing functions require a path to a resource and a callback function. This callback function (also known as “controller”) is called passing the Express request and response objects. These objects - usually aliased as req and res - hold things like headers, parameters and metadata from the initial HTTP request.

1
2
3
4
5
6
7
const app = express()
app.get(`/api/users/`, function handlerFunction(req, res) {
// ...
res.send(/* Result */)
})

Ok, what about it?

Handler functions can’t be reused on other parts of the application. These functions are only prepared to deal with the Express objects (req and res) but that is something specific to the current request and we don’t have control of it.

To illustrate, think of this route for updating existing users:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const app = express()
app.post(`/api/users/:id`, async function updateUser(req, res) {
const user = req.body
const id = req.params.id
const ip = req.ip
// Some logic down here
validateUser(user)
user.updatedAt = new Date()
user.updatedFromIP = ip
if(user.picture){
await saveProfilePicture(user)
}
const updatedUser = await userModel.update(id, user)
// Ends the request
res.send(updatedUser)
})

Even if we isolated and exported the updateUser handler function (export default { updateUser }) it would still be expecting the req and res objects when we called it somewhere else across our application.

This means that the function can only be used there (to handle that specific POST request) and nowhere else.

Of course I won’t consider forging fake objects with properties similar to req and res because that would be very hackish. We certainly can do better!

Reusing Handler Functions

Solving this problem should start with a single goal: abstract anything HTTP-related from the request. Things like req.body, req.query, req.ip and others parameters must still be accessible to handler functions but we need to have control over them. Repeating res.send() on every handler function also cannot happen, otherwise we would be leaking the abstraction.

We will start by creating a brand new module. It will be a wrapper function to our handler functions. I’ll call this module http because that is the thing we are abstracting away from the handlers (I do admit it could use a better name, drop me a line if you think of a good one!).

Let’s build the http module piece by piece since the end result might not be very straightforward at a first glance. This is the start code for our module:

1
2
3
4
5
6
7
// http.js
function http (handlerFunction) {
// More code to follow...
}
export default http

The http module will be used right where Express expects a callback function to handle the req and res objects:

1
2
3
4
5
// controllers/user.js
import 'http' from 'helpers/http'
app.post(`/api/users`, http(updateUser))

So let’s give him one:

1
2
3
4
5
6
7
8
9
// http.js
function http (handlerFunction) {
// Return a handler format function
return function (req, res, next){
// ...
}
}
export default http

Now here is the part where we abstract things from the HTTP request - body, url parameters, query string, hostname, session data, etc.

We will merge everything we need from the req and res objects into a single object called options.

The only thing we’ll want to keep separate is the req.body object. Since the data in this object is usually validated or saved in a database, modifying it is not a good idea.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// http.js
function http (handlerFunction) {
return function (req, res, next){
// We don't want to merge req.body with
// other objects since this data is usually
// validated, saved in a DB, etc.
const obj = req.body
// The options object contains all the data we need from
// the req and res objects. This depends a bit on what
// API you're building but the ones below are very common:
const options = Object.assign(
{},
req.query, // query string parameters
req.params, // url parameters
{ session: req.session } // the session (used on stateless authentication)
)
// ...
}
}
export default http

We could do const options = req but that would pollute the options object too much. Just take what you want from it. You’ll be able to add more fields in the future.

The next step now is to call our handler function with the objects we’ve just created:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// http.js
function http (handlerFunction) {
return function(req, res, next){
const obj = req.body
const options = Object.assign(
{},
req.query,
req.params,
{ session: req.session }
)
// Call the received handler function
return handlerFunction(obj, options)
// Ends the request with the result (or a empty)
// object if the handler function did not return
// a value.
.then(result => res.send(result || {}))
// If something goes wrong, we will leave it to
// the next middleware to handle
.catch(next)
}
}
export default http

Ok, but what about GET and DELETE requests where req.body is usually empty? In the code we’ve built so far, functions that handle GET and DELETE requests would have a null value as the first parameter.

We can do a small tweak to send the data from options as the first parameter if req.body is null:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// http.js
function http(handlerFunction) {
return function(req, res, next) {
let obj = req.body
let options = Object.assign(
{},
req.query,
req.params,
{ session: req.session }
)
// req.body should be null for
// GET and DELETE methods so we
// use "options" as the first parameter
if (!obj) {
obj = options
options = {}
}
return handlerFunction(obj, options)
.then(result => res.send(result || {}))
.catch(next)
}
}
export default http

And here is a final snippet showing how the updateUser example could be written on the controller using the http module:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// controllers/user.js
import 'http' from 'helpers/http'
app.post(`/api/users`, http(updateUser))
async function updateUser(user, options){
const { id, ip } = options
validateUser(user)
user.updatedFromIp = ip
if(user.picture){
await saveProfilePicture(user)
}
const updatedUser = await userModel.update(id, user)
return updatedUser
}
// The function can be reused by other
// parts of the application.
// Now it makes sense to export it!
export default { updateUser }

Now the function is easily reusable across the app. We could reuse it on different controllers, for example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// controllers/auth.js
import 'http' from 'helpers/http'
import { updateUser } from 'controllers/user'
app.post(`/logout`, http(logout))
// Say we needed to store the date of
// signout from users at logout time
async function logout(credentials, options){
// ...
await updateUser({
id: options.session.userId,
lastSignOutDate: new Date()
}, options)
// ...
}
// ...

Conclusion

Abstracting away HTTP-related - and also Express-related - data from requests has its benefits:

  • Handler functions are easier to test;
  • Handler functions can be used as helpers to write tests;
  • Less boilerplate code is written (eg. res.send()-ing and next(err)-ing things on every request);
  • You can easily switch from Express to another framework (eg. Koa or Restify) just by modifying the wrapper.

The http moduled allowed us to do just that with very little code 🎉.




By Ruan Martinelli