Big Ball of Mud in web development

The most popular solution for Node.js applications in recent years — Express. Proven, but painfully low-level and does not offer good architectural approaches. The main pattern is middleware. A typical application on Express resembles a big lump of dirt (this is not a name-calling, but an antipattern).

All layers are mixed up, there is a controller in one file, where there is everything: infrastructure logic, validation, business logic. It’s painful to work with this, I don’t want to support such code. And whether we can write to Node.enterprise-level js code?

This requires a codebase that is easy to maintain and develop. In other words, architecture is needed.

Node architecture.js applications

“The goal of software architecture is to reduce human labor costs for creating and maintaining a system.”

Robert “Uncle Bob” Martin

Architecture consists of two important things: layers and connections between them. We need to break our application into layers, prevent leaks from one to the other, properly organize the hierarchy of layers and the connections between them.

Layers

How to split an application into layers? There is a classic three-level approach: data, logic, representation.

Now this approach is considered outdated. The problem is that the basis is data, which means that the application is designed depending on how the data is presented in the database, and not on what business processes they participate in.

A more modern approach assumes that the application has a domain layer that works with business logic and is a representation of real business processes in the code. However, if we turn to the classic work of Eric Evans Domain-Driven Design, we will find there such a scheme of application layers:

What’s wrong here? It would seem that the basis of a DDD—designed application should be a domain – high-level policies, the most important and valuable logic. But under this layer lies the entire infrastructure: data access layer (DAL), logging, monitoring, etc. That is, policies of a much lower level and of lesser importance.

The infrastructure turns out to be at the center of the application, and a banal replacement of the logger can lead to a shake-up of the entire business logic.

If we turn to Robert Martin again, we will find that in the book Clean Architecture he postulates a different hierarchy of layers in the application, with the domain in the center.

Accordingly, all four layers should be positioned differently:

We have selected the layers and defined their hierarchy. Now let’s move on to the connections.

Communications

Let’s go back to the example with the user logic call. How to get rid of direct dependence on the infrastructure in order to ensure the correct hierarchy of layers? There is a simple and long—known way to reverse dependencies – interfaces.

Now the high-level UserEntity does not depend on the low-level Logger. On the contrary, it dictates the contract that needs to be implemented in order to include the Logger in the system. Replacing the logger in this case boils down to connecting a new implementation that complies with the same contract. The important question is how to connect it?

import {Logger} from ‘../core/logger’;

class UserEntity {

            private _logger: Logger;

            constructor() {

                        this._logger = new Logger();

            }

            …

}

const UserEntity = new UserEntity();

The layers are connected rigidly. There is a tie to both the file structure and the implementation. We need Dependency Inversion, which we will do using Dependency Injection.

export class UserEntity {

            constructor(private _logger: ILogger) { }

            …

}

const logger = new Logger();

const UserEntity = new UserEntity(logger);

Now the “domain” UserEntity no longer knows anything about the implementation of the logger. It provides a contract and expects the implementation to comply with that contract.

Of course, manually generating instances of infrastructure entities is not the most pleasant thing. We need a root file in which we will prepare everything, we will have to somehow drag the created logger instance through the entire application (it is advantageous to have one, and not create many). Tedious. And this is where IoC containers come into play, which can take over this bollerplate work.

What might using a container look like? For example, so:

export class UserEntity {

            constructor(@Inject(LOGGER) private readonly _logger: ILogger){ }

}

What’s going on here? We used the magic of decorators and wrote instructions: “When creating an instance of UserEntity, embed in its private field _logger an instance of the entity that lies in the IoC container under the LOGGER token. It is expected that it corresponds to the ILogger interface.” And then the IoC container will do everything by itself.

We have identified the layers, decided on how we will untie them. It’s time to choose a framework.

Frameworks and architecture

The question is simple: moving away from Express to a modern framework, will we get a good architecture? Let’s take a look at Nest:

  • written in TypeScript,
  • built on top of Express/Fastify, there is compatibility at the middleware level,
  • declares modularity of logic,
  • provides an IoC container.

It seems that everything we need is here! They also left the concept of the application as a middleware chain. But what about good architecture?

Leave a Reply

Your email address will not be published. Required fields are marked *