DEV Community

Remo H. Jansen
Remo H. Jansen

Posted on • Edited on

Implementing SOLID and the onion architecture in Node.js with TypeScript and InversifyJS

In this article, we are going to describe an architecture known as the onion architecture. The onion architecture is a software application architecture that adheres to the SOLID principles. It uses the dependency injection principle extensively, and it is profoundly influenced by the Domain Driven Design (DDD) principles and some functional programming principles.

Prerequisites

The following section describes some software design principles and design patterns that we must learn to be able to understand the onion architecture.

The separation of concerns (SoC) principle

Concerns are the different aspects of software functionality. For instance, the "business logic" of software is a concern, and the interface through which a person uses this logic is another concern.

The separation of concerns is keeping the code for each of these concerns separated. Changing the interface should not require changing the business logic code, and vice versa.

The SOLID principles

SOLID is an acronym that stands for the following five principles:

Single responsibility principle

A class should have only a single responsibility

The most effective way to break applications is to create GOD classes.

A God class is a class that knows too much or does too much. The God object is an example of an anti-pattern.

God classes keep track of a lot of information and have several responsibilities. One code change will most likely affect other parts of the class and therefore indirectly all other classes that use it. That, in turn, leads to an even bigger maintenance mess since no one dares to do any changes other than adding new functionality to it.

The following example is a TypeScript class that defines a Person; this class should not include email validation because that is not related to a person behavior:



class Person {
    public name : string;
    public surname : string;
    public email : string;
    constructor(name : string, surname : string, email : string){
        this.surname = surname;
        this.name = name;
        if(this.validateEmail(email)) {
          this.email = email;
        }
        else {
            throw new Error("Invalid email!");
        }
    }
    validateEmail(email : string) {
        var re = /^([\w-]+(?:\.[\w-]+)*)@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$/i;
        return re.test(email);
    }
    greet() {
        alert("Hi!");
    }
}


Enter fullscreen mode Exit fullscreen mode

We can improve the class above by removing the responsibility of email validation from the Person class and creating a new Email class that will have that responsibility:



class Email {
    public email : string;
    constructor(email : string){
        if(this.validateEmail(email)) {
          this.email = email;
        }
        else {
            throw new Error("Invalid email!");
        }        
    }
    validateEmail(email : string) {
        var re = /^([\w-]+(?:\.[\w-]+)*)@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$/i;
        return re.test(email);
    }
}

class Person {
    public name : string;
    public surname : string;
    public email : Email;
    constructor(name : string, surname : string, email : Email){
        this.email = email;
        this.name = name;
        this.surname = surname;
    }
    greet() {
        alert("Hi!");
    }
}


Enter fullscreen mode Exit fullscreen mode

Making sure that a class has a single responsibility makes it per default also easier to see what it does and how you can extend/improve it.

Open/close principle

Software entities should be open for extension, but closed for modification.

The following code snippet is an example of a piece of code that doesn't adhere to the open/close principle:



class Rectangle {
    public width: number;
    public height: number;
}

class Circle {
    public radius: number;
}

function getArea(shapes: (Rectangle|Circle)[]) {
    return shapes.reduce(
        (previous, current) => {
            if (current instanceof Rectangle) {
                return current.width * current.height;
            } else if (current instanceof Circle) {
                return current.radius * current.radius * Math.PI;
            } else {
                throw new Error("Unknown shape!")
            }
        },
        0
    );
}


Enter fullscreen mode Exit fullscreen mode

The preceding code snippet allows us to calculate the area of two shapes (Rectangle and Circle). If we try to add support for a new kind of shape we will be extending our program. We can certainly add support for a new shape (our application is open for extension), the problem is that to do so we will need to modify the getArea function, which means that our application is also open for modification.

The solution to this problem is to take advantage of polymorphism in object-oriented programming as demonstrated by the following code snippet:



interface Shape {
    area(): number;
}

class Rectangle implements Shape {

    public width: number;
    public height: number;

    public area() {
        return this.width * this.height;
    }
}

class Circle implements Shape {

    public radius: number;

    public area() {
        return this.radius * this.radius * Math.PI;
    }
}

function getArea(shapes: Shape[]) {
    return shapes.reduce(
        (previous, current) => previous + current.area(),
        0
    );
}


Enter fullscreen mode Exit fullscreen mode

The new solution allows us to add support for a new shape (open for extension) without modifying the existing source code (closed for modification).

Liskov substitution principle

Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.

The Liskov substitution principle also encourages us to take advantage of polymorphism in object-oriented programming. In the preceding example:



