Clean Architecture Implementation in NodeJS
NazarioLuis
Posted on June 11, 2024
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.
*Repository: *https://github.com/NazarioLuis/js-clean-architecture/
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.
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.
Posted on June 11, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.