A better file architecture of LoopbackJS application.
Mar 26
LoopbackJS is a great framework to start quickly with an API for a small application. The default architecture proposed by LoopbackJS is enough in that case. Unfortunately, when your application has some advanced logic and a lot of different models, custom boot scripts, remote methods and tons of hooks, it’s starting to be messy and a pain to maintain the code.
I propose an alternative approach to the code organization, which is much cleaner and easy to set up.
Default LoopbackJS behavior.
When you create a vanilla loopback project with the loopback-cli you will get something like this:
// ...
app.start = function() {
// ...
};
boot(app, __dirname, function(err) {
if (err) throw err;
// start the server if `$ node server.js`
if (require.main === module)
app.start();
});
So when we add a boot script server/boot/boot-lvl1.js it will be loaded when the server starts.
// server/boot/boot-lvl1.js
'use strict';
module.exports = function enableAuthentication(app) {
console.log('boot: level 1');
};
So far so good. But let’s imagine a situation when you have several models and each of them has some boot scripts, so the boot directory will grow with every new script. It’s impossible to organize it in directories with a standard loopback configuration. Let’s put a boot script inside a nested directory in the boot and check if it will be called.
// server/boot/nested/boot-lvl2.js
'use strict';
module.exports = function enableAuthentication(app) {
console.log('boot: level 2');
};
Now when we start the server, we won’t see the second console.log in the console output.
server console output of LoopbackJS API project
A Solution.
To solve that issue we need to inform the loopback boot script to load files from more than just one directory. So we need to update the boot script to load all directories that we want. We need to provide an array of directories, from where scripts will be loaded on boot.
const options = {
appRootDir: __dirname,
bootDirs: [ // <--- all locations that we want to load
'./boot/nested',
'./boot/another',
'./boot/yet-another-one',
],
};
boot(app, options, function(err) {
// ...
});
server console output of LoopbackJS API project
So after that, we can group our boot scripts into separate files, which brings some order into our code.
A Better Solution.
So we have a bunch of scripts and modules. Our app is growing and the number of boot scripts increases as well. Naturally, at some point, we want to provide even better organization to our code by creating more subdirectories. It could look like this:
A bit more complex folder structure in the boot directory of LoopbackJS API project
We could insert every directory to the config manually, but that’s not a nice solution, because we need to remember about that every time we create the directory. This can be in the server.js file by loading every js file that’s inside the server/boot directory. I propose to install some module that will do that for us, eg. readdirp. Now our server.js file looks like this:
// load module
const readdirp = require('readdirp');
// prepare settings for the readdirp moduleconst settings = {
root: './server/boot',
entryType: 'directories',
};
// list of directories to populate
const directories = [];
readdirp(settings)
.on('data', entry => {
// add each directory to the directories array
directories.push(entry.fullPath);
})
.on('end', () => {
const options = {
appRootDir: __dirname,
bootDirs: directories, // load directories
};
boot(app, options, ...);
});
So now, with every new directory within boot we don’t have to update our configuration, the file will be loaded automatically.
The Best Solution.
Nice, we’ve provided some order, but it’s still kind of suboptimal to keep everything in the boot script: model related stuff, authorization, custom boot functionality etc. It would be more convenient to separate what belongs to the models one level up and put into the modules directory. Then, within each module, we could also organize it further. For example, we could create a subdirectory for each of:
- configuration: static/constant variables that store the values that don’t have to be hidden in the environment variables,
- remote methods: all custom remote methods are stored here,
- remote hooks : all remote hooks could be here,
- database connector hooks : as it stands for — any connector hooks,
- operational hooks: before … and after … hooks,
- middlewares : any module specific middlewares should go here; a global configuration in server/middlerware.json still needs to be updated to place your middleware at a proper stage,
- model methods : any custom model methods,
- helpers : anything that does not fit the above and contains a login,
- validators : model validation definition and custom validation functions.
That structure will potentially save your time when you’ll have to go back to the code after a while.
Our structure would look like this:
Resources
All the above can be downloaded from the repository: https://github.com/akkonrad/loopback-modules-demo
If you have a different idea about the structure, you are more than welcome to give your input here, I’d love to improve it.