function getArea(shapes: Shape[]) {
    return shapes.reduce(
        (previous, current) => previous + current.area(),
        0
    );
}


Enter fullscreen mode Exit fullscreen mode

We used the interface Shape to ensure that our program is open for extension but closed for modification. The Liskov substitution principle tells us that we should be able to pass any subtype of Shape to the getArea function without altering the correctness of that program. In static programming languages like TypeScript, the compiler will check for us the correct implementation of a subtype (e.g., if an implementation of Shape is missing the area method we will get a compilation error). This means that we will not need to do any manual work to ensure that our application adheres to the Liskov substitution principle.

Interface segregation principle

Many client-specific interfaces are better than one general-purpose interface.

The interface segregation principle helps us prevent violations of the single responsibility principle and the separation of concerns principle.
Let’s imagine that you have two domain entities: Rectangle and Circle. You have been using these entities in your domain services to calculate their area, and it was working very well, but now you need to be able to serialize them in one of your infrastructure layers. We could solve the problem by adding an extra method to the Shape interface:



interface Shape {
    area(): number;
    serialize(): string;
}

class Rectangle implements Shape {

    public width: number;
    public height: number;

    public area() {
        return this.width * this.height;
    }

    public serialize() {
        return JSON.stringify(this);
    }
}

class Circle implements  Shape {

    public radius: number;

    public area() {
        return this.radius * this.radius * Math.PI;
    }

    public serialize() {
        return JSON.stringify(this);
    }

}


Enter fullscreen mode Exit fullscreen mode

Our domain layer needs the area method (from the Shape interface), but it doesn't need to know anything about serialization:



function getArea(shapes: Shape[]) {
    return shapes.reduce(
        (previous, current) => previous + current.area(),
        0
    );
}


Enter fullscreen mode Exit fullscreen mode

Our infrastructure layer needs the serialize method (from the Shape interface), but it doesn't need to know anything about the area:



// ...
return rectangle.serialize();


Enter fullscreen mode Exit fullscreen mode

The problem is that adding a method named serialize to the Shape interface is a violation of the SoC principle and the single responsibility principles. The Shape is a business concern and being serializable is an infrastructure concern. We shouldn’t mix these two concerns in the same interface.

The Interface segregation principle tells us that many client-specific interfaces are better than one general-purpose interface, which means that we should split our interfaces:



interface RectangleInterface {
    width: number;
    height: number;
}

interface CircleInterface {
    radius: number;
}

interface Shape {
    area(): number;
}

interface Serializable {
    serialize(): string;
}


Enter fullscreen mode Exit fullscreen mode

Using the new interfaces, we are implementing our domain layer in a way that is completely isolated from infrastructure concerns like serialization:



class Rectangle implements RectangleInterface, Shape {

    public width: number;
    public height: number;

    public area() {
        return this.width * this.height;
    }
}

class Circle implements CircleInterface, Shape {

    public radius: number;

    public area() {
        return this.radius * this.radius * Math.PI;
    }
}

function getArea(shapes: Shape[]) {
    return shapes.reduce(
        (previous, current) => previous + current.area(),
        0
    );
}


Enter fullscreen mode Exit fullscreen mode

In the infrastructure layer we can use a new set of entities that deal with serialization:



class RectangleDTO implements RectangleInterface, Serializable {

    public width: number;
    public height: number;

    public serialize() {
        return JSON.stringify(this);
    }
}

class CircleDTO implements CircleInterface, Serializable {

    public radius: number;

    public serialize() {
        return JSON.stringify(this);
    }
}


Enter fullscreen mode Exit fullscreen mode

Using multiple interfaces instead of one general-purpose interface has helped us to prevent a violation of the SoC principle (the business layer doesn’t know anything about serialization) and the Single responsibility principle (we don’t have a class God class that knows about both the serialization and the calculation of the area).

We can argue that RectangleDTO and rectangle Rectangle are almost identical and they are a violation of the "Don't repeat yourself" (DRY) principle. I don't think it is the case because while they look the same, they are related to two different concerns. When two pieces of code look alike, it doesn't always mean that they are the same thing.

Also, even if they are a violation of the DRY principle, we would have to choose between violating the DRY principle or the SOLID principles. I believe that the DRY principle is less important than the SOLID principles and I would, therefore "repeat myself" in this particular case.

Dependency inversion principle

One should depend upon abstractions, [not] concretions.

The dependency inversion principle tells us that we should always try to have dependencies on interfaces, not classes. It is important to mention that dependency inversion and dependency injection are NOT the same thing.

