Lesson 34-NestJS Project Architecture and Engineering

Project Architecture Design

Layered Architecture Design

Typical Layered Structure Implementation:

// User module example
@Module({
  controllers: [UserController],
  providers: [UserService, UserRepository],
  imports: [TypeOrmModule.forFeature([UserEntity])]
})
export class UserModule {}

// Controller layer
@Controller('users')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Get(':id')
  async getUser(@Param('id') id: string) {
    return this.userService.findById(id)
  }
}

// Service layer
@Injectable()
export class UserService {
  constructor(private readonly userRepository: UserRepository) {}

  async findById(id: string): Promise<UserDto> {
    const user = await this.userRepository.findById(id)
    return plainToInstance(UserDto, user)
  }
}

// Repository layer
@Repository()
export class UserRepository {
  constructor(
    @InjectRepository(UserEntity)
    private readonly ormRepository: Repository<UserEntity>
  ) {}

  async findById(id: string): Promise<UserEntity> {
    return this.ormRepository.findOne({ where: { id } })
  }
}

Layer Responsibilities Division:

  • Controller Layer: Handles HTTP requests/responses, parameter validation, route mapping
  • Service Layer: Implements business logic, transaction management, coordinates domain services
  • Repository Layer: Manages data persistence operations, optimizes database queries

Modularization and Functional Decomposition

Functional Module Division Example:

src/
├── auth/                  # Authentication module
│   ├── auth.module.ts
│   ├── dto/
│   ├── entities/
│   ├── guards/
│   ├── interceptors/
│   ├── services/
│   └── strategies/
├── user/                  # User module
│   ├── user.module.ts
│   ├── dto/
│   ├── entities/
│   ├── repositories/
│   └── services/
└── shared/                # Shared module
    ├── common/
    ├── config/
    └── utils/

Dynamic Module Registration:

// shared/config.module.ts
@Module({})
export class ConfigModule {
  static forRoot(config: AppConfig): DynamicModule {
    return {
      module: ConfigModule,
      providers: [
        {
          provide: 'APP_CONFIG',
          useValue: config
        }
      ],
      exports: ['APP_CONFIG']
    }
  }
}

// Usage in other modules
@Module({
  imports: [ConfigModule.forRoot(appConfig)]
})
export class UserModule {}

Domain-Driven Design (DDD) Practices

Aggregate Root Implementation Example:

// order/aggregate-root/order.aggregate.ts
export class OrderAggregate {
  private constructor(
    public readonly id: string,
    public readonly userId: string,
    public readonly items: OrderItem[],
    public readonly status: OrderStatus
  ) {}

  static create(userId: string, items: OrderItemDto[]): OrderAggregate {
    // Validate business rules
    if (items.length === 0) {
      throw new BusinessRuleException('Order must contain items')
    }

    const id = generateId()
    const orderItems = items.map(item => new OrderItem(item.productId, item.quantity))

    return new OrderAggregate(id, userId, orderItems, 'PENDING')
  }

  addItem(item: OrderItemDto): void {
    if (this.status !== 'PENDING') {
      throw new BusinessRuleException('Cannot add items to completed orders')
    }
    this.items.push(new OrderItem(item.productId, item.quantity))
  }
}

// Handling aggregates in Repository
@Injectable()
export class OrderRepository {
  async save(aggregate: OrderAggregate): Promise<void> {
    await this.ormRepository.save({
      id: aggregate.id,
      userId: aggregate.userId,
      items: aggregate.items.map(i => ({ productId: i.productId, quantity: i.quantity })),
      status: aggregate.status
    })
  }
}

Project Directory Structure Standards

Recommended Directory Structure:

src/
├── app.module.ts          # Root module
├── main.ts                # Application entry point
├── config/                # Configuration-related
│   ├── configuration.ts
│   └── validation.pipe.ts
├── common/                # Shared module
│   ├── decorators/
│   ├── filters/
│   ├── interceptors/
│   └── utils/
├── modules/               # Functional modules
│   ├── auth/
│   ├── user/
│   └── product/
└── test/                  # Test code
    ├── e2e/
    └── unit/

Naming Conventions:

  • Modules: <feature>.module.ts
  • Controllers: <feature>.controller.ts
  • Services: <feature>.service.ts
  • Entities: <entity>.entity.ts
  • DTOs: <feature>.dto.ts

Configuration File Management

Multi-Environment Configuration Implementation:

