Node.js: Saving time with a boilerplate project

Node.js: Saving time with a boilerplate project

Motivation

How long does it take you to set up a new project before you can actually start working on it? How many times have you coded the same user model with the same sort of authentication? If you answered, like us, too long and too many times, you might consider creating a boilerplate project.

Using a pre-made template for your new projects has many advantages: less time wasted on set up, consistency across projects, a well-known folder structure to start from. If you serve digital solutions to clients, ensuring base quality standards at day 0 is essential and that’s much easier to achieve if you have a base to work up from instead of starting from scratch.

Of course, it comes with some disadvantages too. This is a separate code base that needs to be maintained over time and which might not, after all, fit your new project exactly. Keeping this in mind is important when creating your own boilerplate in order to minimize the risks.

To me, as a Ruby on Rails developer starting to work on Node+Express projects within a company, creating a base of code to enforce following best practices and consistency was basically a given.

Considerations

After working on a few smaller express APIs I started noticing a common pattern repeating: at first everything went peachy and fast. Middleware parse and validate data, check for authentication. Controllers receive requests, access the model and quickly create responses, facilitated by Javascript’s native handling of JSONs. However, the more logic was needed, the more complex the responses became, the messier controller and models got.

Consider this simple web development 101 scenario: You’re writing a blog application and want to return blog posts which have a date attached. Where do you format that date? You could write a helper function and then call it whenever you need it, you could change the toJSON method of the blog post model to always return a formatted date. So far so good, what if we also want to include some data for the writer of the post? What about comments and replies to comments?

What about creating a new instance? Should the controller validate inputs? Should the controller handle any exceptions? Again, maybe this works well for smaller projects and enables quick development, but in larger projects it will necessarily lead to heavier and heavier controllers.

Implementation

So what did we do to handle this? Personally, I like lightweight controllers which are responsible for communication with the client only and which off-load all business and rendering logic to separate services. This pattern allows the controllers to abstract away from the models and implementation. Here’s the folder structure we went with:

.
└── src
├── config
├── controllers
│   ├── authController.js
│   └── usersController.js
├── middleware
├── migrations
├── models
├── server.js
├── services
├── test
│   ├── controllers
│   ├── helpers
│   ├── mocha.opts
│   ├── models
│   └── testHelper.js
└── utils

Since this was just a boilerplate, there wasn’t much to put anywhere, but this is a solid structure which will fit most projects. When a request comes in, middleware parses it before it goes to any controller, this allows for fast responses in some cases, e.g. no auth present, badly formatted requests, bad params. After that, controllers receive the request and delegate the payload to the appropriate service, which performs all business logic. If it returns successfully, the controller then calls a renderer to create the appropriate response payload. If there’s an error, the controller catches it and passes it to the error handler to craft a response.

Decisions, decisions

So after we’ve settled on an architecture, what remains to be seen? Since we had already decided to have a user model with authentication and we wanted to do so that it integrated easily with most front-ends, be they web or mobile apps, we decided to use JWT and passport.js.

Passport allows for a simple and customizable setup. It comes with a long list of strategies for the most common (and also some not so common) authentication flows, which need little configuration to suit each use case. What we did was use the username + password strategy for login and JWT strategy for all other protected endpoints. This meant adding a new folder config where we’ll be saving module configuration and initialization files.

We had already decided to use a relational database and we chose Sequelize as ORM. We also threw in sequelize-cli to create and run migrations. Sequelize actually can work without that by ‘syncing’ model definitions to the database, but using migrations is definitely the better option here. If you let sequelize handle the tables following your model definitions then depending on how you configure it, it will either create your tables but not update them if you modify the model, or create and update the tables but delete all data. There is a newer option that allows for modification of tables without dropping data, but as far as I know, this is not yet fully stable and will obviously only work with simpler modifications. Migrations are safer and more appropriate for a production ready app.

Doing this meant only small additions to our folder structure, namely a migrations and a seeders folder, plus a new config file for sequelize (created by sequelize-cli too!)

For testing, we went with mocha and supertest, allowing us to create both unit and integration tests. With this came a new tests folder holding a copycat structure of what we had originally, each file containing tests for the corresponding module.

Finally, we added eslint for linting. Although I’m writing about this last, this is as important as all the rest. No company, project or developer can aspire to consistency without a linter nagging at you about misplaced commas. We included airbnb's base rules for this, which we had been using for our React projects before this. For good measure, we used pre-commit to tie testing and linting to the commit process and prevent hasty PRs.

End result

What we ended up with represents what we feel is a good compromise between the advantages and disadvantages we expected. Yes, we had to make some limiting decisions like using a relational database and authentication via JWT. However, because the project structure we chose decouples controller logic from models and business logic through services, if we needed to change either of those, we could do it without touching the rest of the app.

In the end, to us, the price paid in maintenance is totally worth the consistency achieved across projects and the speed boost we get in the early stages of a project, so it was an easy decision for us. I personally think doing this will be beneficial in almost any context, unless you’re doing just the one project in a technology, but it’s something you’ll have to weigh for yourself and your own needs.

Take a look at the final result here and drop us a ⭐ if it was useful for you!