← Înapoi la Blog

NestJS și Microservices: Construirea unei Arhitecturi Scalabile în 2024

NestJS a devenit rapid unul dintre framework-urile Node.js preferate pentru construirea aplicațiilor backend scalabile și robuste. În acest ghid comprehensiv, vom explora cum să folosim NestJS pentru a construi o arhitectură de microservices modernă, care poate crește odată cu nevoia afacerii tale.

🏗️ De ce Microservices cu NestJS?

Arhitectura microservices oferă flexibilitate și scalabilitate, în timp ce NestJS aduce structura și organizarea necesare pentru a gestiona complexitatea acestor sisteme distribuite.

✅ Avantaje

  • Scalabilitate independentă
  • Tehnologii diversificate per serviciu
  • Dezvoltare în echipe separate
  • Reziliență la eșecuri
  • Deploy independent

❌ Dezavantaje

  • Complexitate de rețea crescută
  • Overhead de comunicare
  • Dificultate în debugging
  • Consistența datelor
  • Monitorizare complexă

🚀 Configurarea Arhitecturii de Bază

1. API Gateway cu NestJS

API Gateway-ul servește ca punct central de intrare pentru toate cererile:

// apps/api-gateway/src/main.ts
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  
  // Global validation
  app.useGlobalPipes(new ValidationPipe({
    transform: true,
    whitelist: true,
    forbidNonWhitelisted: true,
  }));

  // CORS configuration
  app.enableCors({
    origin: process.env.FRONTEND_URL,
    methods: ['GET', 'POST', 'PUT', 'DELETE'],
    credentials: true,
  });

  // Swagger documentation
  const config = new DocumentBuilder()
    .setTitle('Microservices API')
    .setDescription('API Gateway for microservices architecture')
    .setVersion('1.0')
    .addBearerAuth()
    .build();
  
  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('api/docs', app, document);

  await app.listen(process.env.GATEWAY_PORT || 3000);
  console.log(`API Gateway running on port ${process.env.GATEWAY_PORT || 3000}`);
}

bootstrap();

2. Service Discovery și Communication

NestJS oferă diverse opțiuni pentru comunicarea între microservices:

// apps/api-gateway/src/app.module.ts
import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { ConfigModule, ConfigService } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),
    
    // TCP-based microservices
    ClientsModule.registerAsync([
      {
        name: 'USER_SERVICE',
        useFactory: (configService: ConfigService) => ({
          transport: Transport.TCP,
          options: {
            host: configService.get('USER_SERVICE_HOST'),
            port: configService.get('USER_SERVICE_PORT'),
          },
        }),
        inject: [ConfigService],
      },
      {
        name: 'ORDER_SERVICE',
        useFactory: (configService: ConfigService) => ({
          transport: Transport.TCP,
          options: {
            host: configService.get('ORDER_SERVICE_HOST'),
            port: configService.get('ORDER_SERVICE_PORT'),
          },
        }),
        inject: [ConfigService],
      },
    ]),

    // Redis-based messaging
    ClientsModule.registerAsync([
      {
        name: 'NOTIFICATION_SERVICE',
        useFactory: (configService: ConfigService) => ({
          transport: Transport.REDIS,
          options: {
            host: configService.get('REDIS_HOST'),
            port: configService.get('REDIS_PORT'),
            password: configService.get('REDIS_PASSWORD'),
          },
        }),
        inject: [ConfigService],
      },
    ]),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

👤 Implementarea User Service

Structura User Microservice

