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
├── 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
- Async messaging: Pentru operații non-critice folosește event-driven patterns
- Circuit breakers: Implementează reziliență pentru serviciile externe
- Retry logic: Gestionează eșecurile temporare
- Timeouts: Setează timeout-uri pentru toate comunicările
2. Data Management
- Database per service: Fiecare microservice să aibă propria bază de date
- Event sourcing: Pentru audit trail și reconstruirea stării
- CQRS: Separă operațiile de citire și scriere
- Eventual consistency: Acceptă consistența eventuală
3. Security
- API Gateway security: Centralizează autenticarea și autorizarea
- Service mesh: Pentru comunicare sigură între servicii
- Secrets management: Folosește vault-uri pentru credențiale
- Network isolation: Izolează serviciile în rețele private
🎯 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.