It is unfortunate that the dependency inversion principle is represented by the D in SOLID. It is always the last principle explained, but it is the most important principle in SOLID. Without the dependency inversion principle, most of the other SOLID principles are not possible. If we go back and revisit all the previously explained principles we will realize that the usage of interfaces is one of the most fundamental elements in each of the principles:

  • Depending on an interface that follows the interface segregation principle allows us to isolate a layer from the implementation details of another layer (SoC principle) and helps us to prevent violations of the single responsibility principle.

  • Depending on an interface also allows us to replace an implementation with another (Liskov substitution principle).

  • Depending on an interface enables us to write applications that are open for extension but close for modification (Open/close principle).

Implementing the SOLID principles in a programming language that doesn’t support interfaces or in a programming paradigm that doesn’t support polymorphism is very unnatural. For example, implementing the SOLID principles in JavaScript ES5 or even ES6 feels very unnatural. However, in TypeScript, it feels as natural as it can be.

The model-view-controller (MVC) design pattern

The MVC design pattern separates an application into three main components: the model, the view, and the controller.

Model

Model objects are the parts of the application that implement the logic for the application's data domain. Often, model objects retrieve and store model state in a database. For example, a Product object might retrieve information from a database, operate on it, and then write updated information back to a Products table in a SQL Server database.

In small applications, the model is often a conceptual separation instead of a physical one. For example, if the application only reads a dataset and sends it to the view, the application does not have a physical model layer and associated classes. In that case, the dataset takes on the role of a model object.

View

Views are the components that display the application's user interface (UI). Typically, this UI is created from the model data. An example would be an edit view of a Products table that displays text boxes, drop-down lists, and checks boxes based on the current state of a Product object.

Controller

Controllers are the components that handle user interaction, work with the model, and ultimately select a view to render that displays UI. In an MVC application, the view only displays information; the controller handles and responds to user input and interaction. For example, the controller processes query-string values and passes these values to the model, which in turn might use these values to query the database.

The MVC pattern helps you create applications that separate the different aspects of the application (input logic, business logic, and UI logic) while providing a loose coupling between these elements. The pattern specifies where each kind of logic should be located in the application. The UI logic belongs in the view. Input logic belongs in the controller. Business logic resides in the model. This separation helps you manage complexity when you build an application because it enables you to focus on one aspect of the implementation at a time. For example, you can focus on the view without depending on the business logic.

The loose coupling between the three main components of an MVC application also promotes parallel development. For example, one developer can work on the view, a second developer can work on the controller logic, and a third developer can focus on the business logic in the model. The Model-View-Controller (MVC) design pattern is an excellent example of separating these concerns for better software maintainability.

The repository and the data mapper design patterns

The MVC pattern helps us to decouple the input logic, business logic, and UI logic. However, the model is responsible for too many things. We can use a repository pattern to separate the logic that retrieves the data and maps it to the entity model from the business logic that acts on the model. The business logic should be agnostic to the type of data that comprises the data source layer. For example, the data source layer can be a database, a static file or a Web service.

The repository mediates between the data source layer and the business layers of the application. It queries the data source for the data, maps the data from the data source to a business entity, and persists changes in the business entity to the data source. A repository separates the business logic from the interactions with the underlying data source. The separation between the data and business tiers has three benefits:

  • It centralizes the data logic or Web service access logic.
  • It provides a substitution point for the unit tests.
  • It provides a flexible architecture that can be adapted as the overall design of - the application evolves.

The repository creates queries on the client's behalf. The repository returns a matching set of entities that satisfy the query. The repository also persists new or changed entities. The following diagram shows the interactions of the repository with the client and the data source.

Repositories are bridges between data and operations that are in different domains. A common case is mapping from a domain where data is weakly typed, such as a database, into a domain where objects are strongly typed, such as a domain entity model.

A repository issues the appropriate queries to the data source, and then it maps the result sets to the externally exposed business entities. Repositories often use the Data Mapper pattern to translate between representations.

Repositories remove dependencies that the calling clients have on specific technologies. For example, if a client calls a catalog repository to retrieve some product data, it only needs to use the catalog repository interface. For example, the client does not need to know if the product information is retrieved with SQL queries to a database or Collaborative Application Markup Language (CAML) queries to a SharePoint list. Isolating these types of dependences provides flexibility to evolve implementations.

The onion architecture

The onion architecture divides the application into circular layers (like an onion):

The central layer is the domain model. As we move towards the outer layers, we can see the domain services, the application services and, finally, the test, infrastructure, and UI layers.

In DDD, the center of everything is what is known as β€œthe domain” The domain is composed of two main components:

  • Domain model
  • Domain services