User Service
├── Controllers (HTTP endpoints)
├── Services (Business logic)
├── Repositories (Data access)
├── DTOs (Data transfer objects)
├── Entities (Database models)
└── Guards (Authentication/Authorization)
// apps/user-service/src/user.service.ts
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
import { CreateUserDto, UpdateUserDto, UserResponseDto } from './dto';
import { hash, compare } from 'bcryptjs';

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User)
    private readonly userRepository: Repository<User>,
  ) {}

  async create(createUserDto: CreateUserDto): Promise<UserResponseDto> {
    const existingUser = await this.userRepository.findOne({
      where: { email: createUserDto.email }
    });

    if (existingUser) {
      throw new ConflictException('User with this email already exists');
    }

    const hashedPassword = await hash(createUserDto.password, 12);
    
    const user = this.userRepository.create({
      ...createUserDto,
      password: hashedPassword,
    });

    const savedUser = await this.userRepository.save(user);
    return this.toResponseDto(savedUser);
  }

  async findById(id: string): Promise<UserResponseDto> {
    const user = await this.userRepository.findOne({
      where: { id },
      relations: ['profile', 'orders'],
    });

    if (!user) {
      throw new NotFoundException('User not found');
    }

    return this.toResponseDto(user);
  }

  async validateCredentials(email: string, password: string): Promise<User | null> {
    const user = await this.userRepository.findOne({
      where: { email },
      select: ['id', 'email', 'password', 'isActive'],
    });

    if (!user || !user.isActive) {
      return null;
    }

    const isPasswordValid = await compare(password, user.password);
    return isPasswordValid ? user : null;
  }

  async update(id: string, updateUserDto: UpdateUserDto): Promise<UserResponseDto> {
    const user = await this.userRepository.findOne({ where: { id } });
    
    if (!user) {
      throw new NotFoundException('User not found');
    }

    if (updateUserDto.password) {
      updateUserDto.password = await hash(updateUserDto.password, 12);
    }

    Object.assign(user, updateUserDto);
    const updatedUser = await this.userRepository.save(user);
    
    return this.toResponseDto(updatedUser);
  }

  private toResponseDto(user: User): UserResponseDto {
    const { password, ...userWithoutPassword } = user;
    return userWithoutPassword as UserResponseDto;
  }
}

Message Patterns pentru Inter-Service Communication

// apps/user-service/src/user.controller.ts
import { Controller } from '@nestjs/common';
import { MessagePattern, Payload } from '@nestjs/microservices';
import { UserService } from './user.service';
import { CreateUserDto, UpdateUserDto } from './dto';

@Controller()
export class UserController {
  constructor(private readonly userService: UserService) {}

  @MessagePattern('user.create')
  async createUser(@Payload() createUserDto: CreateUserDto) {
    return await this.userService.create(createUserDto);
  }

  @MessagePattern('user.findById')
  async findUserById(@Payload() id: string) {
    return await this.userService.findById(id);
  }

  @MessagePattern('user.findByEmail')
  async findUserByEmail(@Payload() email: string) {
    return await this.userService.findByEmail(email);
  }

  @MessagePattern('user.validateCredentials')
  async validateCredentials(@Payload() { email, password }: { email: string; password: string }) {
    return await this.userService.validateCredentials(email, password);
  }

  @MessagePattern('user.update')
  async updateUser(@Payload() { id, updateUserDto }: { id: string; updateUserDto: UpdateUserDto }) {
    return await this.userService.update(id, updateUserDto);
  }

  @MessagePattern('user.delete')
  async deleteUser(@Payload() id: string) {
    return await this.userService.remove(id);
  }
}

🛒 Order Service cu Event Sourcing

Event-Driven Architecture

Pentru comenzi, folosim event sourcing pentru a păstra un istoric complet:

// apps/order-service/src/events/order.events.ts
import { IEvent } from '@nestjs/cqrs';

export class OrderCreatedEvent implements IEvent {
  constructor(
    public readonly orderId: string,
    public readonly userId: string,
    public readonly items: OrderItem[],
    public readonly total: number,
    public readonly createdAt: Date,
  ) {}
}

export class OrderStatusChangedEvent implements IEvent {
  constructor(
    public readonly orderId: string,
    public readonly previousStatus: OrderStatus,
    public readonly newStatus: OrderStatus,
    public readonly changedAt: Date,
  ) {}
}

export class PaymentProcessedEvent implements IEvent {
  constructor(
    public readonly orderId: string,
    public readonly paymentId: string,
    public readonly amount: number,
    public readonly status: PaymentStatus,
    public readonly processedAt: Date,
  ) {}
}

CQRS Implementation

