Pular para o conteúdoPedro Farbo

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.

PIX:0737160d-e98f-4a65-8392-5dba70e7ff3e

Microserviç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ípioDescriçãoAplicação em Microserviços
Single ResponsibilityUma classe deve ter apenas um motivo para mudarCada serviço tem uma responsabilidade bem definida
Open/ClosedAberto para extensão, fechado para modificaçãoUse interfaces e injeção de dependências
Liskov SubstitutionSubtipos devem ser substituíveis por seus tipos baseContratos de API consistentes
Interface SegregationMuitas interfaces específicas são melhores que uma geralAPIs focadas e coesas
Dependency InversionDependa de abstrações, não de implementaçõesCamadas 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

typescript
// 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

typescript
// 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

typescript
// 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

typescript
// 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

typescript
// 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

typescript
// 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

typescript
// 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

typescript
// 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

typescript
// 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

typescript
// 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

typescript
// 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

typescript
// 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

typescript
// 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

typescript
// 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

typescript
// 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

dockerfile
# 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

yaml
# 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: bridge

Orquestração com Kubernetes

yaml
# 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)

yaml
# 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: Max

Observabilidade

Health Checks

typescript
// 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:

  1. Comece pelo domínio: Modele seu negócio primeiro, frameworks depois
  2. Mantenha as camadas separadas: Dependency Inversion é fundamental
  3. Testes são obrigatórios: Especialmente na camada de domínio
  4. Invista em observabilidade: Logs, métricas e traces desde o início
  5. Automatize tudo: CI/CD, infraestrutura como código
  6. 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.

PF
Sobre o autor

Pedro Farbo

Platform Engineering Lead & Solutions Architect com +10 anos de experiência. CEO da Farbo TSC. Especialista em Microserviços, Kong, Backstage e Cloud.

Gostou do conteúdo? Sua contribuição ajuda a manter tudo online e gratuito!

PIX:0737160d-e98f-4a65-8392-5dba70e7ff3e