In functional programming, one of the main architecture principles is to push side-effects to the boundaries of the application. The onion architecture also follows this principle. The application core (domain services and domain model) should be free of side effects and implementation details, which means that there should be no references to things like data persistence (e.g., SQL) or data transportation (e.g., HTTP) implementation details.

The domain model and domain services don’t know anything about databases, protocols, cache or any other implementation-specific concern. The application core is only concerned about the characteristics and rules of the business. The external layers (infrastructure, test and user interface) are the ones that interact with the system resources (Network, Storage, etc.) and is where side-effects are isolated and kept away from the application core.

The separation between layers is achieved via the usage of interfaces and the application of the dependency inversion principle: Components should depend upon abstractions (interfaces) not concretions (classes). For example, one of the infrastructure layers is the HTTP layer which is mainly composed of controllers. A controller named AircraftController can have a dependency on an interface named AircraftRepository:



import { inject } from "inversify";
import { response, controller, httpGet } from "inversify-express-utils";
import * as express from "express";
import { AircraftRepository } from "@domain/interfaces";
import { Aircraft } from "@domain/entitites/aircraft";
import { TYPE } from "@domain/types";

@controller("/api/v1/aircraft")
export class AircraftController {

    @inject(TYPE.AircraftRepository) private readonly _aircraftRepository: AircraftRepository;

    @httpGet("/")
    public async get(@response() res: express.Response) {
        try {
            return await this._aircraftRepository.readAll();
        } catch (e) {
            res.status(500).send({ error: "Internal server error" });
        }

    }

    // ...

}


Enter fullscreen mode Exit fullscreen mode

AircraftController is part of the infrastructure layer and its main responsibility is dealing with HTTP related concerns and delegate work to the AircraftRepository The AircraftRepository implementation should be completely unaware of any HTTP concern. At this point, our dependency graph looks as follows:

The arrows in the diagram have different meanings the β€œcomp” arrow defines that AircraftRepository is a property of AircraftController (composition). The β€œref” arrow defines that AircraftController has a reference or dependency on Aircraft.

The AircraftRepository interface is part of the domain services while the AircraftController and AircraftRepository implementation are part of the infrastructure layer:

This means that we have a reference from one of the outer layers (infrastructure) to one of the inside layers (domain services). In the onion architecture we are only allowed to reference from the outer layers to the inner layers and not the other way around:

We use the AircraftRepository interface to decouple the domain layer from the infrastructure layer at design time. However, at runtime, the two layers must be somehow connected. This "connection" between interfaces and implementation is managed by InversifyJS. InversifyJS allow use to declare dependencies to be injected using the @inject decorator. At design time, we can declare that we wish to inject an implementation of an interface:



@inject(TYPE.AircraftRepository) private readonly _aircraftRepository: AircraftRepository;


Enter fullscreen mode Exit fullscreen mode

At runtime, InversifyJS will use its configuration to inject an actual implementation:



container.bind<AircraftRepository>(TYPE.AircraftRepository).to(AircraftRepositoryImpl);


Enter fullscreen mode Exit fullscreen mode

We will now take a look at the AircratRepository and Repository<T> interfaces which is part of the domain services layer.



import { Aircraft } from "@domain/entitites/aircraft";

export interface Repository<T> {
    readAll(): Promise<T[]>;
    readOneById(id: string): Promise<T>;
    // ...
}

export interface AircraftRepository extends Repository<Aircraft> {
    // Add custom methods here ...
}


Enter fullscreen mode Exit fullscreen mode

At this point, our dependency graph looks as follows:

We now need to implement the Repository<T> interface and the AircraftRepository interface:

  • Repository<T> is going to be implemented by a class named GenericRepositoryImpl<D, E>

  • AircraftRepository is going to be implemented by a class named AircraftRepositoryImpl.

Let's start by implementing Repository<T>:



import { injectable, unmanaged } from "inversify";
import { Repository } from "@domain/interfaces";
import { EntityDataMapper } from "@dal/interfaces";
import { Repository as TypeOrmRepository } from "typeorm";

@injectable()
export class GenericRepositoryImpl<TDomainEntity, TDalEntity> implements Repository<TDomainEntity> {

    private readonly _repository: TypeOrmRepository<TDalEntity>;
    private readonly _dataMapper: EntityDataMapper<TDomainEntity, TDalEntity>;

    public constructor(
        @unmanaged() repository: TypeOrmRepository<TDalEntity>,
        @unmanaged() dataMapper: EntityDataMapper<TDomainEntity, TDalEntity>
    ) {
        this._repository = repository;
        this._dataMapper = dataMapper;
    }

