Using Higher-Order Functions To Pass Data and Functions in Node
When using Node.js it is best practice to separate out one's code into modules. So, instead of having one monolithic index.js
or main.js
file, instead you have your routes and your utility modules separated out into separate files which then get imported into your main file. This is often pretty straightforward, but sometimes these modules need to have some data or instantiated functions from your main file before they can function property.
A Troublesome Scenario
If you use Express.js in Node, you know that it is extremely easy to write a route handler. For a smaller project, you might put all your routes in the main file. A barebones server with a single route could look like this:
// import express
const express = require('express');
// create your express app
const app = express()
// handle get requests to the root with an anonymous route handler function
.get('/', (req, res) => {
res.send('Hi!');
});
// start the server running
const server = app.listen(3300, () => {
console.log('App listening at port', server.address().port);
});
Now, if you were going to be handling more routes and building a bigger application, it makes sense to separate out the routes into separate modules. For me to separate out the previous route handler would look like this.
// routes/home.js
const homeHandler = (req, res) => {
res.send('Hi!');
};
module.exports = homeHandler;
// index.js
const express = require('express');
const homeHandler = require('./routes/home');
const app = express()
// handle get requests to the root with the homeHandler function
.get('/', homeHandler);
// start the server running
const server = app.listen(3300, () => {
console.log('App listening at port', server.address().port);
});
This is still very straightforward. But, the difficulty comes if you introduce something that needs to be instantiated once yet made available to imported modules.
Imagine that you instantiate your database in index.js
and need to access that from multiple of your routes. How can you pass the instantiated database driver out of your main file and into your imported modules?
The Wrong Way
The easiest way is also the worst way possible. If you wanted to, you could just throw your database driver onto the global object. The global
object in Node is like the window
object in the browser. It is universally available to every bit of code in the entire application.
This is dangerous in that it pollutes the global scope of your application. You could accidentally overwrite a variable in one of your dependencies without realizing it. There are countless reasons why this is NOT the way to go, but I'll just leave it at that. Do not do that! There is a better way.
A Better Way
The better way is to use higher-order functions. These are functions that either take a function as a parameter or return another function. An example of a function-returning higher-order function is this:
const myFuncWrapper = par1 => {
// par1 is available for use in here
const myFunc = par2 => {
// both par1 and par2 are available for use in here
}
return myFunc;
};
export myFuncWrapper;
In the above case, by wrapping myFunc
in another function, we are able to pass in an initial parameter (par1
) which is then available to all subsequent calls of myFunc
.
Because arrow functions will return whatever object or value follows them (in the absence of brackets), we can shorten the above example to this:
const myFunc = par1 => par2 => {
// both par1 and par2 are available for use in here
};
export myFunc;
The first time you call the function, you pass in par1
and then receive a function which you can call as many times as you like and it will always have access to that initial par1
parameter.
In the case of our express app, we want to pass our database driver into our routes and return a route handler function which is ready to use with express. Our route handler currently looks like this:
// routes/home.js
const homeHandler = (req, res) => {
res.send('Hi!');
};
module.exports = homeHandler;
What we are going to do is wrap that function in another function which when called will return the prepared route handler. We are going to pass our db object into the wrapper function to initialize the route handler. Then, when requests come in to the handler it will be able to respond with data (in this case, a list of names) from the db.
const homeHandler = db => (req, res) => {
db.find({}, (err, docs) => {
if (err) {
console.error(err);
} else {
const names = docs
.map(d => d.name)
.join();
res.send(names);
}
});
};
module.exports = homeHandler;
But, as it turns out I also want to have a centralized error handler which is used in all routes, so let's add that as a parameter as well.
// routes/home.js
const homeHandler = (db, handleError) => (req, res) => {
db.find({}, (err, docs) => {
if (err) {
handleError(err);
} else {
const names = docs
.map(d => d.name)
.join();
res.send(names);
}
});
};
module.exports = homeHandler;
Now we have our route handler module all ready to import into our main file.
// index.js
const express = require('express');
const homeHandler = require('./routes/home');
const Datastore = require('nedb');
// write error handler
const handleError = err => {
console.error(err);
};
// instantiate the database
const db = new Datastore();
const app = express()
// call homeHandler once with the parameters we need
// which will then return the express route handler
.get('/', homeHandler(db, handleError));
// start the server running
const server = app.listen(3300, () => {
console.log('App listening at port', server.address().port);
});
Conclusion
Higher-order functions are a great way to pass functions or data into imported modules. They make for clean and explicit code.