// config/configuration.ts
export default () => ({
  database: {
    host: process.env.DB_HOST || 'localhost',
    port: parseInt(process.env.DB_PORT) || 5432,
    username: process.env.DB_USERNAME || 'postgres',
    password: process.env.DB_PASSWORD || 'postgres',
    database: process.env.DB_NAME || 'nestjs'
  },
  redis: {
    host: process.env.REDIS_HOST || 'localhost',
    port: parseInt(process.env.REDIS_PORT) || 6379
  }
})

// app.module.ts
@Module({
  imports: [
    ConfigModule.forRoot({
      load: [configuration],
      envFilePath: `.env.${process.env.NODE_ENV || 'development'}`
    })
  ]
})
export class AppModule {}

Configuration Validation:

// config/validation.pipe.ts
export class ValidationPipe implements PipeTransform {
  constructor(private readonly validator: Validator) {}

  transform(value: any, metadata: ArgumentMetadata) {
    const object = plainToClass(metadata.metatype, value)
    const errors = this.validator.validate(object)

    if (errors.length > 0) {
      throw new BadRequestException('Validation failed')
    }

    return object
  }
}

Engineering Toolchain

NestJS CLI Advanced Configuration

Custom Generator Implementation:

// tools/generators/entity.generator.ts
export class EntityGenerator implements Generator {
  generate(options: EntityGeneratorOptions): void {
    const { name, path } = options
    const fileContent = `
      import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'

      @Entity('${name.toLowerCase()}')
      export class ${capitalize(name)} {
        @PrimaryGeneratedColumn()
        id: number

        @Column()
        name: string
      }
    `

    writeFileSync(join(path, `${name}.entity.ts`), fileContent)
  }
}

// Register custom generator
nest g -c tools/generators entity User --path=modules/user/entities

Webpack Advanced Configuration

Custom Webpack Configuration:

// webpack.config.js
module.exports = (options, webpack) => {
  return {
    ...options,
    module: {
      rules: [
        ...options.module.rules,
        {
          test: /\.graphql$/,
          loader: 'graphql-tag/loader'
        }
      ]
    },
    plugins: [
      ...options.plugins,
      new BundleAnalyzerPlugin({
        analyzerMode: process.env.ANALYZE ? 'server' : 'disabled'
      })
    ],
    optimization: {
      splitChunks: {
        chunks: 'all',
        cacheGroups: {
          vendor: {
            test: /[\\/]node_modules[\\/]/,
            name: 'vendors',
            chunks: 'all'
          }
        }
      }
    }
  }
}

TypeScript Advanced Configuration

Advanced tsconfig Settings:

{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    "noImplicitThis": true,
    "alwaysStrict": true,
    "esModuleInterop": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "baseUrl": ".",
    "paths": {
      "@app/*": ["src/*"],
      "@config/*": ["src/config/*"],
      "@common/*": ["src/common/*"]
    }
  },
  "exclude": ["node_modules", "dist"]
}

Code Standardization Toolchain

ESLint Configuration Example:

// .eslintrc.js
module.exports = {
  parser: '@typescript-eslint/parser',
  parserOptions: {
    project: 'tsconfig.json',
    sourceType: 'module'
  },
  plugins: ['@typescript-eslint', 'prettier'],
  extends: [
    'plugin:@typescript-eslint/recommended',
    'prettier'
  ],
  rules: {
    '@typescript-eslint/explicit-function-return-type': 'off',
    '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
    'prettier/prettier': 'error'
  }
}

Prettier Configuration:

{
  "printWidth": 120,
  "tabWidth": 2,
  "useTabs": false,
  "semi": true,
  "singleQuote": true,
  "trailingComma": "all",
  "bracketSpacing": true,
  "arrowParens": "always"
}

Automated Testing Framework

Unit Test Example:

// user.service.spec.ts
describe('UserService', () => {
  let service: UserService
  let repository: UserRepository

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        UserService,
        {
          provide: UserRepository,
          useValue: {
            findOne: jest.fn(),
            create: jest.fn()
          }
        }
      ]
    }).compile()

    service = module.get<UserService>(UserService)
    repository = module.get<UserRepository>(UserRepository)
  })

  it('should be defined', () => {
    expect(service).toBeDefined()
  })

  describe('findOne', () => {
    it('should return a user', async () => {
      const user = { id: '1', name: 'Test' }
      jest.spyOn(repository, 'findOne').mockResolvedValue(user)

      const result = await service.findOne('1')
      expect(result).toEqual(user)
    })
  })
})

Integration Test Example:

// user.controller.e2e-spec.ts
describe('UserController (e2e)', () => {
  let app: INestApplication

  beforeAll(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AppModule]
    }).compile()

    app = moduleFixture.createNestApplication()
    await app.init()
  })

  it('/users (GET) should return 200', () => {
    return request(app.getHttpServer())
      .get('/users')
      .expect(200)
  })
})

Performance Optimization and Deployment

Cache Strategy Implementation

Redis Cache Integration:

// cache.module.ts
@Module({
  imports: [
    CacheModule.registerAsync({
      useFactory: async (config: ConfigService) => ({
        store: redisStore,
        host: config.get('REDIS_HOST'),
        port: config.get('REDIS_PORT'),
        ttl: 60 // seconds
      }),
      inject: [ConfigService]
    })
  ],
  exports: [CacheModule]
})
export class CacheConfigModule {}

// Using cache in a service
@Injectable()
export class ProductService {
  constructor(
    private readonly repository: ProductRepository,
    @Inject(CACHE_MANAGER) private readonly cacheManager: Cache
  ) {}

  async findOne(id: string): Promise<Product> {
    const cached = await this.cacheManager.get<Product>(`product:${id}`)
    if (cached) {
      return cached
    }

    const product = await this.repository.findOne(id)
    await this.cacheManager.set(`product:${id}`, product, 60)
    return product
  }
}

Load Balancing and Clustering

PM2 Cluster Mode Configuration:

// ecosystem.config.js
module.exports = {
  apps: [
    {
      name: 'nestjs-api',
      script: 'dist/main.js',
      instances: 'max',
      exec_mode: 'cluster',
      env: {
        NODE_ENV: 'production',
        PORT: 3000
      },
      max_memory_restart: '1G'
    }
  ]
}

Node.js Cluster Implementation:

// cluster.ts
if (cluster.isPrimary) {
  const numCPUs = os.cpus().length
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork()
  }

  cluster.on('exit', (worker) => {
    console.log(`Worker ${worker.process.pid} died`)
    cluster.fork()
  })
} else {
  await app.listen(3000)
  console.log(`Worker ${process.pid} started`)
}

Database Query Optimization

TypeORM Query Optimization:

// 1. Using indexes
@Entity()
@Index(['email']) // Add index to email field
export class User {
  @PrimaryGeneratedColumn()
  id: number

  @Column({ unique: true })
  email: string
}

// 2. Batch operations
await repository
  .createQueryBuilder()
  .insert()
  .into(User)
  .values([
    { name: 'User1' },
    { name: 'User2' }
  ])
  .execute()

// 3. Lazy and eager loading
@Entity()
export class Order {
  @PrimaryGeneratedColumn()
  id: number

  @ManyToOne(() => User, user => user.orders, { eager: true }) // Eager loading
  user: User
}

Logging and Monitoring

Winston Logging Configuration:

// logger.module.ts
@Module({
  providers: [
    {
      provide: 'LOGGER',
      useFactory: () => {
        return winston.createLogger({
          level: 'info',
          format: winston.format.json(),
          transports: [
            new winston.transports.File({ filename: 'error.log', level: 'error' }),
            new winston.transports.File({ filename: 'combined.log' })
          ]
        })
      }
    }
  ],
  exports: ['LOGGER']
})
export class LoggerModule {}

// Using logger
@Injectable()
export class UserService {
  constructor(@Inject('LOGGER') private readonly logger: winston.Logger) {}

  async createUser(user: UserDto) {
    this.logger.info('Creating user', { user })
    try {
      // ...
    } catch (error) {
      this.logger.error('Failed to create user', { error })
      throw error
    }
  }
}

ELK Integration:

// elk.logger.ts
export class ElkLogger {
  constructor(private readonly config: ConfigService) {}

  log(message: string, context?: string, metadata?: any) {
    const logEntry = {
      timestamp: new Date().toISOString(),
      message,
      context,
      ...metadata
    }

    // Send to Logstash
    axios.post(this.config.get('LOGSTASH_URL'), logEntry)
  }
}

Deployment and CI/CD

Dockerfile Example:

# Build stage
FROM node:16-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Production stage
FROM node:16-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./
RUN npm ci --only=production
EXPOSE 3000
CMD ["node", "dist/main.js"]

CI/CD Pipeline Example:

# .github/workflows/deploy.yml
name: Deploy
on:
  push:
    branches: [main]

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: 16
      - run: npm ci
      - run: npm run build
      - uses: azure/webapps-deploy@v2
        with:
          app-name: 'nestjs-api'
          publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }}
          package: ./dist
Share your love