Abstraction - Express.js Guide: The Comprehensive Book on Express.js (2014)

Express.js Guide: The Comprehensive Book on Express.js (2014)

Abstraction

Middleware concept provides flexibility. Software engineers can use anonymous or named functions as middlewares. However, the best thing is to abstract them into external modules based on functionality. Let’s say we have a REST API with stories, elements and users. We can separate request handlers into files accordingly, so that routes/stories.js has:

1 module.exports.findStories = function(req, res, next) {

2 ...

3 };

4 module.exports.createStory = function(req, res, next) {

5 ...

6 };

The routes/users.js holds the logic for user entities:

1 module.exports.findUser = function(req, res, next){

2 ...

3 };

4 module.exports.updateUser = function(req, res, next){

5 ...

6 };

7 module.exports.removeUser = function(req, res, next){

8 ...

9 };

The main app file can use the modules above in this manner:

1 ...

2 var stories = require('./routes/stories');

3 var users = require('./routes/users');

4 ...

5 app.get('/stories', stories.findStories);

6 app.post('/stories', stories.createStory);

7 app.get('/users/:user_id', users.findUser);

8 app.put('/users/:user_id', users.updateUser);

9 app.del('/users/:user_id', users.removeUser);

10 ...

In the example with var stories = require('./routes/stories');, stories is a file stories.js with an omitted (optional) .js extension.

Please notice that the same thing repeats itself over and over with each line/module, i.e., developers have to duplicate code by importing the same modules (e.g., users). Imagine that we need to include these three modules in each file! To avoid this, there’s a clever way to include multiple files — put the index.js file inside the stories folder and let that file include all the routes.

For example, require('./routes').stories.findStories(); will access index.js with exports.stories = require('./find-stories.js') which in turn reads find-stories.js with exports.findStories = function(){...}.

For a working example, you can run node -e "require('./routes-exports').stories.findStories();" from the expressjsguide folder to see a string output by console.log from the module:

The importing of modules via a folder and index.js file.

The importing of modules via a folder and index.js file.

To illustrate another approach of code reuse, assume that there’s an app with these routes:

1 app.get('/admin', function(req, res, next) {

2 if (!req.query._token) return next(new Error('no token provided'));

3 }, function(req, res, next) {

4 res.render('admin');

5 });

6 //middleware that applied to all /api/* calls

7 app.use('/api/*', function(req, res, next) {

8 if (!req.query.api_key) return next(new Error('no api key provided'));

9 });

Wouldn’t it be slick to have something like a function that returns a function instead:

1 var requiredParam = function (param) {

2 //do something with the param, e.g.,

3 //create a private attribute paramName based on the value of param variable

4 var paramName = '';

5 if (param === '_token') paramName = 'token';

6 else if (param === 'api_key') paramName = 'API key'

7 return function (req,res, next) {

8 //use paramName, e.g.,

9 //if query has no such parameter, proceed next() with error using paramName

10 if (!req.query[param]) return next(new Error('no ' + paramName +' provided'));

11 next();

12 });

13 }

14

15 app.get('/admin', requiredParam('_token'), function(req, res, next) {

16 res.render('admin');

17 });

18 //middleware that applied to all /api/* calls

19 app.use('/api/*', requiredParam('api_key'));

This is a very basic example, and in most cases, developers don’t worry about mapping for proper error text messages. Nevertheless, you can use this pattern for anything like restricting output, permissions, and switching between different blocks.

information

Note

The __dirname global provides an absolute path to the file that uses it, while ./ returns current working directory, which might be different depending on where we execute the Node.js script (e.g, $ node ~/code/app/index.js vs. $ node index.js). One exception to the ./ rule is when it’s used in the require() function, e.g., conf = require('./config.json');; then it acts as __dirname.

As you can see, middleware/request handler use is a powerful concept for keeping code organized. The best practice is to keep the router lean and thin by moving all of the logic into corresponding external modules/files. This way, important server configuration parameters will all be neatly in one place when you need them! :-)

The globals module.exports and exports are not quite the same when it comes to the assigning of new values to each of them. While in the example above module.exports = function(){...} works fine and makes total sense, the exports = function() {...} or even exports = someObject;will fail miserably.

This is due to JavaScript fundamentals: the properties of the objects can be replaced without losing the reference to that object, but when we replace the whole object, i.e., exports = ..., we’re losing the link to the outside world that exposes our functions.

This behavior is also referred to as objects being mutable and primitive (strings, numbers, boolean are immutable in JS). Therefore, exports only works by creating and assigning properties to it, e.g., exports.method = function() {...};.

For example, we can run $ node -e "require('./routes-module-exports').FindStories('databases');" and see our nested structure reduced by one level.

The result of using module.exports.

The result of using module.exports.

Take a look at this article for more examples: Node.js, Require and Exports.

For a working example, take a look at the HackHall chapter.