Softwares are subject to change all the time, and one aspect that contributes to defining the quality of a code is precisely how easy it is to be altered. But what makes it be like so?
…if you’re afraid to change something it is clearly poorly designed.
— Martin Fowler
Don’t make your code a swiss army knife
Separation of concerns and responsibilities
“Gather together the things that change for the same reasons. Separate those things that change for different reasons.”
Be those things functions, classes, or modules, they can apply to the single responsibility principle and the separation of concerns. Designing software based on these principles starts out with the architecture.
Architecture
In software development, a responsibility is a task that a unity is committed to achieving, for example: represent the concept of a product in an application, handle network requests, persist a user in the database, and so on and so forth.
Did you notice that these three responsibilities aren’t in the same category? It’s due to the fact that they belong to different layers, which in turn can be divided into concepts. As per the above example, “persist a user in the database” relates to the “user” concept and also to the layer that talks to the database.
In general, architectures correlated to the above concepts tend to be split off into four layers: domain, application, infrastructure, and input interfaces.
Domain layer
In this layer, we may define units which play the role of entities and business rules and have a direct relationship to our domain. For example, in an application of users and teams, we would probably have a User entity, a Team
entity, and a JoinTeamPolicy
to answer whether a user is able to join a given team or not.
This is the most isolated and important layer of our software, and it may be used by the application layer to define use cases.
Application layer
The application layer defines the actual behavior of our application, thus being responsible for performing interactions among units of the domain layer. For example, we can have a JoinTeam use case which receives instances of User
and Team
and passes them on to JoinTeamPolicy
; if the user can join in, it delegates the persistence responsibility to the infrastructure layer.
The application layer can also be used as an adapter to the infrastructure layer. Let’s say our application can send emails; the class responsible for talking directly to the email server (let’s call it MailChimpService
) belongs to the infrastructure layer, but the one that actually sends the emails (EmailService
) belongs to the application layer and uses MailChimpService
internally. Therefore, the rest of our application does not know details about specific implementations— it only knows that EmailService
is able to send emails.
Infrastructure layer
This is the lowest layer of all, and it’s the boundary to whatever is external to our application: the database, email services, queue engines, etc.
A common feature of multilayer applications is the use of the repository pattern to communicate with the database or some other external persistence service (like an API). Repository objects are essentially treated as collections, and the layers using them (domain and application) don’t need to know which persistence technology lies underneath (similarly to our email service example).
The idea here is that the repository interface belongs to the domain layer, and the implementation, in turn, belongs to the infrastructure layer — i.e. the domain only knows about methods and parameters accepted by the repository. That makes both layers more flexible, even with regards to testing! Since JavaScript doesn’t implement the concept of interfaces, we can imagine our own, and based on that create a concrete implementation on the infrastructure layer.
Input interfaces layer
This layer holds all the entry points of our application, such as controllers, the CLI, websockets, graphic user interfaces (in case of desktop applications), and so on.
It should not have any knowledge about business rules, use cases, persistence technologies, and not even about other kinds of logic! It should only receive user input (like URL parameters), pass it on to the use case and finally return a response to the user.
NodeJS and the separation of concerns
OK, after all of this theory, how does it work on a Node application? Truth to be said, some patterns used in multilayer architectures fit in very well with the JavaScript world!
NodeJS and the domain layer
The domain layer on Node can be composed of simple ES6 classes. There are lots of ES5 and ES6+ modules to aid in creating domain entities, for example: Structure, Ampersand State, tcomb and ObjectModel.
Let’s see a simple example using Structure:
const { attributes } = require('structure');
const User = attributes({
id: Number,
name: {
type: String,
required: true
},
age: Number
})(class User {
isLegal() {
return this.age >= User.MIN_LEGAL_AGE;
}
});
User.MIN_LEGAL_AGE = 21;
Notice that our list doesn’t contain Backbone.Model or modules like Sequelize and Mongoose, because these are meant to be used in the infrastructure layer to communicate with the external world. Therefore, the rest of our codebase doesn’t even need to know about their existence.
NodeJS and the application layer
Use cases belong to the application layer, and differently from promises they may have results beyond success and failure. A good Node pattern for such cases is the event emitter. To use it, we have to extend the EventEmitter
class and emit an event for each possible outcome, thus hiding the fact that our repository uses a promise internally:
const EventEmitter = require('events');
class CreateUser extends EventEmitter {
constructor({ usersRepository }) {
super();
this.usersRepository = usersRepository;
}
execute(userData) {
const user = new User(userData);
this.usersRepository
.add(user)
.then((newUser) => {
this.emit('SUCCESS', newUser);
})
.catch((error) => {
if(error.message === 'ValidationError') {
return this.emit('VALIDATION_ERROR', error);
}
this.emit('ERROR', error);
});
}
}
That way our entry point can execute the use case and add a listener for each outcome, just like this:
const UsersController = {
create(req, res) {
const createUser = new CreateUser({ usersRepository });
createUser
.on('SUCCESS', (user) => {
res.status(201).json(user);
})
.on('VALIDATION_ERROR', (error) => {
res.status(400).json({
type: 'ValidationError',
details: error.details
});
})
.on('ERROR', (error) => {
res.sendStatus(500);
});
createUser.execute(req.body.user);
}
};
NodeJS and the infrastructure layer
The implementation of the infrastructure layer should not present difficulties, but be careful for its logic to not leak out to above layers!
For example, we may use Sequelize models to implement a repository that talks to a SQL database, and give it method names which don’t suggest the existence of a SQL layer underneath — such as the generic add
method of our last example.
We can instantiate a SequelizeUsersRepository
and pass it up to its dependents as a usersRepository
variable, and the dependents may just interact with its interface.
class SequelizeUsersRepository {
add(user) {
const { valid, errors } = user.validate();
if(!valid) {
const error = new Error('ValidationError');
error.details = errors;
return Promise.reject(error);
}
return UserModel
.create(user.attributes)
.then((dbUser) => dbUser.dataValues);
}
}
The same reasoning goes for NoSQL databases, email services, queue engines, external APIs and so on.
NodeJS and the input interfaces layer
There are lots of possibilities for implementing this layer on Node apps. For HTTP requests, the Express module is the most used one but you can also use Hapi or Restify. The final choice comes down to implementation details, though changes to this layer should not affect other ones. If migrating from Express to Hapi somehow implies in performing thereof changes, it’s a sign of coupling and you should pay close attention to fix it.
Connecting the layers
Having a layer talk directly to another can be a bad decision and cause coupling between them. In object-oriented programming, a common solution to this problem is dependency injection (DI). This technique consists in making dependencies of a class be received as arguments in its constructor, instead of requiring the dependencies and instantiating them inside of the class itself — hence creating the so-called inversion of control.
Using this technique enables us to isolate dependencies of a class in a very concise way, making it more flexible and easy to be tested because stubbing out dependencies becomes a trivial task.
For Node applications there’s a good DI module called Awilix, which allows us to leverage DI without coupling our code to the DI module itself — so we don’t feel like using that weird dependency injection machinery from Angular 1. The author of Awilix has a series of posts explaining dependency injection with Node which are worth reading, and also an introduction about how to use Awilix. By the way, if you plan on using Express or Koa, you should also take a look at Awilix-Express or Awilix-Koa.
A practical example
Even with all those examples and explanations about layers and concepts, I believe there’s nothing better than a practical example of an application following a multilayer architecture to convince you that indeed, it can be simple to use!
You can check out this production-ready boilerplate for web APIs with Node. It applies the multilayer architecture and has the basics already set up (including documentation) for you, so that you can practice and even use it as a kickstarter for your Node apps.
Additional information
If you want to learn more about multilayer architectures and how to separate concerns, take a look at the following links:
Thanks to Thiago Araújo Silva.
We want to work with you. Check out our "What We Do" section!