本文介绍的是一个NestJS的项目,项目地址为,开发的时候觉得用的架构模式很不错,在下面详细介绍一下nestjs-boilerplateGithubnestjs-boilerplateOwnerbrocodersUpdatedFeb 25, 2025
able of Contents
able of Contents Hexagonal ArchitectureMotivation Description of the module structurePlease provide a detailed introduction (Infrastructure Layer)Key design featuresMapper modeActual application processRecommendations
Hexagonal Architecture
NestJS Boilerplate is based on Hexagonal Architecture. This architecture is also known as Ports and Adapters.
Motivation
The main reason for using Hexagonal Architecture is to separate the business logic from the infrastructure. This separation allows us to easily change the database, the way of uploading files, or any other infrastructure without changing the business logic.
Description of the module structure
. ├── domain │ └── [DOMAIN_ENTITY].ts ├── dto │ ├── create.dto.ts │ ├── find-all.dto.ts │ └── update.dto.ts ├── infrastructure │ └── persistence │ ├── document │ │ ├── document-persistence.module.ts │ │ ├── entities │ │ │ └── [SCHEMA].ts │ │ ├── mappers │ │ │ └── [MAPPER].ts │ │ └── repositories │ │ └── [ADAPTER].repository.ts │ ├── relational │ │ ├── entities │ │ │ └── [ENTITY].ts │ │ ├── mappers │ │ │ └── [MAPPER].ts │ │ ├── relational-persistence.module.ts │ │ └── repositories │ │ └── [ADAPTER].repository.ts │ └── [PORT].repository.ts ├── controller.ts ├── module.ts └── service.ts
[DOMAIN ENTITY].ts
represents an entity used in the business logic. Domain entity has no dependencies on the database or any other infrastructure.[SCHEMA].ts
represents the database structure. It is used in the document-oriented database (MongoDB).[ENTITY].ts
represents the database structure. It is used in the relational database (PostgreSQL).[MAPPER].ts
is a mapper that converts database entity to domain entity and vice versa.[PORT].repository.ts
is a repository port that defines the methods for interacting with the database.[ADAPTER].repository.ts
is a repository that implements the [PORT].repository.ts
. It is used to interact with the database.infrastructure
folder - contains all the infrastructure-related components such as persistence
, uploader
, senders
, etc.Each component has
port
and adapters
. Port
is interface that define the methods for interacting with the infrastructure. Adapters
are implementations of the port
.Please provide a detailed introduction (Infrastructure Layer)
. └── infrastructure/ └── persistence/ ├── document/ # MongoDB adapter ├── relational/ # PostgreSQL adapter └── user.repository.ts # Warehouse interface (port)
[user].repository.ts
// infrastructure/persistence/user.repository.ts export abstract class UserRepository { abstract create(data: User): Promise<User>; abstract findById(id: User['id']): Promise<NullableType<User>>; abstract findByEmail(email: User['email']): Promise<NullableType<User>>; // ... }
- Defined the interface for interacting with the database
- Define all necessary methods using abstract classes
- Does not include specific implementation, only defines the contract
Adapters
// infrastructure/persistence/relational/repositories/user.repository.ts @Injectable() export class UsersRelationalRepository implements UserRepository { constructor( @InjectRepository(UserEntity) private readonly usersRepository: Repository<UserEntity>, ) {} async create(data: User): Promise<User> { const persistenceModel = UserMapper.toPersistence(data); const newEntity = await this.usersRepository.save(...); return UserMapper.toDomain(newEntity); } // ... }
- Implemented specific database operations
- Using Mapper to Transform between Domain Models and Persistent Models
- There can be multiple adapters (such as MongoDB and PostgreSQL)
Key design features
Database independence
- Through the abstract UserRepository interface
- Can easily switch database implementation (MongoDB/PostgreSQL)
- Business logic does not rely on specific databases
Mapper mode
const persistenceModel = UserMapper.toPersistence(data); return UserMapper.toDomain(newEntity);
- Responsible for converting between domain models and persistence models
- Maintain the purity of domain models
Actual application process
Create User Process
Controller -> Service -> UserRepository (Port) -> Specific Repository Implementation (Adapter) -> database
Data conversion process
Domain User <-> Mapper <-> Database Entity
Recommendations
Don't try to create universal methods in the repository because they are difficult to extend during the project's life. Instead of this create methods with a single responsibility.
// ❌ export class UsersRelationalRepository implements UserRepository { async find(condition: UniversalConditionInterface): Promise<User> { // ... } } // ✅ export class UsersRelationalRepository implements UserRepository { async findByEmail(email: string): Promise<User> { // ... } async findByRoles(roles: string[]): Promise<User> { // ... } async findByIds(ids: string[]): Promise<User> { // ... } }