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.
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.
The communication between layers is represented by the following diagram.
awilix
mocha
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;
};
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 };
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;
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;
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;
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;
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;
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;
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;
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.
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.