iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🧇

Clean Architecture Directory Structure in NestJS

に公開

Update: I also wrote this article!
https://zenn.dev/trape_inc/articles/bfc23c829c2eb2

Various Directory Structures

Directory structure can be quite a headache, isn't it? I'd like to list a few patterns, not just limited to NestJS.

Rails Style

This is the directory structure I first learned.

├── controllers
│   ├── userController.ts
│   └── chatController.ts
├── services
│   ├── userService.ts
│   └── chatService.ts
├── models
│   ├── user.ts
│   └── chat.ts
└── configs

Rails-style Clean Architecture

This is a directory structure I created after learning about Clean Architecture.

├── presentations
│   ├── userController.ts
│   └── chatController.ts
├── usecases
│   ├── userService.ts
│   └── chatService.ts
├── domains
│   ├── user
│   │   ├── userRepositoryInterface.ts
│   │   └── userEntity.ts
│   └── chat
│       ├── chatRepositoryInterface.ts
│       └── chatEntity.ts
├── infrastructures
│   ├── sequelize
│   │   └── userRepository.ts
│   └── dynamodb
│       └── chatRepository.ts
└── configs

Major Domain Partitioned Clean Architecture

This structure inherits from the Rails-style Clean Architecture but separates the top-level directories by major domains. I saw in an article somewhere that a certain company had adopted this. Related domains are contained under the major domain directory. For example, a participant list domain would go under the chat domain. That's why the main body of the major domain is separated as core.

├── chat
│   ├── presentations
│   │   └── chatController.ts
│   ├── usecases
│   │   └── chatService.ts
│   ├── domains
│   │   └── core
│   │       ├── chatRepositoryInterface.ts
│   │       └── chatEntity.ts
│   ├── infrastructures
│   │   └── dynamodb
│   │       └── chatRepository.ts
│   └── configs
└── account
    ├── presentations
    │   └── userController.ts
    ├── usecases
    │   └── userService.ts
    ├── domains
    │   └── core
    │       ├── userRepositoryInterface.ts
    │       └── userEntity.ts
    ├── infrastructures
    │   └── sequelize
    │       └── userRepository.ts
    └── configs

Module-based Clean Architecture

If you're implementing Clean Architecture in NestJS, I think it will look like this structure. Since I've named the folders somewhat arbitrarily, you can use any names you like for entity, infra, and so on.

├── chat
│   ├── core
│   │   ├── entity
│   │   │   ├── chat.entity.ts
│   │   │   └── chat.repository.interface.ts
│   │   ├── infra
│   │   │   └── chat.repository.dynamodb.ts
│   │   ├── chat.controller.ts
│   │   ├── chat.service.ts
│   │   └── chat.module.ts
│   └── chat.index.module.ts
└── account
    ├── core
    │   ├── entity
    │   │   ├── user.entity.ts
    │   │   └── user.repository.interface.ts
    │   ├── infra
    │   │   └── user.repository.sequelize.ts
    │   ├── user.controller.ts
    │   ├── user.service.ts
    │   └── user.module.ts
    └── account.index.module.ts

Introducing Query Services

Place the user.query.service.interface.ts interface directly in the module directory. Next, place the actual implementation user.repository.sequelize.ts in the infra directory.

└── account
    ├── core
    │   ├── entity
    │   │   ├── user.entity.ts
    │   │   └── user.repository.interface.ts
    │   ├── infra
    │   │   ├── user.query.service.sequelize.ts
+   │   │   └── user.repository.sequelize.ts
    │   ├── user.controller.ts
    │   ├── user.service.ts
+   │   ├── user.query.service.interface.ts
    │   └── user.module.ts
    └── account.index.module.ts

Introducing External Libraries as Modules

Following the NestJS approach, external libraries like Sequelize or Prisma should not be imported directly. Instead, they should be managed as modules and injected via DI (Dependency Injection) into the services where they are needed. There are two patterns for how to create them.

Pattern 1: Hiding the library's specific name

├── account
│   ├── core
│   │   ├── entity
│   │   │   ├── user.entity.ts
│   │   │   └── user.repository.interface.ts
│   │   ├── infra
│   │   │   └── user.query.service.sequelize.ts
│   │   ├── user.controller.ts
│   │   ├── user.service.ts
│   │   └── user.module.ts
│   └── account.index.module.ts
└── cache
    ├── infra
    │   └── cache.service.redis.ts
    ├── cache.service.interface.ts
    └── cache.module.ts

Pattern 2: Using the library's specific name

├── account
│   ├── core
│   │   ├── entity
│   │   │   ├── user.entity.ts
│   │   │   └── user.repository.interface.ts
│   │   ├── infra
│   │   │   └── user.query.service.sequelize.ts
│   │   ├── user.controller.ts
│   │   ├── user.service.ts
│   │   └── user.module.ts
│   └── account.index.module.ts
└── sequelize
    ├── sequelize.service.ts
    └── sequelize.module.ts

Pattern 1 abstracts the library by bringing the "cache" role provided by Redis to the forefront. Because of this, it uses cache.service.interface.ts instead of cache.service.ts. The implementation is handled within the infra directory.

Pattern 2 turns Sequelize itself into a service.

From an abstraction perspective, Pattern 1 is more recommended than Pattern 2. Pattern 2 is primarily used for injecting libraries that are shared across the infra directories of other modules.

For a more detailed explanation of abstraction, please refer to this article:
https://zenn.dev/dove/articles/40ec3779b39ac6

├── account
│   ├── core
│   │   ├── entity
│   │   │   ├── user.entity.ts
│   │   │   └── user.repository.interface.ts
│   │   ├── event
│   │   │   └── account.created.event.ts
│   │   ├── infra
│   │   │   └── user.query.service.sequelize.ts
│   │   ├── user.controller.ts
│   │   ├── user.service.ts
│   │   └── user.module.ts
│   └── account.index.module.ts
├── notification
│   ├── listener
│   │   └── account.created.listener.ts
│   ├── notification.service.ts
│   └── notification.module.ts
└── share
    ├── baseEvent.ts
    └── util.ts

Please refer to this for the implementation:
https://zenn.dev/dove/articles/40476144e8bf9a

Discussion