This is the fourth article in our series on clean architecture (the previous ones are listed at the bottom of the page), and we're getting more technical with an introduction to implementing the principles in a Node.JS API with Express.
Let's take a look at the overall structure of a project and the details of the main concepts (entities, adapters, injection...).
Organization of main directories
Let's start by taking a look at the general directory structure. As is often the case, all our API source code is grouped together in the "/src" directory. Next, let's take a look at the Core vs. Infrastructure distinction:
The heart of the application in "/core"
This is where use cases, business entities and business rules reside. Not dependent on any external framework or library, it represents the bare minimum needed to run our application: it's the functional core of our application. It is itself divided into several sub-directories.
Use cases
In "/core/use-cases", we find the business use cases. Each use case has its own typescript file with, if required, its own input and output types (what goes into the use case and what comes out). There are two schools of thought:
- A sub-folder with a grouping of use cases. Example: A user subfolder in which I group all user cases.
- All use cases at the root of use-cases, with no particular tree structure.
I personally prefer the second solution, all "flat", for the simple reason that you never have to ask yourself the question "where do I put / where has this use case been put?". A simple example: do you put a user's address in "user" or "address"? There are as many answers as there are developers, so you might as well not put a sub-folder!
An example of a use case that allows you to obtain a book by its id:
class GetBook {
private bookRepository: BookRepository;
private logger: Logger;
constructor() {
this.bookRepository = container.resolve<BookRepository>('BookRepository');
this.logger = container.resolve<Logger>('Logger');
}
async execute(id: string): Promise<Book | 'BOOK_NOT_FOUND'> {
this.logger.debug('[Get-book usecase] Start');
const data = await this.bookRepository.findById(id);
return data ?? 'BOOK_NOT_FOUND';
}
}
export default GetBook;
Entities
The "/core/entities" directory contains our business data models, such as "User" or "Product". These are simple objects that represent the concepts of our domain, and bring together their business rules.
Let's take the example of our "User", and more precisely of a user who is not logged in, and therefore not known to the API:
export class NotExistingUser extends User {
constructor() {
super();
}
public hashPassword(notHashedPassword: string) {
const hmac = createHmac('sha512', this.config.salt);
hmac.update(notHashedPassword);
return hmac.digest('hex');
}
}
Here, we have a class that extends User and thus retrieves its properties, and a method that belongs to it, that is, "its" business rule: You hash a password (to create your account or connect to it).
Ports
These are simple interfaces linking the core and the infrastructure. They serve as a contract without any concrete implementation. You may find ports for technical dependencies, such as a logger, or for repositories (databases, etc.). An example of a User :
interface UserRepository {
findByEmail(email: string): Promise<User | null>
create(user: User): Promise<void>
}
This contract includes two methods, each with input and output parameters. The code part therefore knows what it will receive from the infrastructure, and vice versa.
Technical details in "/infrastructure"
At the same level as core is the infrastructure folder, which manages all the technical details such as data persistence and calls to external services. This is where the interfaces defined in core are actually implemented.
The API
Let's start with the API, the folder in which we'll put Express' config, controllers and other middleware.
In detail, for controllers, the "/infrastructure/api/controllers" folder groups controllers by resource or use case.
Their role here is to retrieve data from the HTTP request, validate and map it to the inputs expected by the use case, execute the use case and finally format the response. We'll therefore find sub-folders for each resource, with a typescript file for the controller itself, the input and output DTOs, and the encoder/decoder for these inputs/outputs.
Adapters
As the name suggests, this directory contains the technical adapters for the ports defined on the core side. It's very important to identify implementation dependencies in the tree structure. Let's take our example of the User repository:
// /core/ports/user-repository.port.ts
interface UserRepository {
findByEmail(email: string): Promise<User | null>
save(user: User): Promise<void>
}
// /infrastructure/adapters/mongo/user.repository.ts
import { UserRepository } from '../../core/ports/user-repository.port.ts'
export class MongoUserRepository implements UserRepository {
async findByEmail(email: string): Promise<User | null> {
// logique d'accès MongoDB
}
async save(user: User): Promise<void> {
// logique d'accès MongoDB
}
}
At tree level, we find "adapters/mongo", which means that in this sub-directory we have all the technical implementation for accessing mongo DB. As you can see, the adapter takes over the contract specified in the port. If tomorrow I want to change my dependency to SQLite, for example, I'll simply create a second adapter, change the dependency injection, and there'll be no impact on the core.
Conclusion
This structure clearly distributes the various responsibilities of a Node.js API. The core has no external dependencies, ports are decoupled from business logic and implementation details are delegated to the infrastructure layer.
By following these Clean Architecture principles, your code gains in maintainability, testability and scalability. Although the example is given with TypeScript and Express code, this organization can easily be adapted to other Node.js frameworks or even any other language.
In the end, that's the whole point of clean architecture: no language or framework, but a return to basics, to common sense, and contrary to popular misconception, a return to simplicity!
Frequently Asked Questions:
What are the advantages of following Clean Architecture principles in an Express project?
The disadvantage of Express (or advantage, depending on your point of view!) is that it's an empty shell. So you can do absolutely anything you want, and also anything at all. Clean architecture provides a framework for the whole team.If I'm doing clean architecture, do I have to follow this structure to the letter?
No, this structure is just one example. The most important thing is to respect the fundamental principles of Clean Architecture: separation of concerns, decoupling of technical details, external dependencies, etc.How are tests structured in this architecture?
We haven't put it in this article, but use case unit tests can be put at the same level as each other, in /core/use-cases. Personally, I prefer to have a tests folder at the root, which is a convention we often see.
This article is an introduction to Clean Architecture and is part of a dedicated series on this topic. Stay tuned for more!
Want to learn how implement it with typescript and express? See my udemy course! In french or english 😉
Articles on Clean Architecture:
Top comments (0)