Published on

Clean Architecture with Node.js and TypeScript: A Practical Guide for Modern Developers

Authors
  • avatar
    Name
    Jonas de Oliveira
    Twitter

In an increasingly complex development landscape, keeping your code organized, modular, and easy to maintain is essential for building scalable, high-quality systems. Clean Architecture is an approach that promotes the separation of concerns, making your code more testable, flexible, and maintainable. In this article, we'll explore the concepts behind Clean Architecture and how to implement it in a Node.js project using TypeScript. You'll see practical, commented examples that illustrate each layer and demonstrate how they interact.

1. What is Clean Architecture?

Clean Architecture, popularized by Robert C. Martin (Uncle Bob), divides an application into well-defined layers, each with a specific responsibility. This separation promotes:

  • Framework Independence: The system doesn’t rely on external library details.
  • Testability: Business rules are isolated, making unit testing easier.
  • Flexibility: Changes in one layer have little to no impact on others.
  • Maintainability: Organized, modular code simplifies future updates and collaboration.

2. Folder Structure

A well-structured project following Clean Architecture principles should be organized as follows:

/clean-architecture-nodejs
│── /src
│   │── /domain                # Domain Layer (Entities and Repositories)
│   │   │── User.ts            # User Entity
│   │   │── IUserRepository.ts  # User Repository Interface
│   │
│   │── /application           # Application Layer (Use Cases)
│   │   │── UserService.ts      # User Service (Use Cases)
│   │
│   │── /infrastructure        # Infrastructure Layer (Database, APIs, etc.)
│   │   │── /repositories
│   │   │   │── InMemoryUserRepository.ts  # In-Memory Repository
│   │   │── /database
│   │   │   │── connection.ts   # Database Connection (PostgreSQL, MongoDB, etc.)
│   │   │── /config
│   │   │   │── env.ts          # Environment Variables Configuration
│   │
│   │── /interface             # Interface Layer (Controllers, Routes, Presentation)
│   │   │── /controllers
│   │   │   │── UserController.ts  # User Controller (Express)
│   │   │── /routes
│   │   │   │── userRoutes.ts       # Routes Definition
│   │
│   │── server.ts               # Server Initialization (Express)
│── /tests                      # Unit and Integration Tests
│   │── /domain                 # Tests for the Domain Layer
│   │── /application            # Tests for Use Cases
│   │── /interface              # Tests for Controllers and Routes
│── /config                     # General Project Configuration
│   │── tsconfig.json            # TypeScript Configuration
│   │── eslint.json              # ESLint Configuration
│   │── jest.config.js           # Jest Configuration for Testing
│── /scripts                     # Auxiliary Scripts (Migrations, Seeders)
│── .env                         # Environment Variables
│── .gitignore                    # Files to Ignore in Git
│── package.json                  # Dependencies and npm Scripts
│── README.md                     # Project Documentation

This structure ensures clear separation of concerns and enhances maintainability.

3. Layers of Clean Architecture

A common structure divides the application into four primary layers:

  1. Domain (Entities): Contains the core business rules and entities. This layer is independent of any external frameworks.
  2. Use Cases (Application): Implements the specific business logic of your application. It orchestrates how the domain rules are applied.
  3. Interface (Interface Adapters): Handles communication with the outside world, such as controllers, gateways, presenters, and views. Adapters convert data to and from the domain’s format.
  4. Infrastructure (Frameworks & Drivers): Manages external dependencies like databases, web frameworks, and third-party services. This layer is the most volatile and can change without affecting the core business rules.

4. Practical Example with Node.js and TypeScript

Below is a simplified example of a Clean Architecture structure in a Node.js project. The example is a user management API that separates responsibilities into different layers.

4.1 Domain Layer

Create a file for the entity and a repository interface.

// src/domain/User.ts

/**
 * Represents the User entity in the domain.
 */
export class User {
  constructor(
    public id: number | null, // 'null' indicates that the ID will be generated by the repository
    public email: string,
    public name: string
  ) {}
}
// src/domain/IUserRepository.ts

import { User } from "./User";

/**
 * Interface defining the methods for the user repository.
 */
export interface IUserRepository {
  create(user: User): Promise<User>;
  findAll(): Promise<User[]>;
  update(user: User): Promise<User>;
  delete(id: number): Promise<void>;
}

4.2 Application Layer

Create a use case for user operations.

// src/application/UserService.ts

import { User } from "../domain/User";
import { IUserRepository } from "../domain/IUserRepository";

/**
 * UserService contains use cases related to user operations.
 * This layer orchestrates business rules using the domain repository.
 */
export class UserService {
  constructor(private userRepository: IUserRepository) {}

  async createUser(email: string, name: string): Promise<User> {
    // Create a new User instance (ID is null since it will be generated)
    const newUser = new User(null, email, name);
    return await this.userRepository.create(newUser);
  }

  async getAllUsers(): Promise<User[]> {
    return await this.userRepository.findAll();
  }

  async updateUser(id: number, email: string, name: string): Promise<User> {
    const updatedUser = new User(id, email, name);
    return await this.userRepository.update(updatedUser);
  }

  async deleteUser(id: number): Promise<void> {
    return await this.userRepository.delete(id);
  }
}

5. Running the Project

Compile and start the server:

npx ts-node src/server.ts

Test the API using tools like Postman or cURL to send HTTP requests to your endpoints and test CRUD operations.

6. Final Thoughts

Implementing Clean Architecture with Node.js and TypeScript enables you to build robust, maintainable systems where each layer has a clearly defined responsibility. By separating business logic, use cases, infrastructure, and interface, your application becomes easier to test, scale, and modify.