Saltar al contenidoPedro Farbo

Arquitectura de Microservicios con Node.js: Guía Completa

Aprende a diseñar e implementar una arquitectura de microservicios robusta usando Node.js con Clean Architecture, DDD y principios SOLID.

¡Este contenido es gratuito! Ayuda a mantener el proyecto en línea.

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

Los microservicios han revolucionado la forma en que construimos aplicaciones escalables. En esta guía completa, compartiré mi experiencia de más de 10 años trabajando con arquitecturas distribuidas, aplicando Clean Architecture, DDD y principios SOLID.

¿Por qué Microservicios?

Antes de sumergirnos en la implementación, es crucial entender cuándo los microservicios tienen sentido:

Beneficios

  • Escalabilidad independiente: Escala solo los servicios que lo necesitan
  • Despliegue independiente: Actualiza un servicio sin afectar a los demás
  • Tecnologías heterogéneas: Usa la mejor herramienta para cada problema
  • Resiliencia: Una falla en un servicio no derriba todo el sistema
  • Ownership claro: Los equipos pueden ser dueños de servicios específicos

Desafíos

  • Mayor complejidad operacional
  • Comunicación entre servicios
  • Consistencia de datos distribuidos
  • Observabilidad y debugging
  • Latencia de red

Fundamentos: SOLID, Clean Architecture y DDD

Antes de estructurar nuestro microservicio, entendamos los principios que guiarán nuestras decisiones.

Principios SOLID

PrincipioDescripciónAplicación en Microservicios
Single ResponsibilityUna clase debe tener solo un motivo para cambiarCada servicio tiene una responsabilidad bien definida
Open/ClosedAbierto para extensión, cerrado para modificaciónUsa interfaces e inyección de dependencias
Liskov SubstitutionLos subtipos deben ser sustituibles por sus tipos baseContratos de API consistentes
Interface SegregationMuchas interfaces específicas son mejores que una generalAPIs enfocadas y cohesivas
Dependency InversionDepende de abstracciones, no de implementacionesLas capas internas no conocen frameworks

Clean Architecture

Clean Architecture organiza el código en capas concéntricas, donde las dependencias siempre apuntan hacia adentro:

┌─────────────────────────────────────────────────────────────┐
│                    Infrastructure                            │
│  ┌─────────────────────────────────────────────────────┐    │
│  │                   Application                        │    │
│  │  ┌─────────────────────────────────────────────┐    │    │
│  │  │                  Domain                      │    │    │
│  │  │  ┌─────────────────────────────────────┐    │    │    │
│  │  │  │            Entities                  │    │    │    │
│  │  │  └─────────────────────────────────────┘    │    │    │
│  │  └─────────────────────────────────────────────┘    │    │
│  └─────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────┘

Domain-Driven Design (DDD)

Conceptos esenciales que usaremos:

  • Entities: Objetos con identidad única
  • Value Objects: Objetos inmutables sin identidad
  • Aggregates: Cluster de entidades tratadas como una unidad
  • Repositories: Abstracción para persistencia
  • Domain Services: Lógica que no pertenece a una entidad
  • Domain Events: Notificaciones de algo que ocurrió en el dominio

Estructura de Carpetas del Microservicio

Aquí está la estructura completa siguiendo Clean Architecture y DDD:

user-service/
├── src/
│   ├── @types/                     # Definiciones de tipos globales
│   │   └── express.d.ts
│   │
│   ├── domain/                     # Capa de Dominio (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/                # Capa de Aplicación (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/             # Capa de Infraestructura
│   │   ├── 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/               # Capa de Presentación (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 compartido
│   │   ├── container/
│   │   │   └── index.ts            # Inyección de dependencias
│   │   │
│   │   ├── patterns/
│   │   │   ├── CircuitBreaker.ts
│   │   │   ├── RetryWithBackoff.ts
│   │   │   └── index.ts
│   │   │
│   │   └── utils/
│   │       ├── Result.ts           # Either/Result pattern
│   │       └── Guard.ts            # Validaciones
│   │
│   ├── app.ts                      # Configuración de 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

Implementación Detallada

1. Capa de Dominio

La capa más interna, sin dependencias 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 punto de creación  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(),    });     // Registrar evento de dominio    user.addDomainEvent(new UserCreatedEvent(user));     return user;  }   // Reconstituir desde la base de datos  static reconstitute(props: UserProps): User {    return new User(props);  }   // Getters - encapsulamiento  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;  }   // Comportamientos de dominio  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. Capa de Aplicación

