Hexagonal Architecture in Node.js: A Comprehensive Guide

Hexagonal Architecture in Node.js: A Comprehensive Guide

The Hexagonal Architecture, also known as Ports and Adapters, was proposed by Alistair Cockburn with the aim of ensuring that the core of an application remains independent of external interfaces and technologies, such as databases, APIs, or even user interfaces. In the context of Node.js, this architecture provides significant benefits, including flexibility, scalability, and ease of maintenance—making it an ideal approach for modern, modular systems.


Why Use Hexagonal Architecture in Node.js?

By adopting this model, we can ensure that the core of the application (where the business logic resides) can evolve independently of external interfaces such as databases, APIs, authentication systems, etc.


How Hexagonal Architecture Works

Hexagonal Architecture consists of three main elements:

Ports: These are interfaces that define how the system interacts with the outside world. There are two main types of ports:

Inbound Ports: Responsible for receiving interactions from the outside world, such as API calls or user commands.

Outbound Ports: Responsible for interacting with external systems, such as databases or third-party services.

Adapters: Concrete implementations that connect the ports to the external world. They transform data between the system and external components.

Inbound Adapters: Connect the inbound interfaces (e.g., an HTTP controller in Express) to the inbound ports.

Outbound Adapters: Connect the outbound ports to external systems (such as a database).

Core of the Application: This is where the business logic of the application resides. The core is completely independent of the adapters and ports, allowing new technologies to be integrated easily without affecting the central logic.

The main advantage of this architecture is the decoupling between the core of the application and external technologies. This allows you to easily swap or replace external technologies (like databases, caching systems, or even front-end frameworks) without affecting the business logic of the application.


Example Technologies for Ports and Adapters

Inbound Ports:

  • Express.js: For handling HTTP requests.
  • GraphQL: For providing a flexible API layer.

Outbound Ports:

  • MongoDB: For NoSQL database storage.
  • PostgreSQL: For relational database storage.
  • Redis: For caching and session management.
  • Third-party APIs: For integrating external services like payment gateways (e.g., Stripe, PayPal).

Inbound Adapters:

  • Express Controller: Adapts the Express routes to interact with inbound ports.

Outbound Adapters:

  • Mongoose Adapter: Adapts the MongoDB library to interact with outbound ports.
  • Sequelize Adapter: Adapts Sequelize ORM for PostgreSQL.
  • Axios Adapter: Adapts the Axios library for making HTTP requests to external services.


Advantages:

  • Decoupling: The core of the application is isolated from external technologies, enabling the swap of technologies without affecting business logic.
  • Ease of Testing: The decoupling makes unit testing easier, as the core can be tested in isolation without depending on external systems like databases or APIs.
  • Scalability and Flexibility: Hexagonal Architecture allows the system to grow modularly, with new adapters being added as needed without affecting the core functionality.
  • Easier Maintenance: The system is easier to maintain and evolve over time, as changes in one part of the system (e.g., the database) do not impact the core logic.
  • Easy Technology Swap: The architecture facilitates technology replacement. For example, if you decide to switch from MongoDB to PostgreSQL, you can do so easily without affecting the core business logic.


Disadvantages:

  • Learning Curve: For developers unfamiliar with the concept of decoupling components, Hexagonal Architecture can be challenging initially.
  • Additional Complexity: For simple systems, implementing this architecture might be overkill, requiring more planning and organization than a simple monolithic approach.
  • Overhead for Small Systems: For small applications or MVPs (Minimum Viable Products), the overhead of defining multiple ports and adapters may not be justified.


How to Implement Hexagonal Architecture in Node.js?

Implementing a Node.js application using Hexagonal Architecture involves creating a modular structure that separates the business logic from external components. Here’s a basic example of how to organize the code:


Project Structure

src/
├── core/
│   ├── domain/
│   ├── services/
│   └── useCases/
├── adapters/
│   ├── input/
│   │   └── routes.ts
│   └── output/
│       └── database.ts
├── ports/
│   ├── input/
│   │   └── UserPort.ts
│   └── output/
│       └── UserRepository.ts
└── index.ts        

Inbound Port

// src/ports/input/UserPort.ts
export interface UserPort {
  createUser(userData: { name: string; email: string }): Promise<void>;
}        

Outbound Port

// src/ports/output/UserRepository.ts
export interface UserRepository {
  saveUser(userData: { name: string; email: string }): Promise<void>;
}        

Inbound Adapter

// src/adapters/input/routes.ts
import { UserPort } from '../../ports/input/UserPort';
import { Router } from 'express';

export class UserRoutes {
  constructor(private userPort: UserPort) {}

  public initRoutes(router: Router) {
    router.post('/users', async (req, res) => {
      await this.userPort.createUser(req.body);
      res.status(201).send();
    });
  }
}        

Outbound Adapter

// src/adapters/output/database.ts
import { UserRepository } from '../../ports/output/UserRepository';

export class MongoUserRepository implements UserRepository {
  async saveUser(userData: { name: string; email: string }): Promise<void> {
    // Here you can use Mongoose or any other database library
    console.log('Saving user to MongoDB:', userData);
  }
}        

Core Application

// src/core/services/UserService.ts
import { UserPort } from '../../ports/input/UserPort';
import { UserRepository } from '../../ports/output/UserRepository';

export class UserService implements UserPort {
  constructor(private userRepository: UserRepository) {}

  async createUser(userData: { name: string; email: string }): Promise<void> {
    await this.userRepository.saveUser(userData);
  }
}        


Changing the Database in Node.js with Hexagonal Architecture

Let’s say you are using MongoDB initially and want to switch to PostgreSQL in the future. With Hexagonal Architecture, you would only need to create a new outbound adapter for PostgreSQL and adjust the UserRepository. The core application, where the business logic resides, would remain untouched.


Conclusion

Hexagonal Architecture is an excellent choice for Node.js applications that need to be modular, scalable, and easy to maintain. By decoupling business logic from external interfaces, this architecture makes it easy to swap technologies, add new features, and perform unit testing. While there may be an initial learning curve and additional complexity for simple systems, it provides a robust foundation for applications that need long-term flexibility and scalability.





Hey Igor... Do you have a public github repo that implements a well strucuture and large project using Hexagonal architecture? I just found several repos but implementing basic examples. I was looking for a complex and huge project to see how to manage the complexity using hexagonal architecture in microservices.... thanks

Like
Reply

To view or add a comment, sign in

More articles by Igor Matsuoka

Explore content categories