    public async readAll() {
        const entities = await this._repository.readAll();
        return entities.map((e) => this._dataMapper.toDomain(e));
    }

    public async readOneById(id: string) {
        const entity = await this._repository.readOne({ id });
        return this._dataMapper.toDomain(entity);
    }

    // ...

}


Enter fullscreen mode Exit fullscreen mode

This particular Repository<T> implementation expects an EntityDataMapper and a TypeOrmRepository to be injected via its constructor. Then it uses both dependencies to read from the database and map the results to domain entities.

We also need the EntityDataMapper interface:



export interface EntityDataMapper<Domain, Entity> {

    toDomain(entity: Entity): Domain;
    toDalEntity(domain: Domain): Entity;
}


Enter fullscreen mode Exit fullscreen mode

And the EntityDataMapper implementation:



import { toDateOrNull, toLocalDateOrNull } from "@lib/universal/utils/date_utils";
import { Aircraft } from "@domain/entitites/aircraft";
import { AircraftEntity } from "@dal/entities/aircraft";
import { EntityDataMapper } from "@dal/interfaces";

export class AircraftDataMapper implements EntityDataMapper<Aircraft, AircraftEntity> {

    public toDomain(entity: AircraftEntity): Aircraft {
        // ...
    }

    public toDalEntity(mortgage: Aircraft): AircraftEntity {
        // ...
    }
}



Enter fullscreen mode Exit fullscreen mode

We use the EntityDataMapper to map from the entities returned by the TypeOrmRepository to our domain entities. At this point, our dependency graph looks as follows:

We can finally implement AircraftRepository:




import { inject, injectable } from "inversify";
import { Repository as TypeOrmRepository } from "typeorm";
import { AircraftRepository } from "@domain/interfaces";
import { Aircraft } from "@domain/entitites/aircraft";
import { GenericRepositoryImpl } from "@dal/generic_repository";
import { AircraftEntity } from "@dal/entities/aircraft";
import { AircraftDataMapper } from "@dal/data_mappers/aircraft";
import { TYPE } from "@dal/types";

@injectable()
export class AircraftRepositoryImpl
    extends GenericRepositoryImpl<Aircraft, AircraftEntity>
    implements AircraftRepository {

    public constructor(
        @inject(TYPE.TypeOrmRepositoryOfAircraftEntity) repository: TypeOrmRepository<AircraftEntity>
    ) {
        super(repository, new AircraftDataMapper())
    }

    // Add custom methods here ...

}



Enter fullscreen mode Exit fullscreen mode

At this point, we are done, and our dependency graph looks as follows:

The preceding diagram uses colors to identify concretions (classes, blue) and abstractions (interfaces, orange):

The following diagram uses colors to identify a component that belongs to the domain layer (green) and components that belong to the infrastructure layer (blue):

This architecture has worked very well for me in large enterprise software projects over the last ten years. I also ended up breaking up some colossal monolithic onions into microservices that follow the same architecture. I like to say that when we have microservices that implement the onion architecture, we have a "bag of onions".

I hope you enjoyed the article! Please let me know your thoughts using the comments or at @RemoHJansen.

Top comments (48)

Collapse
 
jbogard profile image
Jimmy Bogard

So just an FYI - the project that originated the definition, I was the tech lead on, and we ripped out this style of architecture after about 6 months. For us it didn't scale with complexity. I talked about what we moved to at NDC vimeo.com/131633177

The long and short of it - layers don't encapsulate, they abstract. When we moved to CQRS-style architectures, just the object models that is, we were able to ditch all those service/repository layers in favor of queries and commands. The result was truly SOLID, as opposed to our onion architecture, which I finally saw as a gross misunderstanding of SOLID and OO principles.

Collapse
 
remojansen profile image
Remo H. Jansen • Edited

I just watched your talk and maybe I'm getting it wrong but it seems like you are not happy about the n-tier architecture, not the onion architecture. They are actually quite different (I have also experienced the n-tier pain).

n-tier

onion

More info at blog.ploeh.dk/2013/12/03/layers-on...

One of the things you mention is that you are able to change the used ORM tool. In the onion architecture, this is perfectly possible. In fact, we migrated from sequelize to TypeORM and it was a small job.

When a system becomes more complex, the onion architecture can be combined with the CQRS and the Unit of work patterns and when it becomes even more complex it can be split into "a bag of onions".

Collapse
 
jbogard profile image
Jimmy Bogard

No, this was an onion architecture, your second picture, not n-tier. Like literally the dude that invented the term, this was the project and we moved away after a few months.

