Arquitetura de Microserviços com Node.js: Guia Completo
Aprenda a projetar e implementar uma arquitetura de microserviços robusta usando Node.js com Clean Architecture, DDD e princípios SOLID.
Este conteúdo é gratuito! Ajude a manter o projeto no ar.
0737160d-e98f-4a65-8392-5dba70e7ff3eMicroserviços revolucionaram a forma como construímos aplicações escaláveis. Neste guia completo, vou compartilhar minha experiência de mais de 10 anos trabalhando com arquiteturas distribuídas, aplicando Clean Architecture, DDD e princípios SOLID.
Por que Microserviços?
Antes de mergulhar na implementação, é crucial entender quando microserviços fazem sentido:
Benefícios
- Escalabilidade independente: Escale apenas os serviços que precisam
- Deploy independente: Atualize um serviço sem afetar os outros
- Tecnologias heterogêneas: Use a melhor ferramenta para cada problema
- Resiliência: Falha em um serviço não derruba todo o sistema
- Ownership claro: Times podem ser donos de serviços específicos
Desafios
- Complexidade operacional aumentada
- Comunicação entre serviços
- Consistência de dados distribuídos
- Observabilidade e debugging
- Latência de rede
Fundamentos: SOLID, Clean Architecture e DDD
Antes de estruturar nosso microserviço, vamos entender os princípios que guiarão nossas decisões.
Princípios SOLID
| Princípio | Descrição | Aplicação em Microserviços |
|---|---|---|
| Single Responsibility | Uma classe deve ter apenas um motivo para mudar | Cada serviço tem uma responsabilidade bem definida |
| Open/Closed | Aberto para extensão, fechado para modificação | Use interfaces e injeção de dependências |
| Liskov Substitution | Subtipos devem ser substituíveis por seus tipos base | Contratos de API consistentes |
| Interface Segregation | Muitas interfaces específicas são melhores que uma geral | APIs focadas e coesas |
| Dependency Inversion | Dependa de abstrações, não de implementações | Camadas internas não conhecem frameworks |
Clean Architecture
A Clean Architecture organiza o código em camadas concêntricas, onde as dependências sempre apontam para dentro:
┌─────────────────────────────────────────────────────────────┐
│ Infrastructure │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Application │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ Domain │ │ │
│ │ │ ┌─────────────────────────────────────┐ │ │ │
│ │ │ │ Entities │ │ │ │
│ │ │ └─────────────────────────────────────┘ │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
Domain-Driven Design (DDD)
Conceitos essenciais que usaremos:
- Entities: Objetos com identidade única
- Value Objects: Objetos imutáveis sem identidade
- Aggregates: Cluster de entidades tratadas como uma unidade
- Repositories: Abstração para persistência
- Domain Services: Lógica que não pertence a uma entidade
- Domain Events: Notificações de algo que aconteceu no domínio
Estrutura de Pastas do Microserviço
Aqui está a estrutura completa seguindo Clean Architecture e DDD:
user-service/
├── src/
│ ├── @types/ # Definições de tipos globais
│ │ └── express.d.ts
│ │
│ ├── domain/ # Camada de Domínio (núcleo)
│ │ ├── entities/
│ │ │ ├── User.ts
│ │ │ └── index.ts
│ │ │
│ │ ├── value-objects/
│ │ │ ├── Email.ts
│ │ │ ├── Password.ts
│ │ │ ├── UserId.ts
│ │ │ └── index.ts
│ │ │
│ │ ├── events/
│ │ │ ├── UserCreatedEvent.ts
│ │ │ ├── UserUpdatedEvent.ts
│ │ │ └── index.ts
│ │ │
│ │ ├── repositories/
│ │ │ └── IUserRepository.ts
│ │ │
│ │ ├── services/
│ │ │ └── IPasswordHasher.ts
│ │ │
│ │ └── errors/
│ │ ├── DomainError.ts
│ │ ├── UserNotFoundError.ts
│ │ ├── EmailAlreadyExistsError.ts
│ │ └── index.ts
│ │
│ ├── application/ # Camada de Aplicação (casos de uso)
│ │ ├── use-cases/
│ │ │ ├── CreateUser/
│ │ │ │ ├── CreateUserUseCase.ts
│ │ │ │ ├── CreateUserDTO.ts
│ │ │ │ └── index.ts
│ │ │ │
│ │ │ ├── GetUser/
│ │ │ │ ├── GetUserUseCase.ts
│ │ │ │ ├── GetUserDTO.ts
│ │ │ │ └── index.ts
│ │ │ │
│ │ │ ├── UpdateUser/
│ │ │ │ ├── UpdateUserUseCase.ts
│ │ │ │ ├── UpdateUserDTO.ts
│ │ │ │ └── index.ts
│ │ │ │
│ │ │ ├── DeleteUser/
│ │ │ │ ├── DeleteUserUseCase.ts
│ │ │ │ └── index.ts
│ │ │ │
│ │ │ └── ListUsers/
│ │ │ ├── ListUsersUseCase.ts
│ │ │ ├── ListUsersDTO.ts
│ │ │ └── index.ts
│ │ │
│ │ ├── services/
│ │ │ ├── IEventPublisher.ts
│ │ │ └── ILogger.ts
│ │ │
│ │ └── errors/
│ │ ├── ApplicationError.ts
│ │ ├── ValidationError.ts
│ │ └── index.ts
│ │
│ ├── infrastructure/ # Camada de Infraestrutura
│ │ ├── database/
│ │ │ ├── prisma/
│ │ │ │ ├── schema.prisma
│ │ │ │ └── migrations/
│ │ │ ├── repositories/
│ │ │ │ └── PrismaUserRepository.ts
│ │ │ └── connection.ts
│ │ │
│ │ ├── messaging/
│ │ │ ├── RabbitMQEventPublisher.ts
│ │ │ ├── consumers/
│ │ │ │ └── UserEventsConsumer.ts
│ │ │ └── connection.ts
│ │ │
│ │ ├── cache/
│ │ │ ├── RedisCache.ts
│ │ │ └── ICache.ts
│ │ │
│ │ ├── services/
│ │ │ ├── BcryptPasswordHasher.ts
│ │ │ └── WinstonLogger.ts
│ │ │
│ │ └── config/
│ │ ├── env.ts
│ │ └── index.ts
│ │
│ ├── presentation/ # Camada de Apresentação (API)
│ │ ├── http/
│ │ │ ├── controllers/
│ │ │ │ ├── UserController.ts
│ │ │ │ └── HealthController.ts
│ │ │ │
│ │ │ ├── middlewares/
│ │ │ │ ├── errorHandler.ts
│ │ │ │ ├── requestLogger.ts
│ │ │ │ ├── rateLimiter.ts
│ │ │ │ ├── authenticate.ts
│ │ │ │ └── validate.ts
│ │ │ │
│ │ │ ├── routes/
│ │ │ │ ├── user.routes.ts
│ │ │ │ ├── health.routes.ts
│ │ │ │ └── index.ts
│ │ │ │
│ │ │ ├── validators/
│ │ │ │ ├── createUserValidator.ts
│ │ │ │ └── updateUserValidator.ts
│ │ │ │
│ │ │ └── presenters/
│ │ │ └── UserPresenter.ts
│ │ │
│ │ └── grpc/ # Opcional: gRPC
│ │ ├── protos/
│ │ │ └── user.proto
│ │ └── handlers/
│ │ └── UserHandler.ts
│ │
│ ├── shared/ # Código compartilhado
│ │ ├── container/
│ │ │ └── index.ts # Injeção de dependências
│ │ │
│ │ ├── patterns/
│ │ │ ├── CircuitBreaker.ts
│ │ │ ├── RetryWithBackoff.ts
│ │ │ └── index.ts
│ │ │
│ │ └── utils/
│ │ ├── Result.ts # Either/Result pattern
│ │ └── Guard.ts # Validações
│ │
│ ├── app.ts # Configuração do Express
│ └── server.ts # Entry point
│
├── tests/
│ ├── unit/
│ │ ├── domain/
│ │ │ └── entities/
│ │ │ └── User.spec.ts
│ │ └── application/
│ │ └── use-cases/
│ │ └── CreateUserUseCase.spec.ts
│ │
│ ├── integration/
│ │ └── repositories/
│ │ └── PrismaUserRepository.spec.ts
│ │
│ └── e2e/
│ └── user.e2e.spec.ts
│
├── docker/
│ ├── Dockerfile
│ ├── Dockerfile.dev
│ └── docker-compose.yml
│
├── k8s/
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── configmap.yaml
│ ├── secret.yaml
│ └── hpa.yaml
│
├── .env.example
├── .eslintrc.js
├── .prettierrc
├── jest.config.js
├── tsconfig.json
├── package.json
└── README.md
Implementação Detalhada
1. Camada de Domínio
A camada mais interna, sem dependências externas.
Entity: User
// src/domain/entities/User.tsimport { Email } from '../value-objects/Email';import { Password } from '../value-objects/Password';import { UserId } from '../value-objects/UserId';import { UserCreatedEvent } from '../events/UserCreatedEvent';import { DomainEvent } from '../events/DomainEvent'; interface UserProps { id: UserId; email: Email; password: Password; name: string; isActive: boolean; createdAt: Date; updatedAt: Date;} export class User { private readonly props: UserProps; private domainEvents: DomainEvent[] = []; private constructor(props: UserProps) { this.props = props; } // Factory method - único ponto de criação static create(props: { email: Email; password: Password; name: string; }): User { const user = new User({ id: UserId.create(), email: props.email, password: props.password, name: props.name, isActive: true, createdAt: new Date(), updatedAt: new Date(), }); // Registra evento de domínio user.addDomainEvent(new UserCreatedEvent(user)); return user; } // Reconstitui do banco de dados static reconstitute(props: UserProps): User { return new User(props); } // Getters - encapsulamento get id(): UserId { return this.props.id; } get email(): Email { return this.props.email; } get name(): string { return this.props.name; } get isActive(): boolean { return this.props.isActive; } get createdAt(): Date { return this.props.createdAt; } // Comportamentos de domínio updateEmail(newEmail: Email): void { this.props.email = newEmail; this.props.updatedAt = new Date(); } updateName(newName: string): void { if (newName.length < 2) { throw new Error('Name must have at least 2 characters'); } this.props.name = newName; this.props.updatedAt = new Date(); } deactivate(): void { this.props.isActive = false; this.props.updatedAt = new Date(); } activate(): void { this.props.isActive = true; this.props.updatedAt = new Date(); } validatePassword(plainPassword: string): boolean { return this.props.password.compare(plainPassword); } // Domain Events private addDomainEvent(event: DomainEvent): void { this.domainEvents.push(event); } pullDomainEvents(): DomainEvent[] { const events = [...this.domainEvents]; this.domainEvents = []; return events; }}Value Object: Email
// src/domain/value-objects/Email.tsimport { DomainError } from '../errors/DomainError'; export class Email { private readonly value: string; private constructor(email: string) { this.value = email.toLowerCase().trim(); } static create(email: string): Email { if (!email) { throw new DomainError('Email is required'); } const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(email)) { throw new DomainError('Invalid email format'); } return new Email(email); } getValue(): string { return this.value; } equals(other: Email): boolean { return this.value === other.value; } toString(): string { return this.value; }}Value Object: Password
// src/domain/value-objects/Password.tsimport bcrypt from 'bcrypt';import { DomainError } from '../errors/DomainError'; export class Password { private readonly hashedValue: string; private readonly isHashed: boolean; private constructor(value: string, isHashed: boolean) { this.hashedValue = value; this.isHashed = isHashed; } static create(plainPassword: string): Password { if (!plainPassword || plainPassword.length < 8) { throw new DomainError('Password must have at least 8 characters'); } if (!/[A-Z]/.test(plainPassword)) { throw new DomainError('Password must have at least one uppercase letter'); } if (!/[0-9]/.test(plainPassword)) { throw new DomainError('Password must have at least one number'); } const hashed = bcrypt.hashSync(plainPassword, 12); return new Password(hashed, true); } static fromHashed(hashedPassword: string): Password { return new Password(hashedPassword, true); } compare(plainPassword: string): boolean { return bcrypt.compareSync(plainPassword, this.hashedValue); } getValue(): string { return this.hashedValue; }}Repository Interface
// src/domain/repositories/IUserRepository.tsimport { User } from '../entities/User';import { Email } from '../value-objects/Email';import { UserId } from '../value-objects/UserId'; export interface IUserRepository { findById(id: UserId): Promise<User | null>; findByEmail(email: Email): Promise<User | null>; findAll(options?: { page?: number; limit?: number; filter?: { isActive?: boolean }; }): Promise<{ users: User[]; total: number }>; save(user: User): Promise<void>; delete(id: UserId): Promise<void>; exists(email: Email): Promise<boolean>;}2. Camada de Aplicação
Orquestra os casos de uso usando as entidades de domínio.
Result Pattern
// src/shared/utils/Result.tsexport class Result<T, E = Error> { private constructor( private readonly _isSuccess: boolean, private readonly _value?: T, private readonly _error?: E ) {} static ok<T>(value: T): Result<T, never> { return new Result(true, value); } static fail<E>(error: E): Result<never, E> { return new Result(false, undefined, error); } isSuccess(): boolean { return this._isSuccess; } isFailure(): boolean { return !this._isSuccess; } getValue(): T { if (!this._isSuccess) { throw new Error('Cannot get value of a failed result'); } return this._value as T; } getError(): E { if (this._isSuccess) { throw new Error('Cannot get error of a successful result'); } return this._error as E; }}Use Case: CreateUser
// src/application/use-cases/CreateUser/CreateUserUseCase.tsimport { User } from '../../../domain/entities/User';import { Email } from '../../../domain/value-objects/Email';import { Password } from '../../../domain/value-objects/Password';import { IUserRepository } from '../../../domain/repositories/IUserRepository';import { IEventPublisher } from '../../services/IEventPublisher';import { ILogger } from '../../services/ILogger';import { Result } from '../../../shared/utils/Result';import { CreateUserDTO, CreateUserResponseDTO } from './CreateUserDTO';import { EmailAlreadyExistsError } from '../../../domain/errors'; export class CreateUserUseCase { constructor( private readonly userRepository: IUserRepository, private readonly eventPublisher: IEventPublisher, private readonly logger: ILogger ) {} async execute( dto: CreateUserDTO ): Promise<Result<CreateUserResponseDTO, Error>> { try { // 1. Criar Value Objects (validação no domínio) const email = Email.create(dto.email); const password = Password.create(dto.password); // 2. Verificar regras de negócio const emailExists = await this.userRepository.exists(email); if (emailExists) { return Result.fail(new EmailAlreadyExistsError(dto.email)); } // 3. Criar entidade const user = User.create({ email, password, name: dto.name, }); // 4. Persistir await this.userRepository.save(user); // 5. Publicar eventos de domínio const domainEvents = user.pullDomainEvents(); for (const event of domainEvents) { await this.eventPublisher.publish(event); } this.logger.info('User created successfully', { userId: user.id.getValue() }); // 6. Retornar DTO de resposta return Result.ok({ id: user.id.getValue(), email: user.email.getValue(), name: user.name, createdAt: user.createdAt, }); } catch (error) { this.logger.error('Error creating user', { error, dto }); return Result.fail(error as Error); } }}DTO
// src/application/use-cases/CreateUser/CreateUserDTO.tsexport interface CreateUserDTO { email: string; password: string; name: string;} export interface CreateUserResponseDTO { id: string; email: string; name: string; createdAt: Date;}3. Camada de Infraestrutura
Implementações concretas das interfaces.
Repository: Prisma
// src/infrastructure/database/repositories/PrismaUserRepository.tsimport { PrismaClient } from '@prisma/client';import { User } from '../../../domain/entities/User';import { Email } from '../../../domain/value-objects/Email';import { Password } from '../../../domain/value-objects/Password';import { UserId } from '../../../domain/value-objects/UserId';import { IUserRepository } from '../../../domain/repositories/IUserRepository'; export class PrismaUserRepository implements IUserRepository { constructor(private readonly prisma: PrismaClient) {} async findById(id: UserId): Promise<User | null> { const data = await this.prisma.user.findUnique({ where: { id: id.getValue() }, }); if (!data) return null; return this.toDomain(data); } async findByEmail(email: Email): Promise<User | null> { const data = await this.prisma.user.findUnique({ where: { email: email.getValue() }, }); if (!data) return null; return this.toDomain(data); } async findAll(options?: { page?: number; limit?: number; filter?: { isActive?: boolean }; }): Promise<{ users: User[]; total: number }> { const page = options?.page || 1; const limit = options?.limit || 10; const skip = (page - 1) * limit; const where = options?.filter?.isActive !== undefined ? { isActive: options.filter.isActive } : {}; const [data, total] = await Promise.all([ this.prisma.user.findMany({ where, skip, take: limit, orderBy: { createdAt: 'desc' }, }), this.prisma.user.count({ where }), ]); return { users: data.map(this.toDomain), total, }; } async save(user: User): Promise<void> { const data = this.toPersistence(user); await this.prisma.user.upsert({ where: { id: data.id }, create: data, update: data, }); } async delete(id: UserId): Promise<void> { await this.prisma.user.delete({ where: { id: id.getValue() }, }); } async exists(email: Email): Promise<boolean> { const count = await this.prisma.user.count({ where: { email: email.getValue() }, }); return count > 0; } // Mappers private toDomain(data: any): User { return User.reconstitute({ id: UserId.fromString(data.id), email: Email.create(data.email), password: Password.fromHashed(data.password), name: data.name, isActive: data.isActive, createdAt: data.createdAt, updatedAt: data.updatedAt, }); } private toPersistence(user: User): any { return { id: user.id.getValue(), email: user.email.getValue(), password: user.password.getValue(), name: user.name, isActive: user.isActive, createdAt: user.createdAt, updatedAt: user.updatedAt, }; }}Event Publisher: RabbitMQ
// src/infrastructure/messaging/RabbitMQEventPublisher.tsimport amqp, { Channel, Connection } from 'amqplib';import { IEventPublisher } from '../../application/services/IEventPublisher';import { DomainEvent } from '../../domain/events/DomainEvent';import { ILogger } from '../../application/services/ILogger'; export class RabbitMQEventPublisher implements IEventPublisher { private connection: Connection | null = null; private channel: Channel | null = null; private readonly exchange = 'domain.events'; constructor( private readonly url: string, private readonly logger: ILogger ) {} async connect(): Promise<void> { this.connection = await amqp.connect(this.url); this.channel = await this.connection.createChannel(); await this.channel.assertExchange(this.exchange, 'topic', { durable: true, }); this.logger.info('Connected to RabbitMQ'); } async publish(event: DomainEvent): Promise<void> { if (!this.channel) { throw new Error('RabbitMQ channel not initialized'); } const routingKey = event.eventName; const message = Buffer.from(JSON.stringify({ eventId: event.eventId, eventName: event.eventName, occurredAt: event.occurredAt, data: event.toJSON(), })); this.channel.publish(this.exchange, routingKey, message, { persistent: true, contentType: 'application/json', timestamp: Date.now(), }); this.logger.info('Event published', { eventName: event.eventName }); } async disconnect(): Promise<void> { await this.channel?.close(); await this.connection?.close(); this.logger.info('Disconnected from RabbitMQ'); }}4. Camada de Apresentação
Controllers e rotas HTTP.
Controller
// src/presentation/http/controllers/UserController.tsimport { Request, Response } from 'express';import { CreateUserUseCase } from '../../../application/use-cases/CreateUser';import { GetUserUseCase } from '../../../application/use-cases/GetUser';import { ListUsersUseCase } from '../../../application/use-cases/ListUsers';import { UpdateUserUseCase } from '../../../application/use-cases/UpdateUser';import { DeleteUserUseCase } from '../../../application/use-cases/DeleteUser';import { UserPresenter } from '../presenters/UserPresenter'; export class UserController { constructor( private readonly createUserUseCase: CreateUserUseCase, private readonly getUserUseCase: GetUserUseCase, private readonly listUsersUseCase: ListUsersUseCase, private readonly updateUserUseCase: UpdateUserUseCase, private readonly deleteUserUseCase: DeleteUserUseCase ) {} async create(req: Request, res: Response): Promise<Response> { const result = await this.createUserUseCase.execute(req.body); if (result.isFailure()) { const error = result.getError(); return res.status(this.getErrorStatus(error)).json({ error: error.message, }); } return res.status(201).json(UserPresenter.toJSON(result.getValue())); } async getById(req: Request, res: Response): Promise<Response> { const result = await this.getUserUseCase.execute({ id: req.params.id }); if (result.isFailure()) { return res.status(404).json({ error: 'User not found' }); } return res.json(UserPresenter.toJSON(result.getValue())); } async list(req: Request, res: Response): Promise<Response> { const { page = 1, limit = 10, isActive } = req.query; const result = await this.listUsersUseCase.execute({ page: Number(page), limit: Number(limit), filter: isActive !== undefined ? { isActive: isActive === 'true' } : undefined, }); if (result.isFailure()) { return res.status(500).json({ error: 'Internal server error' }); } const data = result.getValue(); return res.json({ data: data.users.map(UserPresenter.toJSON), pagination: { page: Number(page), limit: Number(limit), total: data.total, totalPages: Math.ceil(data.total / Number(limit)), }, }); } async update(req: Request, res: Response): Promise<Response> { const result = await this.updateUserUseCase.execute({ id: req.params.id, ...req.body, }); if (result.isFailure()) { const error = result.getError(); return res.status(this.getErrorStatus(error)).json({ error: error.message, }); } return res.json(UserPresenter.toJSON(result.getValue())); } async delete(req: Request, res: Response): Promise<Response> { const result = await this.deleteUserUseCase.execute({ id: req.params.id }); if (result.isFailure()) { return res.status(404).json({ error: 'User not found' }); } return res.status(204).send(); } private getErrorStatus(error: Error): number { switch (error.constructor.name) { case 'EmailAlreadyExistsError': return 409; case 'UserNotFoundError': return 404; case 'ValidationError': case 'DomainError': return 400; default: return 500; } }}Presenter
// src/presentation/http/presenters/UserPresenter.tsexport class UserPresenter { static toJSON(user: any): object { return { id: user.id, email: user.email, name: user.name, isActive: user.isActive, createdAt: user.createdAt?.toISOString(), updatedAt: user.updatedAt?.toISOString(), }; }}5. Injeção de Dependências
// src/shared/container/index.tsimport { PrismaClient } from '@prisma/client';import { PrismaUserRepository } from '../../infrastructure/database/repositories/PrismaUserRepository';import { RabbitMQEventPublisher } from '../../infrastructure/messaging/RabbitMQEventPublisher';import { WinstonLogger } from '../../infrastructure/services/WinstonLogger';import { CreateUserUseCase } from '../../application/use-cases/CreateUser';import { GetUserUseCase } from '../../application/use-cases/GetUser';import { ListUsersUseCase } from '../../application/use-cases/ListUsers';import { UpdateUserUseCase } from '../../application/use-cases/UpdateUser';import { DeleteUserUseCase } from '../../application/use-cases/DeleteUser';import { UserController } from '../../presentation/http/controllers/UserController';import { env } from '../../infrastructure/config/env'; // Infrastructureconst prisma = new PrismaClient();const logger = new WinstonLogger();const eventPublisher = new RabbitMQEventPublisher(env.RABBITMQ_URL, logger); // Repositoriesconst userRepository = new PrismaUserRepository(prisma); // Use Casesconst createUserUseCase = new CreateUserUseCase( userRepository, eventPublisher, logger);const getUserUseCase = new GetUserUseCase(userRepository, logger);const listUsersUseCase = new ListUsersUseCase(userRepository, logger);const updateUserUseCase = new UpdateUserUseCase( userRepository, eventPublisher, logger);const deleteUserUseCase = new DeleteUserUseCase( userRepository, eventPublisher, logger); // Controllersexport const userController = new UserController( createUserUseCase, getUserUseCase, listUsersUseCase, updateUserUseCase, deleteUserUseCase); // Initialize connectionsexport async function initializeContainer(): Promise<void> { await eventPublisher.connect(); logger.info('Container initialized');} // Cleanupexport async function disposeContainer(): Promise<void> { await eventPublisher.disconnect(); await prisma.$disconnect(); logger.info('Container disposed');}Padrões de Resiliência
Circuit Breaker
// src/shared/patterns/CircuitBreaker.tstype State = 'CLOSED' | 'OPEN' | 'HALF_OPEN'; interface CircuitBreakerOptions { timeout: number; errorThreshold: number; resetTimeout: number; onStateChange?: (from: State, to: State) => void;} export class CircuitBreaker { private state: State = 'CLOSED'; private failures = 0; private successes = 0; private lastFailureTime?: number; private readonly halfOpenMaxAttempts = 3; constructor(private readonly options: CircuitBreakerOptions) {} async execute<T>(fn: () => Promise<T>): Promise<T> { if (this.state === 'OPEN') { if (Date.now() - this.lastFailureTime! > this.options.resetTimeout) { this.transitionTo('HALF_OPEN'); } else { throw new CircuitBreakerOpenError('Circuit breaker is OPEN'); } } try { const result = await Promise.race([ fn(), this.timeout(), ]); this.onSuccess(); return result as T; } catch (error) { this.onFailure(); throw error; } } private timeout(): Promise<never> { return new Promise((_, reject) => { setTimeout( () => reject(new Error('Operation timed out')), this.options.timeout ); }); } private onSuccess(): void { if (this.state === 'HALF_OPEN') { this.successes++; if (this.successes >= this.halfOpenMaxAttempts) { this.transitionTo('CLOSED'); } } else { this.failures = 0; } } private onFailure(): void { this.failures++; this.lastFailureTime = Date.now(); if (this.state === 'HALF_OPEN') { this.transitionTo('OPEN'); } else if (this.failures >= this.options.errorThreshold) { this.transitionTo('OPEN'); } } private transitionTo(newState: State): void { if (this.state !== newState) { this.options.onStateChange?.(this.state, newState); this.state = newState; this.failures = 0; this.successes = 0; } } getState(): State { return this.state; }} class CircuitBreakerOpenError extends Error { constructor(message: string) { super(message); this.name = 'CircuitBreakerOpenError'; }}Retry com Exponential Backoff e Jitter
// src/shared/patterns/RetryWithBackoff.tsinterface RetryOptions { maxRetries: number; baseDelay: number; maxDelay: number; shouldRetry?: (error: Error) => boolean;} export async function retryWithBackoff<T>( fn: () => Promise<T>, options: RetryOptions): Promise<T> { const { maxRetries, baseDelay, maxDelay, shouldRetry } = options; let lastError: Error; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { return await fn(); } catch (error) { lastError = error as Error; // Verifica se deve tentar novamente if (shouldRetry && !shouldRetry(lastError)) { throw lastError; } if (attempt < maxRetries) { // Exponential backoff com jitter const exponentialDelay = baseDelay * Math.pow(2, attempt); const jitter = Math.random() * 0.3 * exponentialDelay; const delay = Math.min(exponentialDelay + jitter, maxDelay); await new Promise((resolve) => setTimeout(resolve, delay)); } } } throw lastError!;}Testes
Teste Unitário do Caso de Uso
// tests/unit/application/use-cases/CreateUserUseCase.spec.tsimport { CreateUserUseCase } from '../../../../src/application/use-cases/CreateUser';import { IUserRepository } from '../../../../src/domain/repositories/IUserRepository';import { IEventPublisher } from '../../../../src/application/services/IEventPublisher';import { ILogger } from '../../../../src/application/services/ILogger'; describe('CreateUserUseCase', () => { let useCase: CreateUserUseCase; let mockUserRepository: jest.Mocked<IUserRepository>; let mockEventPublisher: jest.Mocked<IEventPublisher>; let mockLogger: jest.Mocked<ILogger>; beforeEach(() => { mockUserRepository = { findById: jest.fn(), findByEmail: jest.fn(), findAll: jest.fn(), save: jest.fn(), delete: jest.fn(), exists: jest.fn(), }; mockEventPublisher = { publish: jest.fn(), }; mockLogger = { info: jest.fn(), error: jest.fn(), warn: jest.fn(), debug: jest.fn(), }; useCase = new CreateUserUseCase( mockUserRepository, mockEventPublisher, mockLogger ); }); it('should create a user successfully', async () => { mockUserRepository.exists.mockResolvedValue(false); mockUserRepository.save.mockResolvedValue(); mockEventPublisher.publish.mockResolvedValue(); const result = await useCase.execute({ email: 'test@example.com', password: 'Password123', name: 'John Doe', }); expect(result.isSuccess()).toBe(true); expect(result.getValue().email).toBe('test@example.com'); expect(mockUserRepository.save).toHaveBeenCalledTimes(1); expect(mockEventPublisher.publish).toHaveBeenCalledTimes(1); }); it('should fail when email already exists', async () => { mockUserRepository.exists.mockResolvedValue(true); const result = await useCase.execute({ email: 'existing@example.com', password: 'Password123', name: 'John Doe', }); expect(result.isFailure()).toBe(true); expect(result.getError().message).toContain('already exists'); expect(mockUserRepository.save).not.toHaveBeenCalled(); }); it('should fail with invalid email', async () => { const result = await useCase.execute({ email: 'invalid-email', password: 'Password123', name: 'John Doe', }); expect(result.isFailure()).toBe(true); expect(result.getError().message).toContain('email'); }); it('should fail with weak password', async () => { const result = await useCase.execute({ email: 'test@example.com', password: 'weak', name: 'John Doe', }); expect(result.isFailure()).toBe(true); expect(result.getError().message).toContain('Password'); });});Containerização com Docker
# docker/Dockerfile# Build stageFROM node:20-alpine AS builder WORKDIR /app # Copiar arquivos de dependênciasCOPY package*.json ./COPY prisma ./prisma/ # Instalar dependênciasRUN npm ci # Copiar código fonteCOPY . . # Gerar cliente Prisma e buildRUN npx prisma generateRUN npm run build # Remover devDependenciesRUN npm prune --production # Production stageFROM node:20-alpine WORKDIR /app # Criar usuário não-rootRUN addgroup -g 1001 -S nodejs && \ adduser -S nodejs -u 1001 # Copiar artefatos do buildCOPY --from=builder --chown=nodejs:nodejs /app/dist ./distCOPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modulesCOPY --from=builder --chown=nodejs:nodejs /app/prisma ./prismaCOPY --from=builder --chown=nodejs:nodejs /app/package.json ./ # Variáveis de ambienteENV NODE_ENV=productionENV PORT=3000 # Usuário não-rootUSER nodejs # Health checkHEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health/live || exit 1 EXPOSE 3000 CMD ["node", "dist/server.js"]Docker Compose
# docker/docker-compose.ymlversion: '3.8' services: user-service: build: context: .. dockerfile: docker/Dockerfile ports: - "3000:3000" environment: - NODE_ENV=production - DATABASE_URL=postgresql://user:password@postgres:5432/users - RABBITMQ_URL=amqp://guest:guest@rabbitmq:5672 - REDIS_URL=redis://redis:6379 depends_on: postgres: condition: service_healthy rabbitmq: condition: service_healthy redis: condition: service_started networks: - microservices postgres: image: postgres:15-alpine environment: POSTGRES_USER: user POSTGRES_PASSWORD: password POSTGRES_DB: users volumes: - postgres_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U user -d users"] interval: 5s timeout: 5s retries: 5 networks: - microservices rabbitmq: image: rabbitmq:3-management-alpine ports: - "15672:15672" environment: RABBITMQ_DEFAULT_USER: guest RABBITMQ_DEFAULT_PASS: guest healthcheck: test: rabbitmq-diagnostics -q ping interval: 10s timeout: 5s retries: 5 networks: - microservices redis: image: redis:7-alpine volumes: - redis_data:/data networks: - microservices volumes: postgres_data: redis_data: networks: microservices: driver: bridgeOrquestração com Kubernetes
# k8s/deployment.yamlapiVersion: apps/v1kind: Deploymentmetadata: name: user-service labels: app: user-servicespec: replicas: 3 strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0 selector: matchLabels: app: user-service template: metadata: labels: app: user-service annotations: prometheus.io/scrape: "true" prometheus.io/port: "3000" prometheus.io/path: "/metrics" spec: terminationGracePeriodSeconds: 30 containers: - name: user-service image: myregistry/user-service:latest imagePullPolicy: Always ports: - containerPort: 3000 protocol: TCP env: - name: NODE_ENV value: "production" - name: DATABASE_URL valueFrom: secretKeyRef: name: user-service-secrets key: database-url - name: RABBITMQ_URL valueFrom: secretKeyRef: name: user-service-secrets key: rabbitmq-url resources: requests: memory: "128Mi" cpu: "100m" limits: memory: "512Mi" cpu: "500m" livenessProbe: httpGet: path: /health/live port: 3000 initialDelaySeconds: 15 periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 3 readinessProbe: httpGet: path: /health/ready port: 3000 initialDelaySeconds: 5 periodSeconds: 5 timeoutSeconds: 3 failureThreshold: 3 lifecycle: preStop: exec: command: ["/bin/sh", "-c", "sleep 10"]HPA (Horizontal Pod Autoscaler)
# k8s/hpa.yamlapiVersion: autoscaling/v2kind: HorizontalPodAutoscalermetadata: name: user-service-hpaspec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: user-service minReplicas: 3 maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 70 - type: Resource resource: name: memory target: type: Utilization averageUtilization: 80 behavior: scaleDown: stabilizationWindowSeconds: 300 policies: - type: Percent value: 10 periodSeconds: 60 scaleUp: stabilizationWindowSeconds: 0 policies: - type: Percent value: 100 periodSeconds: 15 - type: Pods value: 4 periodSeconds: 15 selectPolicy: MaxObservabilidade
Health Checks
// src/presentation/http/controllers/HealthController.tsimport { Request, Response } from 'express';import { PrismaClient } from '@prisma/client';import { Channel } from 'amqplib'; interface HealthCheck { name: string; status: 'healthy' | 'unhealthy'; responseTime?: number; error?: string;} export class HealthController { constructor( private readonly prisma: PrismaClient, private readonly rabbitChannel: Channel | null ) {} // Liveness: o container está rodando? async live(req: Request, res: Response): Promise<Response> { return res.status(200).json({ status: 'alive', timestamp: new Date().toISOString(), }); } // Readiness: o serviço está pronto para receber tráfego? async ready(req: Request, res: Response): Promise<Response> { const checks: HealthCheck[] = await Promise.all([ this.checkDatabase(), this.checkRabbitMQ(), ]); const allHealthy = checks.every((c) => c.status === 'healthy'); return res.status(allHealthy ? 200 : 503).json({ status: allHealthy ? 'ready' : 'not ready', timestamp: new Date().toISOString(), checks, }); } private async checkDatabase(): Promise<HealthCheck> { const start = Date.now(); try { await this.prisma.$queryRaw`SELECT 1`; return { name: 'database', status: 'healthy', responseTime: Date.now() - start, }; } catch (error) { return { name: 'database', status: 'unhealthy', error: (error as Error).message, }; } } private async checkRabbitMQ(): Promise<HealthCheck> { try { if (!this.rabbitChannel) { throw new Error('Channel not initialized'); } return { name: 'rabbitmq', status: 'healthy', }; } catch (error) { return { name: 'rabbitmq', status: 'unhealthy', error: (error as Error).message, }; } }}Checklist de Produção
Antes de fazer deploy em produção, verifique:
Segurança
- Variáveis sensíveis em secrets (não no código)
- HTTPS habilitado
- Rate limiting configurado
- Validação de input em todas as rotas
- Headers de segurança (Helmet)
- Autenticação/Autorização implementada
Performance
- Connection pooling do banco de dados
- Cache implementado (Redis)
- Queries otimizadas (índices)
- Compressão gzip habilitada
- Logs em formato JSON estruturado
Resiliência
- Circuit breaker para chamadas externas
- Retry com backoff
- Timeouts configurados
- Graceful shutdown implementado
- Health checks funcionando
Observabilidade
- Logs estruturados
- Métricas expostas (Prometheus)
- Tracing distribuído (Jaeger/Zipkin)
- Alertas configurados
- Dashboard de monitoramento
Conclusão
Microserviços com Node.js, seguindo Clean Architecture, DDD e SOLID, oferecem uma base sólida para sistemas escaláveis e manuteníveis. Pontos-chave para lembrar:
- Comece pelo domínio: Modele seu negócio primeiro, frameworks depois
- Mantenha as camadas separadas: Dependency Inversion é fundamental
- Testes são obrigatórios: Especialmente na camada de domínio
- Invista em observabilidade: Logs, métricas e traces desde o início
- Automatize tudo: CI/CD, infraestrutura como código
- Resiliência por padrão: Circuit breaker, retry, timeout
No próximo artigo, aprenda como implementar um API Gateway com Kong para gerenciar seus microserviços.
Gostou do conteúdo? Sua contribuição ajuda a manter tudo online e gratuito!
0737160d-e98f-4a65-8392-5dba70e7ff3e