Orquesta los casos de uso usando las entidades de dominio.

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. Crear Value Objects (validación en dominio)      const email = Email.create(dto.email);      const password = Password.create(dto.password);       // 2. Verificar reglas de negocio      const emailExists = await this.userRepository.exists(email);      if (emailExists) {        return Result.fail(new EmailAlreadyExistsError(dto.email));      }       // 3. Crear entidad      const user = User.create({        email,        password,        name: dto.name,      });       // 4. Persistir      await this.userRepository.save(user);       // 5. Publicar eventos de dominio      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 respuesta      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);    }  }}

3. Capa de Infraestructura

Implementaciones concretas de las 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 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,    };  }}

4. Capa de Presentación

Controllers y rutas 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 { UserPresenter } from '../presenters/UserPresenter'; export class UserController {  constructor(    private readonly createUserUseCase: CreateUserUseCase,    private readonly getUserUseCase: GetUserUseCase  ) {}   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()));  }   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;    }  }}

Patrones de Resiliencia

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;    }  }}

Retry con Exponential Backoff y 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;       if (shouldRetry && !shouldRetry(lastError)) {        throw lastError;      }       if (attempt < maxRetries) {        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!;}

Containerización con Docker

dockerfile
# docker/DockerfileFROM node:20-alpine AS builder WORKDIR /appCOPY package*.json ./COPY prisma ./prisma/RUN npm ci COPY . .RUN npx prisma generateRUN npm run buildRUN npm prune --production FROM node:20-alpine WORKDIR /app RUN addgroup -g 1001 -S nodejs && \    adduser -S nodejs -u 1001 COPY --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 ./prisma ENV NODE_ENV=productionENV PORT=3000 USER nodejs HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \  CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health/live || exit 1 EXPOSE 3000CMD ["node", "dist/server.js"]

Orquestación con Kubernetes

yaml
# k8s/deployment.yamlapiVersion: apps/v1kind: Deploymentmetadata:  name: user-servicespec:  replicas: 3  strategy:    type: RollingUpdate    rollingUpdate:      maxSurge: 1      maxUnavailable: 0  selector:    matchLabels:      app: user-service  template:    spec:      containers:        - name: user-service          image: myregistry/user-service:latest          ports:            - containerPort: 3000          resources:            requests:              memory: "128Mi"              cpu: "100m"            limits:              memory: "512Mi"              cpu: "500m"          livenessProbe:            httpGet:              path: /health/live              port: 3000            initialDelaySeconds: 15            periodSeconds: 10          readinessProbe:            httpGet:              path: /health/ready              port: 3000            initialDelaySeconds: 5            periodSeconds: 5

Observabilidad

Health Checks

typescript
// src/presentation/http/controllers/HealthController.tsimport { Request, Response } from 'express'; export class HealthController {  async live(req: Request, res: Response): Promise<Response> {    return res.status(200).json({      status: 'alive',      timestamp: new Date().toISOString(),    });  }   async ready(req: Request, res: Response): Promise<Response> {    const checks = 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,    });  }}

Checklist de Producción

Antes de desplegar en producción, verifica:

Seguridad

  • Variables sensibles en secrets (no en código)
  • HTTPS habilitado
  • Rate limiting configurado
  • Validación de input en todas las rutas
  • Headers de seguridad (Helmet)
  • Autenticación/Autorización implementada

Performance

  • Connection pooling de base de datos
  • Cache implementado (Redis)
  • Queries optimizadas (índices)
  • Compresión gzip habilitada
  • Logs en formato JSON estructurado

Resiliencia

  • Circuit breaker para llamadas externas
  • Retry con backoff
  • Timeouts configurados
  • Graceful shutdown implementado
  • Health checks funcionando

Observabilidad

  • Logs estructurados
  • Métricas expuestas (Prometheus)
  • Tracing distribuido (Jaeger/Zipkin)
  • Alertas configuradas
  • Dashboard de monitoreo

Conclusión

Los microservicios con Node.js, siguiendo Clean Architecture, DDD y SOLID, ofrecen una base sólida para sistemas escalables y mantenibles. Puntos clave para recordar:

  1. Comienza por el dominio: Modela tu negocio primero, frameworks después
  2. Mantén las capas separadas: Dependency Inversion es fundamental
  3. Los tests son obligatorios: Especialmente en la capa de dominio
  4. Invierte en observabilidad: Logs, métricas y traces desde el inicio
  5. Automatiza todo: CI/CD, infraestructura como código
  6. Resiliencia por defecto: Circuit breaker, retry, timeout

En el próximo artículo, aprende cómo implementar un API Gateway con Kong para gestionar tus microservicios.

PF
Sobre el autor

Pedro Farbo

Platform Engineering Lead & Solutions Architect con +10 años de experiencia. CEO de Farbo TSC. Especialista en Microservicios, Kong, Backstage y Cloud.

¿Te gustó el contenido? ¡Tu contribución ayuda a mantener todo online y gratuito!

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