I read this article on Node.js architecture layering on Medium, and I thought it was a good idea. Many of these ideas can also be applied to front-end projects.

The original link blog.codeminer42.com/nodejs-and-…

It was first posted on my blog github.com/mcuking/blo…

Software can change at any time, and one aspect that defines code quality is how easy it is to change the code. But what makes it so?

. If you’re afraid to change something, it’s clearly not well designed. — Martin Fowler

Separation of concerns and responsibilities

“Bringing together things that change for the same reason. Separate things that change for different reasons.”

Whether function, class or module, they can be applied to the single responsibility principle and the separation of concerns. Software architecture is designed based on these principles.

architecture

In software development, responsibility is the unity of tasks to be implemented, such as: representing product concepts in applications, handling network requests, keeping users in a database, and so on.

Have you noticed that these three responsibilities are not in the same category? This is because they belong to different layers and can therefore be divided into concepts. According to the above example, “save users in the database” is related to the concept of “users” and also to the layer on which the database communicates.

Generally, architectures associated with the above concepts tend to be divided into four layers: Domain, Application, Infrastructure, and input interfaces.

Domain layer

In this layer, we can define units that act as entities and business rules and are directly related to our domains. For example, in a User and Team application, we might have a User entity, a Team entity, and a JoinTeamPolicy to answer whether a User can join a given Team.

This is the most isolated and important layer in our software that the Application layer can use to define use cases.

The Application layer

The Application layer defines the actual behavior of our Application and is therefore responsible for performing the interactions between the units of the Domain layer. For example, we could have a JoinTeam use case that takes instances of User and Team and passes them to JoinTeamPolicy. If users can join, it delegates persistence responsibilities to the infrastructure layer.

The Application layer can also be used as an adapter for the Infrastructure layer. Suppose our application can send E-mail; The class directly responsible for communicating with the E-mail server (called MailChimpService) belongs in the Infrastructure layer, but the E-mail Service that actually sends the E-mail belongs in the Application layer, And use MailChimpService internally. As a result, the rest of our application doesn’t know the details about the particular implementation – it just knows that EmailService can send E-mail.

The Infrastructure layer

This is the lowest layer of all, and it is the outer boundary of the application: database, E-mail service, queue engine, and so on.

A common feature of multi-tier applications is the use of the Repository pattern to communicate with databases or some other external persistence service, such as an API. Repository objects are essentially treated as collections, and the layers that use them (domains and applications) do not need to know about the underlying persistence technology (similar to our E-mail service example).

The idea here is that the Repository interface belongs to the Domain layer, and the implementation belongs to the infrastructure layer, i.e. the domain layer only knows the methods and parameters that Repository accepts. Even in terms of testing, this makes both layers more flexible! Since JavaScript does not implement the concept of interfaces, we can imagine our own interfaces and build concrete implementations on top of them in the infrastructure layer.

The Input interface tier

This layer contains all entry points for the application, such as controllers, CLI, Websockets, graphical user interfaces (if desktop applications), and so on.

It should have no knowledge of business rules, use cases, persistence techniques, or even other logic! It should only take user input (such as URL parameters), pass it to the use case, and finally return the response to the user.

NodeJS and separation of concerns

Ok, after all this theory, how does it work on Node applications? To be honest, some of the patterns used in multi-tier architectures are perfectly suited to the JavaScript world!

NodeJS and domain layers

The Domain layer on Node can consist of simple ES6 classes. There are many ES5 and ES6 + modules to help create entities, such as Structure, Ampersand State, TComb, and ObjectModel.

Let’s look at 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;
Copy the code

Note that we do not include backbone. Model or modules like Sequelize and Mongoose in our list, as they are intended to be used in the infrastructure layer to communicate with the outside world. So the rest of our code base doesn’t even need to know they exist.

NodeJS and application layer

Use cases are at the application layer. Unlike Promises, use cases can have consequences beyond success and failure. A good Node model for this situation is Event Emitter. To use it, we must extend the EventEmitter class and emit an event for each possible result, thereby hiding the fact that our repository uses promises internally:

const EventEmitter = require('events');

class CreateUser extends EventEmitter {
  constructor({ usersRepository }) {
    super(a);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); }); }}Copy the code

Thus, our entry point can execute the use case and add a listener for each result, as follows:

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); }};Copy the code

NodeJS and infrastructure layer

The infrastructure layer should not be too difficult to implement, but be careful that the logic does not leak over to the above layers! For example, we could use the Sequelize model to implement a repository that communicates with an SQL database and provide it with method names that do not imply an SQL layer beneath it – such as the generic Add method of our previous example.

We can instantiate a SequelizeUsersRepository and pass it as a usersRepository variable to its dependencies, which may simply 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); }}Copy the code

The same is true for NoSQL databases, E-mail services, queue engines, external apis, and so on.

NodeJS and Input interfaces

There are several ways to implement this layer on Node applications. The Express module is the most used module for HTTP requests, but you can also use Hapi or Restify. The final choice depends on the implementation details, although changes made to this layer should not affect the other details. If migrating from Express to Hapi in some way means that you are coupled when you change some code, and you should pay close attention to fixing it.

Connect the layers

Communicating directly with another layer can be a bad decision and lead to coupling between them. A common solution to this problem in object-oriented programming is dependency Injection (DI). This technique involves making the class’s dependencies accepted as parameters in its constructor, rather than importing the dependencies and instantiating them inside the class itself, creating what is known as inversion of control.

Using this technique allows us to isolate the dependencies of a class in a very compact way, making it more flexible and easier to test because resolving dependencies becomes a trivial task

For Node applications, there is a nice DI module called Awilix that allows us to take advantage of DI without coupling our code to the DI module itself, so we don’t want to use Angular 1’s strange dependency injection mechanism. The author of Awilix has a series of articles that explain Node dependency injection and are worth reading, as well as how to use Awilix. By the way, if you plan to use Express or Koa, you should also look at Awilix-Express or Awilix-KOa.

A practical example

Even with all these examples and illustrations of layers and concepts, I believe there is nothing better than a real-world example of an application that follows a multi-tier architecture to convince you that it is simple to use!

You can check out the BoilerPlate for Web APIs with Node available in production. It has a multi-tier architecture and the basic configuration (including documentation) has been set up for you so you can practice and even use it as a starting template for Node applications.

Additional information

If you want to learn more about multi-tier architecture and how to separate concerns, check out the following links:

  • FourLayerArchitecture

  • Architecture — The Lost Years

  • The Clean Architecture

  • Hexagonal Architecture

  • Domain-driven design