Photo by wd toro ๐ฒ๐จ on Unsplash
Build an ExpressJS Application With Clean Architecture
One that actually adheres to Clean Architecture
Table of contents
- Motivation
- What is Clean Architecture
- What are we going to build
- Behaviors
- File Structure
- Files
- Define a Game Model
- Create the base structure for Clean Architecture
- Create the Board Entity
- Starting with our first use case
- Add the board repository
- Exception management
- Complete our use case
- Adding missing use cases
- Binding use cases to a controller
- Dependency Injection
- Using a presenter
- Completing controller
- Conclusion
Motivation
Similarly to my Front-End implementation, I'm not satisfied with the available resources related to Clean Architecture. That's right, even on the Back-End!
What is Clean Architecture
If you stumbled upon this article, you probably know about Clean Architecture, but are looking for more information.
In the rare event of a beginner reading this article, I highly recommend getting started with the necessary theory.
What are we going to build
While I wrote this article, my goal was to test if Clean Architecture was as flexible as it promised. That's the reason I re-used the same entities and use cases as my Front-End implementation, and tried to move the infrastructure part to the Back-End with Express.
That means, the application is the same: a Tic-Tac-Toe game (in TypeScript). You will be able to send requests to a Back-End and play a complete game.
If you want to follow along with me, you can clone my starter project.
Behaviors
You can find the Front-End Tic-Tac-Toe on CodeSandbox to understand what the application is doing in practice. There are two main functionalities:
Obviously, you can play a game of Tic-Tac-Toe.
You can go back to a previous move (history) and play again.
Instead of being a Front-End application, we will provide endpoints with the same behavior.
File Structure
The original application already follows the idea of Screaming Architecture, with three directories:
common/
includes all the shared source code.core/
defines the entry point of our application:app.ts
contains the ExpressJS server.bootstrap.ts
uses the code insideapp.ts
to build a server and listen to it.
tic-tac-toe/
contains everything related to the game itself.
Files
While you can have a look at the entire project by yourself, please note there are three main files:
App
createApp
is responsible for building the ExpressJS server and uses the Tic-Tac-Toe module (which is responsible for routing but is not the focus of this article).
Tic-Tac-Toe Controller
TicTacToeController
is responsible for handling requests. It's the one that contains the business logic and the core part of the original implementation.
Board Service
BoardService
takes the role of a data access object, which is an in-memory implementation for simplicity.
Define a Game Model
Similarly to my Front-End implementation, I like to start by defining a clear Domain Model. Its goal is to define the elements that make up our system. Please, keep in mind this is not strictly a part of Clean Architecture but Domain-Driven-Design.
For our game of Tic-Tac-Toe, we need:
A board composed of 9 squares.
Players and winners.
At one point in time, the state of our game:
Is there a winner?
The history of moves (so we can navigate among them).
Who is playing next?
Potentially, the index of the last game played.
I'll create this file under tic-tac-toe/
, using a namespace to make it clear:
Create the base structure for Clean Architecture
We will create an entity and use case in a minute. We can create the base classes for them under common/
:
Keep in mind:
There are multiple ways to define those.
For
Entity
, I like to store properties inside a_data
object and make it protected. It prevents developers from mutating it directly, which is a common source of issues. Depending on our needs, this class will definitely change.There are multiple practices when it comes to use cases, but a very common one is to provide an
execute
method, which returns aPromise
most of the time.
Create the Board Entity
Most of the logic of the Tic-Tac-Toe game seems related to the state of the board. Let's create an entity for it, inside a new entities
subfolder, under tic-tac-toe
:
Here:
We store squares, based on the types from our
GameDomainModel
.- We add a getter to easily access the property.
We already have the formula to calculate if player X plays next.
We copy it from the
TicTacToe
file inside the starter project.We can calculate the current step based on the filled squares.
The entity is also a great place to calculate if there is a winner.
We can also copy the formula from the starter TicTacToeController
file.
In "Clean Architecture", Uncle Bob says:
An Entity is an object within our computer system that embodies a small set of critical business rules operating on Critical Business Data.
In our context, our critical business rules are the rules of the game of Tic-Tac-Toe.
Starting with our first use case
Now, we have everything we need to create an empty use case. We can get started with an obvious one, playing a move.
Obviously, it will take the square we play on. On the other hand, we also need to define the step we play on, as we can play on a previous move. Our use case will probably return an object similar to the game state:
We could probably use
GameDomainModel.GameState
asOutput
here. On the other hand, those objects may only seem familiar, and change for different reasons.
To be honest, the current code inside TicTacToeController
is very similar to the future content of the Play
use case. It might be because I created this project as another implementation, but the original project is still far from using Clean Architecture.
If you remember well, a use case shouldn't depend on a data access gateway from the Interface Adapters layer.
Add the board repository
Instead of the current BoardService
, I'll define an interface as a port
and its implementation will be an adapter
. I'll also use the definition of Repository
instead of Service
for consistency.
Let's start with the interface by creating a sub-directory ports/
under tic-tac-toe/
, with a file called board-repository.port.ts
:
Then, we can create an adapters/
sub-directory under tic-tac-toe/
. Also, the previous BoardService
shouldn't be called BoardRepository
.
It's not the implementation of IBoardRepository, it's one among others. To be more precise, it's an in-memory implementation.
Let's create in-memory-board.repository.ts
:
Before we go back to our use case, we need one more thing: exceptions.
Exception management
If you look at the original TicTacToeController
, you can see we throw exceptions when:
The step we are supposed to play on doesn't exist
We play on a square that's already taken
With Clean Architecture, we cannot throw exceptions that belong to the infrastructure layer (Interface Adapters & Framework and Drivers), from the use case layer.
Instead, we need to throw a Domain Exception, that is handled by the controller.
Let's define two exceptions, stored inside an exceptions/
sub-directory (itself inside tic-tac-toe/
):
Complete our use case
Now that we have our entity, repository, and exceptions, we can complete our use case. Most of the logic can be transferred from TicTacToeController
, in handlePlay
.
There are a few changes necessary:
Business rules are now inside the
Board
entity.We rely on the
IBoardRepository
.We throw a Domain Exception
This use case was added under tic-tac-toe/
in its own sub-directory use-cases/
.
Adding missing use cases
The process is similar for the two other use cases. We can retrieve most of the logic from the TicTacToeController
but use our brand new Board
entity and IBoardRepository
.
Let's start with jump-to.use-case.ts
:
Then, we can add initialize.use-case.ts
, which is responsible for starting a new game:
Binding use cases to a controller
The last part is definitely way easier in a Back-End environment. We simply need to:
Execute the right use case
If there is a Domain (or unknown) Exception, throw the related Application Exception
Otherwise, return the result with the right status code.
Dependency Injection
Same thing here, using dependency injection is definitely easy. Depending on the Back-End framework you're using, you will have different solutions (or you can setup your own with InversifyJS).
In this example, I manually instantiate and inject dependencies from app.ts
:
Using a presenter
If you look closely at our current controller, it's doing more than orchestration. It's also formatting data for moves
, status
, and squares
, which is not really its role.
A better architecture would use a presenter instead. There are multiple ways to define a presenter. A common practice is to define a class with a format
method.
Let's create presenter.ts
inside common/
:
Now, we need to define our actual presenter. It's supposed to present our game and return the related squares
, moves
, and status
. Based on our current code, it needs the current history
, winner
, xIsNext
, and step
.
A controller shouldn't depend directly on a presenter but on its interface.
Let's create a game-presenter.port.ts
inside tic-tac-toe/ports/
:
Now, we can add the implementation, as game.presenter.ts
inside tic-tac-toe/adapters/
:
Completing controller
Now, we can expect our IGamePresenter
inside the TicTacController
constructor (which we instantiate and inject from app.ts
). Then, we can remove the method related to formatting and instead use the presenter:
Conclusion
Your Back-End Tic-Tac-Toe game is now fully functional, and adheres to Clean Architecture! The domain (logic) is completely isolated from the infrastructure.
Feel free to look at the end result.
You would have no problem moving away from ExpressJS and using another Back-End framework like Fastify or NestJS. If you did, you wouldn't need to modify any code that's part of the domain layer.
Not only could you use another Back-End framework, but you could even make this game a Front-End application!
Are looking for more in-depth resources about Clean Architecture? Let me know here.