Skip to content

NazarioLuis/js-clean-architecture

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

17 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Clean Architecture Implementation in NodeJS

Clean Architecture is a software architectural approach that emphasizes the separation of concerns and the independence of dependencies within a system. The main idea behind Clean Architecture is to design software systems in a way that allows for easy maintenance, scalability, and testability, while also promoting a clear understanding of the system's structure and behavior.

Layers

In a Clean Architecture setup, the system is divided into layers, each with its own specific responsibility. This repository contains a base implementation of Clean Architecture in JavaScript using NodeJS. The code includes three layers of abstraction:

  • Domain Layer: This is where the models and business rules are implemented via the behavior of the models.
  • Application Layer: This is where the application logic is implemented according to use cases.
  • Infrastructure Layer: This is where the persistence strategy is implemented using the models from the domain layer.

Capas

Communication Between Layers

The communication between layers is represented by the following diagram.

ComunicaciĂłn

Dependencies

  • awilix
  • mocha

Utility for Module Import

This utility module provides a function for importing multiple modules dynamically from a directory

util/import-modules.js

const fs = require('fs');
const path = require('path');

module.exports = function (filename, dirname) {
    const imports = [];
    const basename = path.basename(filename);
    fs.readdirSync(dirname)
        .filter(file => (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js'))
        .forEach(file => {
            const mod = require(path.join(dirname, file));
            imports.push(mod);
        });
    return imports;
};

Domain Layer

Helper

This module contains helper functions commonly used across the domain layer, such as a function to check for required parameters and a function to throw an error for unimplemented methods.

domain/helper.js

const REQUIRED = (attr) => {
    if (attr === undefined) throw new Error('ERR_REQUIRED_PARAM');
    return attr;
};

const NON_IMPLEMENTED = () => {
    throw new Error('ERR_METHOD_NOT_IMPLEMENTED');
};

module.exports = { REQUIRED, NON_IMPLEMENTED };

Models

This module defines the User model class, representing a user entity with attributes such as id, firstname, lastname, etc.

domain/models/user.model.js

const { REQUIRED } = require('../helper');

class User {
    constructor({ id = 0, firstname, lastname, nick, pass, createdAt, updatedAt }) {
        this.id = id;
        this.firstname = REQUIRED(firstname);
        this.lastname = REQUIRED(lastname);
        this.nick = REQUIRED(nick);
        this.pass = pass;
        this.createdAt = createdAt;
        this.updatedAt = updatedAt;
    }
}

module.exports = User;

Behavior

This module defines the behavior interface for user-related operations such as getAll, get, create, update, and delete. These methods are placeholders and should be implemented by concrete classes.

domain/behavior/user.behavior.js

const { NON_IMPLEMENTED } = require('../helper');

class UserBehavior {
    getAll = () => NON_IMPLEMENTED();
    get = id => NON_IMPLEMENTED();
    create = entity => NON_IMPLEMENTED();
    update = (entity, id) => NON_IMPLEMENTED();
    delete = id => NON_IMPLEMENTED();
}

module.exports = UserBehavior;

Application Layer

User Interactor

This module acts as an intermediary between the application layer and the domain layer, handling user-related use cases. It encapsulates the business logic associated with user operations, such as user creation, retrieval, updating, and deletion.

application/user.interactor.js

const UserBehavior = require('../domain/behavior/user.behavior');
const User = require('../domain/models/user.model');

class UserInteractor extends UserBehavior {
    constructor({ UserRepository }) {
        super();
        this._entityRepo = UserRepository;
    }

    getAll = () => this._entityRepo.getAll();
    get = id => this._entityRepo.get(id);
    create = entity => this._entityRepo.create(new User(entity));
    update = (entity, id) => this._entityRepo.update(new User(entity), id);
    delete = id => this._entityRepo.delete(id);
}

module.exports = UserInteractor;

Application Index

This file dynamically imports all modules from the application layer using the importModules utility function.

application/index.js

const importModules = require('../util/import-modules');
const infrastructureModules = importModules(__filename, __dirname);
module.exports = infrastructureModules;

Infrastructure Layer

User Repository

This module defines the persistence strategy for user entities. It encapsulates data access logic and implements CRUD (Create, Read, Update, Delete) operations on user data.

infrastructure/repositories/user.repository.js

const UserBehavior = require('../../domain/behavior/user.behavior');

class UserRepository extends UserBehavior {
    users = [
        { id: 1, firstname: "Eddard", lastname: "Stark", nick: "ned", pass: "123" },
        { id: 2, firstname: "Catelyn", lastname: "Tully", nick: "cat", pass: "123" }
    ];

    getAll = () => this.users;
    get = id => this.users.find(x => x.id == id);
    create = entity => {
        const newUser = { ...entity, id: this.users[this.users.length - 1].id + 1 };
        this.users.push(newUser);
        return newUser;
    };
    update = (entity, id) => {
        const index = this.users.findIndex(x => x.id == id);
        this.users[index] = { ...entity, id: this.users[index].id };
        return this.users[index];
    };
    delete = id => {
        const index = this.users.findIndex(x => x.id == id);
        this.users.splice(index, 1);
        return id;
    };
}

module.exports = UserRepository;

Repository Index

This file dynamically imports all modules from the repository layer using the importModules utility function.

infrastructure/repositories/index.js

const importModules = require('../../util/import-modules');
const infrastructureModules = importModules(__filename, __dirname);
module.exports = infrastructureModules;

Dependency Injection Container

Container

This module configures a dependency injection container using Awilix. It registers application and repository modules as dependencies, facilitating dependency injection across the application.

container.js

const awilix = require('awilix');
const applicationModules = require('./application');
const infrastructureModules = require('./infrastructure/repositories');

const modules = [
    ...applicationModules,
    ...infrastructureModules,
];

const container = awilix.createContainer();

const resolvers = {};
modules.forEach(module => {
    if (module.toString().substring(0, 5) === 'class') {
        resolvers[module.name] = awilix.asClass(module).singleton();
    } else {
        resolvers[module.name] = awilix.asFunction(module).singleton();
    }
});

container.register(resolvers);

module.exports = container;

Testing

This module contains test cases for the user-related functionality implemented in the application layer. It uses the Mocha framework for test execution and assertion.

User Tests

test/user.test.js

var assert = require('assert');
const interactor = require('../container').cradle.UserInteractor;

describe("User", function () {
    describe("CRUD operations", function () {
        it("create", function () {
            const result = interactor.create({
                firstname: "John",
                lastname: "Snow",
                nick: "snow",
                pass: "123",
            });
            assert.strictEqual(result.id > 0, true);
        });

        it("get all", function () {
            const result = interactor.getAll();
            assert.strictEqual(result.length > 0, true);
        });

        it("get", function () {
            const result = interactor.get(1);
            assert.strictEqual(result == null, false);
        });

        it("update", function () {
            const result = interactor.update({
                firstname: "Sansa",
                lastname: "Stark",
                nick: "sansa",
                pass: "123",
            }, 1);
            const aux = interactor.get(1);
            assert.strictEqual(result.firstname, aux.firstname);
        });

        it("delete", function () {
            const deletedId = interactor.delete(1);
            const result = interactor.get(deletedId);
            assert.strictEqual(result == null, true);
        });
    });
});

This implementation covers the basic structure and flow of Clean Architecture in NodeJS, focusing on separation of concerns and modularity. The provided code includes models, behaviors, repositories, and tests to demonstrate how each layer interacts with one another.