7 simple rules to make your Node & Express apps easier to modify and maintain

When your team grows and when your application complexity grows, there are many new obstacles that you face. One of them is that your app becomes much more difficult to modify and maintain.

Adding new features or modifying old ones is much harder. Even adding tests is more difficult, because there are more tests cases than ever before.

To avoid those problems, you have begun using early on the MVC pattern, but there are still some issues.

We have tried to solve them in our article Best practices for Express app structure where we showed you how to use MVC and a basic Express project to start your application.

Even after that there are still many places where things can go wrong. Wouldn’t it be nice to easily make changes and maintain even complex Node & Express apps?

Models should not know about other models

When you have a site which accepts users and comments, usually you know which comment to which user belongs. Those two types of data are related.

However, this does not mean that the comment model should know about the existence of the user model. There should be just some way to identify which comment to which user belongs.

Let’s have the following user model which can create a user and then it can get a user.

exports.create = function(email, password, name, done) {
  // Create the user and call 'done(null, id)' where id is a string.

}

exports.get = function(id, done) {
  // Finds the user and calls 'done(null, user)'

}

As a result your comment model only needs to use a string to identify who the user is and this string can be the user id. Let’s see how it will work.

exports.create = function(user, text, done) {
  // Creates the comment with the text and calls 'done(null, id)' 

}

exports.getAll = function(done) {
  // Finds all comments and calls 'done(null, comments)'

}

exports.getAllByUser = function(user, done) {
  // Finds all comments by user and calls 'done(null, comments)'

}

You see in the comment model we don’t know anything about the user model. The comment model only knows that the value of the parameter user identifies which comments belong to which user.

In this specific case it makes sense to use the user id, because it is unique and it will never change.

What are the benefits of this approach? First, it makes testing very easy because your model has none outside dependcies beside the database.

Second, whenever you need to change the internal of your comment or user model you can do it without affecting other parts of your code.

Third, this makes reasoning about models much easier, which is critical to maintenance. It is easy to find why something happens and where it happens.

Then were do you make the connection between the controllers? This brings us to the next rule.

Controllers should connect the work between models

One of the core purposes of a controller is to connect the data from different models.

Whenever, a new request is received it gets or updates all the models which are affected. This is its responsibility.

Let’s have a look how your comments controller would look like with the models from above.

var express = require('express')
  , router = express.Router()

var Comment = require('../models/comment')
  , User = require('../models/user')

router.get('/comments/:user', function(req, res) {
  Comment.getAllByUser(req.params.user, function(err, comments) {
    res.render('dashboard', {comments: comments})
  })
})

router.post('/comments', function(req, res) {
  Comment.create(req.session.user.id, req.body.text, function(err, id) {
    res.redirect('/comments/' + req.session.user.id)
  })
})

As you can see the controller is the one passing the necessary information so that the data from the different models is related.

Request handler should not call other request handlers

This is a rule very similar to the rule for the models. Its main benefit is that all request are very easy to follow.

You know that they always come from an http request. Then they are then handled by the controller, which in turn uses one or more models to retrieve or store the necessary data. Whenever a change is required, you can make it much more easily.

However, sometimes you do some processing and than you want to show another page. It may be tempting to just call the controller method, instead it is better to use redirect, just like in the example from above

router.post('/comments', function(req, res) {
  Comment.create(req.session.user.id, req.body.text, function(err, id) {
    res.redirect('/comments/' + req.session.user.id)
  })
})

This is critical because if the user just hits refresh on the page, only the rendering will happen instead of all the processing.

Imagine that in this case we rendered the page by calling the other request handler instead of redirecting. Then on each refresh more processing will happen and a new comment will be created.

Common code is used in helpers for both models and controllers

However, sometimes there is code that needs to be shared between multiple models or controllers or both. Common examples for this are functions which process numbers or dates.

Sometimes this code can be so complex so having it at multiple places, like every model and controller that needs it, is way too much.

So it can be tempting to implement it one controller or model and then call it from everywhere.

There is a better solution. In addition to your controllers and models, it is a good idea to also have a helpers folder to contain all code which is shared between different parts of your app.

This not only allows you to make all changes and improvments at once place, but it also makes this code easier to test because it is separated and can be tested separately from the rest.

Models don’t expose internal implementation

Another common mistake is to expose internal implementation. What does it mean? Let’s look at an example.

exports.select = function(columns, orderby, done) {
  // uses 'columns' and 'orderby' for an SQL query

}

Now let’s look at the better example.

exports.getAllByUser = function(user, done) {
  // Finds all comments by user and calls 'done(null, comments)'

}

In the first case we use something specifically from SQL, but the controller should not know anything about it as it doesn’t care about SQL, instead it cares about how to best return the appropriate results to the user.

Not only that but it also makes testing more difficult as it provides way too many cases.

Instead having business specific methods makes your code more readable and specific for your use case. Not only that but due to its specifics it is also much easier to test.

Models never return http errors or other direct responses

Very often you immediately return the data from your models. Especially when you create a REST API.

In an API when there is an error you return an HTTP error. As a result, it is very tempting the error to be generated by the model.

However, HTTP errors have several problems. First, they are not specific enough and most models are used by multiple controllers. So later when you need to use your model at another place it will be much more difficult to decide what happens.

Second, HTTP errors are for the controller as it is its responsibility to format the response.

Models don’t do templates they only care about data

Similar to HTTP errors, templates should not be handled in the model. The model only provides data, it is not his responsibility to present it in the appropriate format.

Next

It might not seem obvious but those rules really make developing Node applications a lot easier.

Your next step might be to try to apply them to your existing code base.


Other articles that you may like

Did you like this article?

Please share it
Enter your email and get our NPM Cheat Sheet for NodeJS Developers and the links to our 5 most popular articles which have helped thousands of developers build faster, more reliable and easier to maintain Node applications.

We are Stefan Fidanov & Vasil Lyutskanov. We share actionable advice about development with Node, Express, React and other web & mobile technologies.

It is everything that we have learned from years of experience working with customers from all over the world on projects of all sizes.

Let's work together
© 2018 Terlici Ltd · Terms · Privacy