// apps/order-service/src/commands/create-order.command.ts
import { ICommand } from '@nestjs/cqrs';

export class CreateOrderCommand implements ICommand {
  constructor(
    public readonly userId: string,
    public readonly items: OrderItem[],
    public readonly shippingAddress: Address,
    public readonly paymentMethod: PaymentMethod,
  ) {}
}

// apps/order-service/src/handlers/create-order.handler.ts
import { CommandHandler, ICommandHandler, EventBus } from '@nestjs/cqrs';
import { CreateOrderCommand } from '../commands/create-order.command';
import { OrderCreatedEvent } from '../events/order.events';
import { Order } from '../entities/order.entity';

@CommandHandler(CreateOrderCommand)
export class CreateOrderHandler implements ICommandHandler<CreateOrderCommand> {
  constructor(
    private readonly orderRepository: Repository<Order>,
    private readonly eventBus: EventBus,
  ) {}

  async execute(command: CreateOrderCommand): Promise<Order> {
    const { userId, items, shippingAddress, paymentMethod } = command;
    
    // Calculate total
    const total = items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
    
    // Create order
    const order = this.orderRepository.create({
      userId,
      items,
      total,
      shippingAddress,
      paymentMethod,
      status: OrderStatus.PENDING,
    });

    const savedOrder = await this.orderRepository.save(order);

    // Publish event
    const orderCreatedEvent = new OrderCreatedEvent(
      savedOrder.id,
      savedOrder.userId,
      savedOrder.items,
      savedOrder.total,
      savedOrder.createdAt,
    );

    this.eventBus.publish(orderCreatedEvent);

    return savedOrder;
  }
}

📧 Notification Service cu Message Queues

Redis-based Pub/Sub

// apps/notification-service/src/notification.service.ts
import { Injectable, Logger } from '@nestjs/common';
import { EventPattern, Payload } from '@nestjs/microservices';
import { MailerService } from '@nestjs-modules/mailer';
import { OrderCreatedEvent, PaymentProcessedEvent } from '../events';

@Injectable()
export class NotificationService {
  private readonly logger = new Logger(NotificationService.name);

  constructor(private readonly mailerService: MailerService) {}

  @EventPattern('order.created')
  async handleOrderCreated(@Payload() event: OrderCreatedEvent) {
    this.logger.log(`Processing order created event: ${event.orderId}`);
    
    try {
      // Get user details
      const user = await this.getUserDetails(event.userId);
      
      // Send order confirmation email
      await this.mailerService.sendMail({
        to: user.email,
        subject: 'Comanda ta a fost confirmată',
        template: './order-confirmation',
        context: {
          userName: user.name,
          orderId: event.orderId,
          items: event.items,
          total: event.total,
        },
      });

      this.logger.log(`Order confirmation email sent for order: ${event.orderId}`);
    } catch (error) {
      this.logger.error(`Failed to send order confirmation: ${error.message}`);
    }
  }

  @EventPattern('payment.processed')
  async handlePaymentProcessed(@Payload() event: PaymentProcessedEvent) {
    this.logger.log(`Processing payment event: ${event.paymentId}`);
    
    if (event.status === PaymentStatus.SUCCESS) {
      // Send payment success notification
      await this.sendPaymentSuccessNotification(event);
    } else {
      // Send payment failed notification
      await this.sendPaymentFailedNotification(event);
    }
  }

  private async sendPaymentSuccessNotification(event: PaymentProcessedEvent) {
    // Implementation for successful payment notification
  }

  private async sendPaymentFailedNotification(event: PaymentProcessedEvent) {
    // Implementation for failed payment notification
  }

  private async getUserDetails(userId: string) {
    // Call user service to get user details
    return this.userServiceClient.send('user.findById', userId).toPromise();
  }
}

🔐 Authentication & Authorization

JWT Authentication Strategy

