This is a sample project for introducing Clean Architecture into services using TypeScript.
It is an extended version of the React with Clean Architecture
project, which configures React
and React Native
services sharing the same domain. This setup allows for the expansion of various TypeScript-based services while sharing the same domain.
This document is a work in progress as I study OOP, DDD, Clean Architecture, and related topics. Since my knowledge is still growing, there may be parts that I have misunderstood or explained incorrectly. If you find any issues or have suggestions for improvement, please feel free to submit them through issues or pull requests, and I will incorporate them.
☺️
+ My English is not perfect, so please bear with me.
For a domain-driven, clean architecture-based React application setup, rather than implementing clean architecture across the entire service, please refer to the project below.
(Domain-driven React Architecture)
https://github.com/falsy/domain-driven-react-architecture
As with many architectures, the primary goal of Clean Architecture is to separate concerns. It divides layers according to each concern, designs around the domain rather than detailed implementations, and ensures the inner layers do not depend on external elements like frameworks, databases, or UIs.
- Separate the detailed implementation area and the domain area.
- The architecture does not depend on the framework.
- The outer layers can depend on the inner layers, but the inner layers cannot depend on the outer layers.
- Both high-level and low-level modules depend on abstractions.
The flow of Clean Architecture can be briefly illustrated in the diagram above.
This document uses the Parcel Tracking
service, provided as an extension for the Whale browser, as an example project
. This service is a simple application that crawls the delivery status information from the delivery company's tracking page using the delivery company and tracking number. Please refer to the example project's code.
Parcel Tracking
is an ongoing service, so please be mindful of the version.
The version at the time of writing is as follows.
Various services sharing the same domain were organized using Git
submodules.
The core
repository of the domain area is composed, and the remaining services are configured using this core
repository as a submodule.
In the example project
, there are three repositories: core-dev
for developing and testing the core, api-server
for the API server, and extension-for-whale
for the extension client's code.
All these repositories use the core
repository as a submodule.
All services that share the domain, as mentioned above, are structured with a clean architecture and operate according to the previously discussed Communication Flow
.
In the domain layer, business rules and business logic are defined.
Example project code
Parcel Tracking - core(v1.0.0)
/core
├─ domains
│ ├─ entities
│ └─ usecases
├─ dtos
├─ vos
└─ (repositories/interfaces)
First, let's define the services of the example project 'Parcel Tracking':
- Users can view a list of delivery companies available for tracking.
- Users can input and edit the delivery company, tracking number, and label.
- Users can check current delivery status information through the delivery company and tracking number.
- Users can view the list of entered delivery companies, tracking numbers, and labels.
Based on the above service definitions, we define the entities.
- Carrier - An object containing information about the delivery company.
interface ICarrier {
readonly id: string
readonly no: number // (Legacy properties)
readonly name: string
readonly displayName: string
readonly isCrawlable: boolean
readonly isPopupEnabled: boolean
readonly popupURL: string
}
- Tracker - An object containing the user's delivery tracking information.
interface ITracker {
readonly id: string
carrierId: string
label: string
trackingNumber: string
memos: string[]
updateLabel(newLabel: string): void
updateTrackingNumber(newTrackingNumber: string): void
updateCarrierId(newCarrierId: string): void
addMemo(): void
updateMemo(index: number, newMemo: string): void
deleteMemo(index: number): void
}
The Use Case
layer encapsulates data into entities, coordinates the interactions between the defined rules of entities and other layers, and implements business logic using request parameters passed by Controllers
or methods of Repositories
.
As the Repository
belongs to the Adapter
layer, the Use Case
should not know about the Repository
. Therefore, in the Use Case, the Repository is abstracted into an interface and operates through Dependency Injection
.
The core
is shared among various services using the same domain. However, as mentioned above, the Use Case needs to know the abstracted Repository, which may vary according to the service.
In the example project
, the addTracker method in the TrackerUseCase adds a Tracker using the Repository, which uses the Web API LocalStorage to store values. This is used only in the web client (extension-for-whale
) and not in other services (api-server
).
As the Repository
can be used differently for each service, the abstracted Repository methods in the core are all used as optional properties(?
).
In the TrackerUseCase of the
example project
, thegenerateUUID
method is used to generate a unique ID for the tracker. This was done to avoid using external modules in the Domain area and purely composed of TypeScript, as the added tracker is not a critical value. However, generally, if a UUID is required, it is better to encapsulate libraries likecrypto
oruuid
and use Dependency Injection.
The example project
uses NestJS
. NestJS
is a widely used Node.js
framework that facilitates the composition of object-oriented services through decorators, dependency injection mechanisms, and a module-based structured code.
Example project code
Parcel Tracking - api-serive(v1.0.0)
/api-server
└─ src
├─ core(domains) #submodule
├─ adapters
│ ├─ controllers
│ ├─ repositories
│ ├─ infrastructures
│ └─ utilities
└─ frameworks
├─ controllers
├─ usecases
├─ repositories
├─ models
└─ moduls
The directory structure is composed of core(domains), imported as a submodule using Git, as well as adapters and frameworks, similar to the layers of Clean Architecture.
The architecture of Clean Architecture should not depend on the framework. That is, even if the framework is changed, it should be possible to apply it with minimal changes. However, since NestJS
uses Decorators
to compose services, each layer inevitably depends on them.
Therefore, the operations are implemented in the adapters
layer, which does not depend on NestJS, and the frameworks
layer inherits from the parent (controllers
, usecases
, ...) to add decorators
, override methods, and call the parent's implementation using super
.
In Infrastructures
, the functionality of the fetch API
is abstracted to define the ServerHTTP
class. This class subsequently performs HTTP communication by injecting fetch
.
Since the fetch API was released in Node.js version 18, if using a previous version, the
ServerHTTP
should be composed using the built-in http module or an additional library in Node.js.
The example project
uses MySQL
and Sequelize
as the database. NestJS
supports such an environment easily, so the DB-related logic is implemented in the Repository
of the frameworks
layer through NestJS's dependency injection, not in the adapter
layer's Repository
.
It is built and distributed as an extension for the Whale browser, but it is not significantly different from a simple general web service.
Example project code
Parcel Tracking - extension-for-whale(v1.8.1)
/extension-for-whale
└─ src
├─ core(domains) #submodule
├─ constants
├─ adapters
│ ├─ controllers
│ ├─ repositories
│ └─ infrastructures
└─ frameworks
├─ di
└─ components
The basic directory structure is similar to the api-server
.
Adding another service later can be configured similarly without significant differences.
In the web service, ClientHTTP
, providing HTTP communication functions, and WebLocalStorage
, providing the browser's storage function, are defined in Infrastructures and injected into Repositories. These ultimately operate by injecting fetch
and localStorage
, respectively.
The structure in the image above is implemented identically in the api-server.
Fetch
and WebStorage
are Web APIs that can be accessed globally, but since these are changeable detailed implementations, they are encapsulated into classes and positioned according to their roles to facilitate future changes or maintenance.
The example project was developed using React
and Webpack
.
After injecting dependencies in the di
directory, the service is composed using the methods defined in Controllers
.
Similar to the api-server
, the framework composing the client is designed to rely only on the data through DI
within the frameworks
layer. Therefore, the architecture does not depend on the framework and can flexibly handle changes or replacements.
The example project(Parcel Tracking)
can be downloaded and run locally.
Nestjs, Sequelize, MySQL
Webpack, React, Emotion
$ git clone --branch v1.0.0 --single-branch https://github.com/parcel-tracking/api-server.git
$ cd api-server
$ npm install
$ git clone --branch v1.8.1 --single-branch https://github.com/parcel-tracking/extension-for-whale.git
$ cd extension-for-whale
$ npm install
Since the
example project(Parcel Tracking)
is a live service, it may be updated differently from the described content, so it was cloned with the version branch at the time of document writing.
To run the api-server
locally, additional DB (MySQL
) settings are required.
Create an additional .env
file in the root
directory of the api-server
and add values as follows.
$ cd api-server
$ vi .env
DB_USERNAME=root
DB_PASSWORD=password
DB_NAME=database-name
DB_HOST=localhost
DB_DIALECT=mysql
The data of the currently used DB in the service can be downloaded here
.
For more details, see the LICENSE file.
I'm grateful for all the support and interest 🙇♂️