So we start with CQRS and only add any kind of layering like onion as necessary. It's just been 8 years going CQRS-first, and have yet to really need any layers. No abstractions, nothing like a repository or service layer, nothing like that. Just unnecessary.

Collapse
 
hdennen profile image
Harry Dennen • Edited

"The long and short of it - layers don't encapsulate, they abstract." Effing nail on the head there mate. We've been slowly massaging our front end away from onion because of exactly that over-abstraction to the point of sprint velocity crushing complexity. It also lead to a lot of premature generalization.

Collapse
 
ajoshi31 profile image
Atul Joshi

I saw your video and seems its like coming back to square one after all the DDD, that that patterns, just simple separation of concern .
In one of my earlier project I was doing dividing the project based on the controller, model and service structure, then in new version I thought the feature separation will help, however eventually I felt that feature separation was making me more concerned as where to keep some files or function.

Like some functions need to be dependent on two or more features, where to create that folder structure, then many a times comes a lot of cross dependability, how those things can be taken care?

In that case I feel that a particular functionality instead of feature works better.

Collapse
 
alexmreis profile image
Alex Reis

Congratulations, you have now turned JavaScript into a shittier version of Java.

I have had this discussion before with friends that started on dynamic languages and are now enamored with types and the SOLID principles. Dependency injection and nonsense like 300 interfaces are the things that drove half of the Java community to move to Ruby and Python 10 uears ago.

I personally love JavaScript for it's practicality and adaptness. Tieing it up with types is bad enough, and if you add DI and interfaces all around the place, it loses everything that makes JS more interesting as a language than Java or C#.

Coming up next, Fizz Buzz Enterprise Edition in Typescript, because serious node.js code is written by serious businessmen to support serious businesses.

Honestly though, I recommend you read Eloquent Ruby to learn new idioms that make sense on dynamic languages.

Collapse
 
remojansen profile image
Remo H. Jansen • Edited

I respect your point of view but my personal experience is radically different. When I have something like the following:

import { inject } from "inversify";
import { response, controller, httpGet } from "inversify-express-utils";
import * as express from "express";
import { AircraftRepository } from "@domain/interfaces";
import { Aircraft } from "@domain/entitites/aircraft";
import { TYPE } from "@domain/types";

@controller("/api/v1/aircraft")
export class AircraftController {

    @inject(TYPE.AircraftRepository) private readonly _aircraftRepository: AircraftRepository;

    @httpGet("/")
    public async get(@response() res: express.Response) {
        try {
            return await this._aircraftRepository.readAll();
        } catch (e) {
            res.status(500).send({ error: "Internal server error" });
        }

    }

    // ...

}
Enter fullscreen mode Exit fullscreen mode

There are a few things that I wouldn't get without types and without dependency injection:

  • I can do TDD / design by contract with confidence. If I'm implementing a type, the first thing that I can do is click on "Implement interface" on my IDE and it will create a default implementation. If my implementation is a violation of the interface I will get a compilation error.

  • While I implement this controller, another engineer in my team can be creating an implementation of AircraftRepository. We can work in parallel and we know that there will be no integration issues because the interface is a contract between us that cannot be violated by either side.

  • I can use declarative routing @controller("/api/v1/aircraft") without the need to implement a factory for my controller by hand.

  • I have a lot of metadata that can be used for things like generating UML diagrams or Open API definitions.

  • If I need to refactor something I can do it with confidence, a change in the contract will break all the related parts. I know exactly what is left for my refactoring to be completed.

  • I can also use InversifyJS to implement interception (This is not documented in the example but it is supported by inversifyJS). This allows implementing an aspect (e.g., logging) while keeping my entire code base free of logging concerns.

These advantages make a difference for me and I enjoy them. This is why I advocate this approach and I'm sure I'm not alone because there are a lot of emerging Node.js libs and frameworks in this space and some of them are becoming very popular (e.g, nestjs.com/).

Collapse
 
bmarkovic profile image
Bojan Markovic • Edited

I can agree that if you are used to that way of working and actually enjoy it (personally I'm a J2EE refugee and enjoy the fact that I've left that world of pain), sure, have a blast.

But the thing is that almost every meaningful feature in this approach already has a simpler, more streamlined and more idiomatic alternative in Javascript.

For example, DI is useful for testing. But there are testing-oriented DI implementations for JavaScript that do it much nicer than the square-peg-round-hole approaches like InversifyJS. I'm talking SinonJS, Testdouble.js and Steal.js.

There are numerous AOP solutions for JavaScript like Aspect.js or Meld that are both simpler and more idiomatic than DI inception based ones and things like Express' Debug clearly demonstrate that where AOP is needed it can be added without needless OOP overengineering and it's actually the anyithing-goes, dynamic nature of the language/runtime that massively helps in this regard (higher order functions alone make a world of difference).