// libs/auth/src/jwt.strategy.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
import { ClientProxy } from '@nestjs/microservices';
import { Inject } from '@nestjs/common';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(
    private readonly configService: ConfigService,
    @Inject('USER_SERVICE') private readonly userServiceClient: ClientProxy,
  ) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: configService.get<string>('JWT_SECRET'),
    });
  }

  async validate(payload: any) {
    const { sub: userId } = payload;
    
    try {
      const user = await this.userServiceClient
        .send('user.findById', userId)
        .toPromise();

      if (!user || !user.isActive) {
        throw new UnauthorizedException('Invalid token');
      }

      return user;
    } catch (error) {
      throw new UnauthorizedException('Invalid token');
    }
  }
}

// libs/auth/src/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from './roles.decorator';
import { Role } from './role.enum';

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);

    if (!requiredRoles) {
      return true;
    }

    const { user } = context.switchToHttp().getRequest();
    return requiredRoles.some((role) => user.roles?.includes(role));
  }
}

📊 Monitoring și Health Checks

Health Check Implementation

// libs/health/src/health.controller.ts
import { Controller, Get } from '@nestjs/common';
import { HealthCheckService, HealthCheck, TypeOrmHealthIndicator, MemoryHealthIndicator } from '@nestjs/terminus';

@Controller('health')
export class HealthController {
  constructor(
    private health: HealthCheckService,
    private db: TypeOrmHealthIndicator,
    private memory: MemoryHealthIndicator,
  ) {}

  @Get()
  @HealthCheck()
  check() {
    return this.health.check([
      // Database health check
      () => this.db.pingCheck('database'),
      
      // Memory health check
      () => this.memory.checkHeap('memory_heap', 150 * 1024 * 1024),
      () => this.memory.checkRSS('memory_rss', 150 * 1024 * 1024),
    ]);
  }

  @Get('ready')
  @HealthCheck()
  readiness() {
    return this.health.check([
      () => this.db.pingCheck('database'),
    ]);
  }

  @Get('live')
  @HealthCheck()
  liveness() {
    return this.health.check([
      () => this.memory.checkHeap('memory_heap', 200 * 1024 * 1024),
    ]);
  }
}

🚀 Deploy cu Docker și Kubernetes

Docker Configuration

# Dockerfile.user-service
FROM node:18-alpine AS builder

WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force

FROM node:18-alpine AS runtime
WORKDIR /app

RUN addgroup -g 1001 -S nodejs
RUN adduser -S nestjs -u 1001

COPY --from=builder /app/node_modules ./node_modules
COPY --chown=nestjs:nodejs . .

USER nestjs
EXPOSE 3001

CMD ["node", "dist/apps/user-service/main.js"]

Kubernetes Deployment

# k8s/user-service.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: user-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: user-service
  template:
    metadata:
      labels:
        app: user-service
    spec:
      containers:
      - name: user-service
        image: cotvision/user-service:latest
        ports:
        - containerPort: 3001
        env:
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: user-service-secrets
              key: database-url
        - name: JWT_SECRET
          valueFrom:
            secretKeyRef:
              name: user-service-secrets
              key: jwt-secret
        livenessProbe:
          httpGet:
            path: /health/live
            port: 3001
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /health/ready
            port: 3001
          initialDelaySeconds: 5
          periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
  name: user-service-service
spec:
  selector:
    app: user-service
  ports:
  - port: 3001
    targetPort: 3001
  type: ClusterIP

💡 Best Practices și Recomandări

1. Service Communication

2. Data Management

3. Security

🎯 Concluzie

Construirea unei arhitecturi de microservices cu NestJS oferă o fundație solidă pentru aplicații scalabile și robuste. Combinația dintre stilul arhitectural al microservice-urilor și puterea framework-ului NestJS permite dezvoltatorilor să construiască sisteme complexe într-un mod organizat și manevrabil.

Pentru Cotvision, arhitectura microservices reprezintă soluția ideală pentru proiectele enterprise care necesită scalabilitate mare, echipe de dezvoltare distribuite și tehnologii diverse. Folosind NestJS, putem livra sisteme backend robuste care cresc odată cu afacerea clienților noștri.

Dacă compania ta are nevoie de o arhitectură backend scalabilă și robustă, contactează echipa Cotvision pentru o consultație despre cum microservices-urile cu NestJS pot accelera dezvoltarea produsului tău.