Implementing Clean Architecture with NestJS: Building Scalable Enterprise Applications
Introduction
As applications grow in complexity, maintaining clean, testable, and scalable code becomes increasingly challenging. Clean Architecture, popularized by Robert C. Martin, provides a solution by organizing code into layers with clear dependencies and responsibilities. NestJS, with its modular structure and dependency injection system, is perfectly suited for implementing Clean Architecture patterns.
In this comprehensive guide, we'll build a user management system that demonstrates how to apply Clean Architecture principles in a real NestJS application.
Understanding Clean Architecture Layers
Clean Architecture organizes code into concentric circles, with dependencies pointing inward:
- Domain Layer (Entities): Core business logic and rules
- Application Layer (Use Cases): Application-specific business rules
- Interface Adapters: Controllers, presenters, and gateways
- Frameworks & Drivers: External frameworks, databases, web servers
Setting Up the Project Structure
First, let's establish a clear folder structure that reflects our architectural layers:
src/
├── domain/
│ ├── entities/
│ ├── repositories/
│ └── value-objects/
├── application/
│ ├── use-cases/
│ └── interfaces/
├── infrastructure/
│ ├── repositories/
│ ├── database/
│ └── external-services/
├── presentation/
│ ├── controllers/
│ ├── dto/
│ └── guards/
└── shared/
├── exceptions/
└── types/Implementing the Domain Layer
The domain layer contains our business entities and core logic. Let's create a User entity:
// src/domain/entities/user.entity.ts
export class User {
constructor(
private readonly id: string,
private readonly email: string,
private readonly firstName: string,
private readonly lastName: string,
private readonly createdAt: Date,
private updatedAt: Date
) {}
getId(): string {
return this.id;
}
getEmail(): string {
return this.email;
}
getFullName(): string {
return `${this.firstName} ${this.lastName}`;
}
isActive(): boolean {
// Business logic for determining if user is active
return this.updatedAt > new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
}
static create(data: {
email: string;
firstName: string;
lastName: string;
}): User {
return new User(
crypto.randomUUID(),
data.email,
data.firstName,
data.lastName,
new Date(),
new Date()
);
}
}Next, define the repository interface in the domain layer:
// src/domain/repositories/user.repository.ts
import { User } from '../entities/user.entity';
export interface UserRepository {
findById(id: string): Promise;
findByEmail(email: string): Promise;
save(user: User): Promise;
delete(id: string): Promise;
} Building the Application Layer
The application layer contains use cases that orchestrate domain entities. Here's a CreateUser use case:
// src/application/use-cases/create-user.use-case.ts
import { Injectable, ConflictException } from '@nestjs/common';
import { User } from '../../domain/entities/user.entity';
import { UserRepository } from '../../domain/repositories/user.repository';
export interface CreateUserCommand {
email: string;
firstName: string;
lastName: string;
}
@Injectable()
export class CreateUserUseCase {
constructor(private readonly userRepository: UserRepository) {}
async execute(command: CreateUserCommand): Promise {
// Check if user already exists
const existingUser = await this.userRepository.findByEmail(command.email);
if (existingUser) {
throw new ConflictException('User with this email already exists');
}
// Create new user
const user = User.create({
email: command.email,
firstName: command.firstName,
lastName: command.lastName,
});
// Save user
return await this.userRepository.save(user);
}
} Infrastructure Layer Implementation
The infrastructure layer provides concrete implementations of our repository interfaces:
// src/infrastructure/repositories/typeorm-user.repository.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from '../../domain/entities/user.entity';
import { UserRepository } from '../../domain/repositories/user.repository';
import { UserEntity } from '../database/entities/user.entity';
@Injectable()
export class TypeOrmUserRepository implements UserRepository {
constructor(
@InjectRepository(UserEntity)
private readonly repository: Repository
) {}
async findById(id: string): Promise {
const userEntity = await this.repository.findOne({ where: { id } });
return userEntity ? this.toDomain(userEntity) : null;
}
async findByEmail(email: string): Promise {
const userEntity = await this.repository.findOne({ where: { email } });
return userEntity ? this.toDomain(userEntity) : null;
}
async save(user: User): Promise {
const userEntity = this.toEntity(user);
const savedEntity = await this.repository.save(userEntity);
return this.toDomain(savedEntity);
}
async delete(id: string): Promise {
await this.repository.delete(id);
}
private toDomain(entity: UserEntity): User {
return new User(
entity.id,
entity.email,
entity.firstName,
entity.lastName,
entity.createdAt,
entity.updatedAt
);
}
private toEntity(user: User): UserEntity {
const entity = new UserEntity();
entity.id = user.getId();
entity.email = user.getEmail();
// Map other properties...
return entity;
}
} Presentation Layer Controllers
Finally, create controllers that handle HTTP requests:
// src/presentation/controllers/user.controller.ts
import { Body, Controller, Post } from '@nestjs/common';
import { CreateUserUseCase } from '../../application/use-cases/create-user.use-case';
import { CreateUserDto } from '../dto/create-user.dto';
@Controller('users')
export class UserController {
constructor(private readonly createUserUseCase: CreateUserUseCase) {}
@Post()
async createUser(@Body() createUserDto: CreateUserDto) {
const user = await this.createUserUseCase.execute({
email: createUserDto.email,
firstName: createUserDto.firstName,
lastName: createUserDto.lastName,
});
return {
id: user.getId(),
email: user.getEmail(),
fullName: user.getFullName(),
isActive: user.isActive(),
};
}
}Dependency Injection Configuration
Wire everything together in your module:
// src/user.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserController } from './presentation/controllers/user.controller';
import { CreateUserUseCase } from './application/use-cases/create-user.use-case';
import { TypeOrmUserRepository } from './infrastructure/repositories/typeorm-user.repository';
import { UserRepository } from './domain/repositories/user.repository';
import { UserEntity } from './infrastructure/database/entities/user.entity';
@Module({
imports: [TypeOrmModule.forFeature([UserEntity])],
controllers: [UserController],
providers: [
CreateUserUseCase,
{
provide: UserRepository,
useClass: TypeOrmUserRepository,
},
],
})
export class UserModule {}Benefits and Testing
This Clean Architecture approach provides several advantages:
- Testability: Easy to unit test business logic in isolation
- Flexibility: Can swap database implementations without affecting business logic
- Maintainability: Clear separation of concerns makes code easier to understand
- Scalability: New features can be added without modifying existing code
Testing becomes straightforward since dependencies are clearly defined and can be easily mocked.
Conclusion
Clean Architecture with NestJS creates a robust foundation for enterprise applications. While it requires more initial setup, the long-term benefits in maintainability, testability, and scalability make it worthwhile for complex projects. The key is to maintain strict dependency rules and keep business logic independent of external frameworks.
Related Posts
Building a Real-Time Chat Application with Socket.IO and Express
Learn to build a scalable real-time chat app using Socket.IO, Express, and modern JavaScript patterns with authentication and room management.
Implementing Custom JWT Authentication in NestJS: A Production-Ready Guide
Build secure, scalable JWT authentication in NestJS with refresh tokens, role-based access control, and best security practices.
Building Production-Ready APIs with NestJS: A Complete Guide to Scalable Backend Architecture
Master NestJS fundamentals and build enterprise-grade APIs with proper architecture, validation, and error handling.