In actuality an API is a contract. OO interfaces are really just one way to go about them and not necessarily a better one. But as IDE support is a given using interfaces, I can imagine circumstances where that might be preferable to documenting an API or adhering to agreements, but IRL I've never run across such a situation, yet I've dealt with incredibly slow progress and numerous pain-points in OO-heavy codebases using these patterns (despite working with stellar engineers).

I'm not even interested in going into a debate on typing. The thing is that there are no conclusive proofs that types reduce bugs at all (but abandoning OOP for functional programming apparently does), and one can sprinkle Typescript or native type annotations where one finds them needed without full-on commitment (where lack of strict types has actually helped dynamic languages and JSON to flourish in this world of distributed software and fast-changing APIs).

The "without the need to implement a factory" and "generating UML diagrams" bits in your comment do say a lot too.

Just my 2c.

Thread Thread
 
godsavethewww profile image
Derek Branizor • Edited

History lesson...Microsoft Windows is built as a bundle of exposed API's -- unlike linux with text config files -- and they caused immense pain in the 90's by breaking their bogus contracts. So I beg to differ, API's are not real contracts. In fact I'm sure the heinous behavior of Microsoft's APIs were one of the reasons Java made interfaces a big part of its paradigm.

Collapse
 
falagan profile image
Eloy PΓ©rez • Edited

