iTranslated by AI
Clean Architecture Directory Structure in NestJS
Update: I also wrote this article!
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:
Including Directories Related to Domain Events
├── 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:
Discussion