Some of nodejs
projects rely on expressjs
for routing. There is a realization that past a certain threshold, some request handlers start looking like copycats. Extreme cases of such instances become a nightmare to debug, hinder scalability. The increase in code reusability, modularity, improves overall testability — along the way scalability and user experience. The question we have to ask is How do we get there?.
This blog article will explore some of the ways to achieve that. In expressjs
routes context, we will shift focus on making sure most of the parts are accessible and testable.
In this article we will talk about:
- The need to modularize
expressjs
routes
- How to modularize
expressjs
routes for reusability
- How to modularize
expressjs
routes for testability
- The need for a manifest route modularization strategy
- How to modularize
expressjs
routes for composability
- How to modularize
expressjs
route handlers for reusability
- How to modularize
expressjs
route handlers for performance
- How to modularize
expressjs
route handlers for composability
- The need to have route handlers as controllers
- How to specialize routes handlers as controllers
Even though this blog post was designed to offer complementary materials to those who bought my Testing nodejs
Applications book, the content can help any software developer to tuneup working environment. You use this link to buy the book.
Show me the code
While following a simple principle “make it work”, you realize that route lines of code(LoC) grows linearly(or leaning towards exponential) as feature requests increase. All this growth can happen inside one file, or on a single function. Assuming all our models are NOT in the same files as our routes, the following source code may be available to us in the early days of a project:
var User = require('./models').User;
/** code that initializes everything then comes this route*/
app.get('/users/:id', function(req, res, next){
User.findById(req.params.id, function(error, user){
if(error) return next(error);
return res.status(200).json(user);
});
});
/**
* More code, more time, more developers
* Then you realize that you actually need:
*/
app.get('/admin/:id', function(req, res, next){
User.findById(req.params.id, function(error, user){
if(error) return next(error);
return res.status(200).json(user);
});
});
Example:
What can possibly go wrong?
When trying to figure out how to approach modularization of expressjs
routes, the following points highlight some challenges:
- Understanding where to start, and where to stop when modularizing routes
- Making a choice between a layered architecture with or without controllers
- Making a choice between a layered architecture with or without services
In the next sections, we will explore more on points made raised earlier.
The need to modularize expressjs
routes
One heavily relied upon a feature in expressjs
is its router. The routes tend to grow out of proportion and can be a source of trouble when the time comes to test, refactor or extend existing functionalities. One of the tools to make our job easy is to apply modularization techniques to expressjs
routes.
How to modularize expressjs
routes for reusability
There is only one route per application, so the notion of route re-usability may not be as evident as it should be in such a context.
However, when we look closer to the constructions of a handler, we may get a sense of how many times an actual route's work can be spread across multiple instances and use cases. When we look at the path itself, it is possible to find matching suffixes.
Suffixes indicate that multiple routes may indeed be using one handler. To keep it simple, different contexts, same actions. /admin/add/user
, /profile/add/user
, /school/:id/add/user
etc. All of the roots, or prefix of /add/user
are contexts in which some action is taking place.
Deep-down the end result is a user being added. There is a probability that the user is going to be added to the same database table or document.
//in one file
let router = require('express').Router();
router.post('/add/user', addUser);
module.exports = router;
//later in another file
let router = require('express').Router(),
add = require('/one/file');
router.use('/admin', add);
router.use('/profile', add);
router.use('/school/:id', add);
module.exports = router;
The modularization of routes, for that matter — route handlers, should not stop at their ability to be reusable.
Modularization can guarantee the stability of routes and their handlers. To put things in perspective, for two distinct routes that share the same route handler, a change in parameter naming should not affect other routes. Likewise, a change in route handler affects routes using the same handler but does not necessarily affect any route configuration.
Like in other use cases, modularizing an expressjs
route consists of two major changes. The first step is to identify, name and eject route handlers. This step may be a bit challenging when the middleware is involved. The second and last step is to move and group similar handlers under the same library. The said library can be exposed to the public using the index trick we discussed in other blog posts.
How to modularize expressjs
routes for testability
The challenge when mocking an expressjs
route is losing route handler implementation in the process.
That may not be an issue when executing integration or end-to-end testing tasks. Taking into consideration that individual handlers can be tested in isolation, we get the benefits of reducing the number of tests and mocking work required per route.
The second challenge is to find a sweet spot between integration testing, unit testing and apply both ideas to the route and route handler, per test case needs.
Loading any library in unit tests is expensive, let alone to load entire expressjs
in every unit test. To avoid this, either loading express from a mockable library or injecting expressjs
application as needed, maybe two healthy alternatives we have to look into so to speak.
The need of manifest route modularization strategy
There is a common pattern that reveals itself at the end of the modularization effort. Related Routes can be grouped into independent modules, to be reused independently on demand. To make this thought a reality, the manifest route technique attaches a route to a router and makes that router available and ready to be used by other routers, or by an expressjs
application.
How to modularize expressjs
routes for composability
There is a lot to unpack when dealing with the composability of expressjs
routes. The takeout when composing routes is a route that should be defined in a way to can plugged on any router, and just work. Another example would be the ability to mount a server or an expressjs
app
instance to the route definition on the get-go and have an application that just works.
How to modularize expressjs
route handlers for reusability
The reusability aspect of business comes in handy to help reduce instances of code duplication. One can argue that this also helps with performance, as well as better test coverage. The advanced use case of higher re-usability ends in a controller or well-organized module of handlers.
The nodejs
module loader is expensive. For fairness, reading a file is expensive. The node_modules is notorious for the number of directories and files associated with it. It is by no surprise that reading and loading all those files may be a performance bottleneck. The fewer the files we read from the disk, the better. The following modularization for composability is a living example of how modularization can be used alongside performance improvements.
How to modularize expressjs
route handlers for composability
Be in this blog post, as in the ones that came ahead of it, we strive to make the application more reusable while at the same time reducing the time it takes to load the application for use or testing purposes. One way of reducing the number of imports is to leverage thunks or injections.
The need to have route handlers as controllers
If we look up close to route handlers are tightly coupled to a route. Previous techniques broke the coupling and moved individual route handlers in their own modules. Another up-close look, reveals two key points: first some route handlers are copycats, second some route handlers are related at the point they may constitute an independent entity on their own. If we group all handlers related to providing one feature, we completely land in the controller space.
How to specialize routes handlers as controllers
If there exist multiple ways to brew a beer, there should be multiple ways to clustering related handlers in the same module or component! Ok, let's admit that that example does not have any sound logic, but You see the point.
One of the ways to group related handlers is to start grouping by feature. Then if for some reason multiple features happen to use similar(or copycat handlers), choosing an advanced level of abstraction becomes ideal. When we have an equivalent of a base controller, that base controller can move to a common library. The name of the common library can be for instance: /core
, /common
, or even /lib
. We can get creative here.
Modularization of Express routes
The easy way to mitigate that is by grouping functions that are similar into the same file. Since the service layer is sometimes not so relevant, we can group functions into a controller.
//in controller/user.js
module.exports = function(req, res, next){
User.findById(req.params.id, function(error, user){
if(error || !user){
return next(error);#return right away
}
return res.status(200).json(user);
});
};
//in routes/user.js
var getUser = require('controller/user);
var router = require('express').Router();
router.get('users/:id', getUser);
router.get('admin/:id', getUser);
//exporting the
module.exports = app;
Example:
Both controller/user.js
and two routes can be tested in isolation.
Conclusion
The complexity that comes with working on large-scale nodejs
/expressjs
applications reduces significantly when the application is in fact well modularized. Modularization is a key strategy in making expressjs
routes more re-usable, composable, and stable as the rest of the system evolves. Modularization brings not only elegance to the routes but also reduces the possibility of route redundancy, as well as improved testability.
In this article, we revisited techniques that improve expressjs
routes elegance, their testability, and re-usability. We focused more on layering the route into routes and controllers, as well as applying modularization techniques based on module.exports
and index
files. There are additional complimentary materials in the “Testing nodejs
applications” book.
References
#snippets #code #annotations #question #discuss