Take it easy. Here, in the article, thereΒ΄s some work and time invested trying to explain another point of view about node and how to build software in this platform. So, respect for other opinions. I respect what you`re saying about JS and how people work with it taking advance of the flexibility that provides...but this requires a very advanced skills and discipline to maintain the state of the soft clean, stable and readable for others. You must be really good on JS (sure you are), but for others like me who are not that proficient on JS we appreciate solutions like this or Nest that allow us to build (yes, the same house, with the same structure, features etc many times...) projects safely, with rail guidance etc because sometimes you can not wait to be a rock and roll programming star to build and finish an app. With JS you can build custom fancy houses, great... but sometimes you have to build a block hundred identical houses with many workers and in this scenario, Remo solution, fits very well in terms of productivity. I think that it was not RemoΒ΄s intention to offend "node's pures engineers" ;)

Collapse
 
ydennisy profile image
Dennis

Hi Alex,
I sort of agree with your view. However could you suggest some alternatives patterns to better structure node apps?

Collapse
 
luisnoresv profile image
Luis Nores

So you will probably hate DENO too

Collapse
 
ewnx01 profile image
Drunken Dev

Really nice article and it's about time to introduce methods like this to the JavaScript developers.

Actually I'm not surprised, that some or a lot of JavaScript developers are complaining about it. My experience is that it is very hard for them to understand what design are for and what benefit they have. Especially whe it comes to bigger or more complex applications.
It was, and sometimes still is, the same in other languages. For example I know PHP developers who likes global functions, unclean architecture or everything with static calls, or don't use things like namespaces right.

Collapse
 
btomoretti profile image
Mariano Moretti

First of all, great post! But, i can't understand why you shouldn't use java instead of convert javascript into something similar to java. I mean, i can understand why use ts in frontend since you don't have the java possibility.. but, why should i use it in backend? And i'm just asking this because i want to understand and share ideas :)

Collapse
 
remojansen profile image
Remo H. Jansen • Edited

Well, I can enjoy some of the things I like about Node.js / JavaScript / TypeScript:

  • I love JavaScript.
  • I love static types.
  • I enjoy not having to think about threads and deadlocks (non-blocking I/O and single threaded).
  • I like being able to share code between my front end and my backend (e.g., some interface declarations and domain entities).
  • I like the speed of change of the JavaScript community (for some it is fatigue for me it is excitement).
  • I like the fact that Node.js is self-hosted (don't have to deal with tomcat or anything like that).
  • I like that the whole thing is very lightweight (except the node_modules folder xD).
  • I like that being able to execute client-side code on the server side (e.g., React server-side rendering)

But I also get some of the benefits of Java / C#. I had many conversations with many engineers and some of them are completely against things like dependency injection in JavaScript.

They argue that it is not needed because in JavaScript you can monkey patch everything if you need to mock something during a unit test and the same applies to languages like Ruby. For example, you can read this post.

On the other hand, you have people like me or Rob Wormald:

β€œTypically when I go to JavaScript conferences and I talk about dependency injection people are like ΒΏWhat?, ΒΏWhat is dependency injection? ΒΏWhy would you do that?... I love coming Build (Microsoft conference for developers) because I can talk about dependency injection and everybody in this room is probably convinced that it is a good idea” - Rob Wormald, Developer Advocate @ Google

This is cool for me, after a long time thinking about this topic (I'm quite passionate about this topic), I have reached the conclusion that it is not right or wrong. If monkey patching works well for you and your team that is awesome. I stand on the other side: monkey patching and lack of static types don't work well for me.

I appreciate questions like yours because as I said I love thinking about this topic :) When I wrote this article I wasn't saying "This is the way you should build Node.js apps", what I was trying to say was "This is the way I've been building very large monolithic apps, with large teams and it has been working very well so far".

Collapse
 
btomoretti profile image
Mariano Moretti

Yes, I totally understand what you are saying. DI it's pretty cool and it's something that I suffered while I was a working with Ruby. I mean, you can redefine methods or classes at execution time when you need it. But it could be pretty complex at the beginning.

Like you say, there's not good or right for this kinds of topics.
Thanks for your answer!

Collapse
 
liranbri profile image
Liran • Edited

thank you for a great article!

few points:

  1. don't you think it smells that you need to have your IoC framework decorators inside the lowest level layers such as the domain? Uncle Bob in Clean Architecture specifically suggests to avoid that.. the IoC is also just an implementation detail.

  2. I couldn't find where you use the "Application Layer". isn't it missing?
    for example "create new Aircraft" should not be considered a use-case belongs to the Application Layer?

As far as I understand, domain services should be introduced when you have a domain business rules. but in your case, you put there operations which don't hold any business rules ("get all aircrafts" for example). I think it indicates that it does not belong there.

I even picked at the demo you shared (github.com/stelltec/public-tech-de...) and there is not even a folder for this layer.

on this article:
blog.codeminer42.com/nodejs-and-go...
(you can skip to this code at: github.com/talyssonoc/node-api-boi...)
The guy put those operations in the application layer for example. do you think he's wrong?

Collapse
 
mpachin profile image
Misha Pachin

Thank you so much for your comment and for your questions and links! It's really helpful, especially this one (github.com/talyssonoc/node-api-boi...)

Collapse
 
azumelzu profile image
Andres Zumelzu

Hi Remo! Great post! Thanks for sharing!!

Is the source code available somewhere? i.e github, etc?

Thanks!

Collapse
 
remojansen profile image
Remo H. Jansen

Not the one described in this post because is private but I have a small demo is much more simplified and rudimentary but it can help you to get an idea github.com/stelltec/public-tech-de...

Collapse
 
turcicjosip profile image
Josip Turcic

Wow...Really good work with the blog, finally found an example that totally makes sense. Thank you so much, been searching ages for a decent JS example that ties in with DDD and SOLID. Would love to see the source code for the actual blog. Saw the small demo example and it is very neat. I don't understand why others cant see the benefits of DDD and SOLID. Is there a way to see the private example?

Collapse
 
azumelzu profile image
Andres Zumelzu

Awesome! Thanks!

Collapse
 
jonasalbert profile image
jonasalbert

what is the project template did you use in your sample source code? thanks.

Thread Thread
 
remojansen profile image
Remo H. Jansen

The code samples are based on some proprietary code that we created at my current job. I cannot share the original source code as I described already. Sorry.

Collapse
 
blouzada profile image
Bruno Louzada

Extremely nice post! Well explained SOLID and onion architecture with good examples, thanks!

Collapse
 
remojansen profile image
Remo H. Jansen

Thanks! :)

Collapse
 
donkeykong91 profile image
Daryl Occ

I think you forgot to add the Serializable interface to the Rectangle class in the Interface Segregation Principle section so that it can use the serialize() method.

Collapse
 
remojansen profile image
Remo H. Jansen

Thanks a lot for the heads up!

Collapse
 
donkeykong91 profile image
Daryl Occ

Yup yup!

Great article by the way! πŸ‘

Collapse
 
lephuocmy668 profile image
My Le Phuoc

Any one know what is @dal?

Collapse
 
remojansen profile image
Remo H. Jansen

DAL means "data access layer" and is an alias for a path. Check this out to learn how to create aliases for paths in TS stackoverflow.com/questions/432817...

Collapse
 
brycepc profile image
BrycePC

A great article, thanks; I'm pleased to say much of this looks similar to what we have been using for a little while; ie Onion Architecture with Nodejs + Inversify with appropriate scaffolding oriented around DDD. Its worked really well for us for producing decoupled and extensible solutions (having also experienced 'Big Ball of Mud' consequences in the past).