Volver al blog

Microservicios con Node.js: Arquitectura Moderna en 2025

Hola HaWkers, los microservicios ya no son buzzword - en 2025, son la arquitectura estándar para aplicaciones escalables. Empresas como Netflix, Uber y Amazon corren miles de microservicios en producción. ¿Y Node.js? Sigue siendo la opción #1 para construir estos servicios debido a su naturaleza asíncrona y ecosistema rico.

Pero construir microservicios no es solo separar un monolito en piezas menores. Se trata de comunicación, resiliencia, observabilidad y deployment coordinado. Se trata de arquitectura distribuida que escala sin volverse caos.

Vamos a explorar cómo construir microservicios modernos con Node.js, desde lo básico hasta los patrones avanzados usados en producción.

Anatomía de un Microservicio Node.js

Un microservicio bien diseñado es pequeño, enfocado e independiente:

// user-service/src/server.js
import express from 'express';
import { createClient } from 'redis';
import postgres from 'postgres';
import { Registry, collectDefaultMetrics } from 'prom-client';

const app = express();
const PORT = process.env.PORT || 3001;

// Métricas para Prometheus
const register = new Registry();
collectDefaultMetrics({ register });

// Conexión a base de datos
const sql = postgres({
  host: process.env.DB_HOST,
  database: process.env.DB_NAME,
  username: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  max: 10
});

// Redis para cache
const redis = createClient({
  url: process.env.REDIS_URL
});

await redis.connect();

// Middleware
app.use(express.json());

// Health checks (esencial para K8s)
app.get('/health', async (req, res) => {
  try {
    await sql`SELECT 1`;
    await redis.ping();

    res.json({
      status: 'healthy',
      timestamp: new Date().toISOString(),
      service: 'user-service',
      version: process.env.VERSION || '1.0.0'
    });
  } catch (error) {
    res.status(503).json({
      status: 'unhealthy',
      error: error.message
    });
  }
});

// Readiness probe (¡diferente de liveness!)
app.get('/ready', async (req, res) => {
  // Verificar si el servicio está listo para recibir tráfico
  const isReady = await checkDependencies();

  if (isReady) {
    res.json({ ready: true });
  } else {
    res.status(503).json({ ready: false });
  }
});

// Endpoint de métricas
app.get('/metrics', async (req, res) => {
  res.set('Content-Type', register.contentType);
  res.end(await register.metrics());
});

// API endpoints
app.get('/users/:id', async (req, res) => {
  const { id } = req.params;

  try {
    // Intentar cache primero
    const cached = await redis.get(`user:${id}`);
    if (cached) {
      return res.json(JSON.parse(cached));
    }

    // Buscar del banco
    const [user] = await sql`
      SELECT id, name, email, created_at
      FROM users
      WHERE id = ${id}
    `;

    if (!user) {
      return res.status(404).json({ error: 'User not found' });
    }

    // Cachear por 5 minutos
    await redis.setEx(`user:${id}`, 300, JSON.stringify(user));

    res.json(user);

  } catch (error) {
    console.error('Error fetching user:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

app.post('/users', async (req, res) => {
  const { name, email } = req.body;

  if (!name || !email) {
    return res.status(400).json({ error: 'Name and email required' });
  }

  try {
    const [user] = await sql`
      INSERT INTO users (name, email, created_at)
      VALUES (${name}, ${email}, NOW())
      RETURNING id, name, email, created_at
    `;

    // Invalidar cache (patrón: write-through)
    await redis.del(`users:list`);

    // Publicar evento
    await publishEvent('user.created', user);

    res.status(201).json(user);

  } catch (error) {
    console.error('Error creating user:', error);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// Graceful shutdown
process.on('SIGTERM', async () => {
  console.log('SIGTERM received, shutting down gracefully...');

  // Parar de aceptar nuevas conexiones
  server.close(async () => {
    // Cerrar conexiones
    await redis.quit();
    await sql.end();

    console.log('Shutdown complete');
    process.exit(0);
  });

  // Forzar shutdown después de 30s
  setTimeout(() => {
    console.error('Forced shutdown after timeout');
    process.exit(1);
  }, 30000);
});

const server = app.listen(PORT, () => {
  console.log(`User service listening on port ${PORT}`);
});

async function checkDependencies() {
  try {
    await Promise.all([
      sql`SELECT 1`,
      redis.ping()
    ]);
    return true;
  } catch {
    return false;
  }
}

async function publishEvent(type, data) {
  // Publicar en message broker (RabbitMQ, Kafka, etc)
  // Implementación simplificada
  await redis.publish('events', JSON.stringify({ type, data }));
}

Comunicación Entre Microservicios

Existen dos patrones principales: síncrono (HTTP/gRPC) y asíncrono (mensajería).

Comunicación HTTP con Circuit Breaker

// shared/http-client.js
import axios from 'axios';
import CircuitBreaker from 'opossum';

export class ServiceClient {
  constructor(baseURL, options = {}) {
    this.baseURL = baseURL;

    // Crear cliente axios
    this.client = axios.create({
      baseURL,
      timeout: options.timeout || 5000,
      headers: {
        'Content-Type': 'application/json'
      }
    });

    // Circuit breaker para resiliencia
    this.breaker = new CircuitBreaker(
      async (config) => this.client.request(config),
      {
        timeout: options.timeout || 5000,
        errorThresholdPercentage: 50,
        resetTimeout: 30000,
        volumeThreshold: 10
      }
    );

    // Eventos del circuit breaker
    this.breaker.on('open', () => {
      console.warn(`Circuit breaker OPEN for ${baseURL}`);
    });

    this.breaker.on('halfOpen', () => {
      console.log(`Circuit breaker HALF_OPEN for ${baseURL}`);
    });

    this.breaker.on('close', () => {
      console.log(`Circuit breaker CLOSED for ${baseURL}`);
    });
  }

  async get(path, config = {}) {
    return this.breaker.fire({
      method: 'GET',
      url: path,
      ...config
    });
  }

  async post(path, data, config = {}) {
    return this.breaker.fire({
      method: 'POST',
      url: path,
      data,
      ...config
    });
  }

  getStats() {
    return {
      state: this.breaker.status.name,
      stats: this.breaker.stats
    };
  }
}

// Uso en otro servicio
import { ServiceClient } from './shared/http-client.js';

const userService = new ServiceClient('http://user-service:3001');
const orderService = new ServiceClient('http://order-service:3002');

// En un endpoint
app.get('/user-orders/:userId', async (req, res) => {
  const { userId } = req.params;

  try {
    // Llamar múltiples servicios
    const [userRes, ordersRes] = await Promise.all([
      userService.get(`/users/${userId}`),
      orderService.get(`/orders?userId=${userId}`)
    ]);

    res.json({
      user: userRes.data,
      orders: ordersRes.data
    });

  } catch (error) {
    // Circuit breaker se abrirá automáticamente si hay muchos errores
    console.error('Error fetching user orders:', error);

    if (error.message.includes('breaker is open')) {
      return res.status(503).json({
        error: 'Service temporarily unavailable'
      });
    }

    res.status(500).json({ error: 'Internal server error' });
  }
});

Microservices communication

Comunicación Asíncrona con RabbitMQ

// shared/message-broker.js
import amqp from 'amqplib';

export class MessageBroker {
  constructor(url) {
    this.url = url;
    this.connection = null;
    this.channel = null;
  }

  async connect() {
    this.connection = await amqp.connect(this.url);
    this.channel = await this.connection.createChannel();

    console.log('Connected to message broker');

    // Manejo de errores
    this.connection.on('error', (err) => {
      console.error('Connection error:', err);
    });

    this.connection.on('close', () => {
      console.log('Connection closed, reconnecting...');
      setTimeout(() => this.connect(), 5000);
    });
  }

  async publish(exchange, routingKey, message) {
    await this.channel.assertExchange(exchange, 'topic', {
      durable: true
    });

    const content = Buffer.from(JSON.stringify(message));

    this.channel.publish(exchange, routingKey, content, {
      persistent: true,
      timestamp: Date.now(),
      messageId: crypto.randomUUID()
    });

    console.log(`Published: ${routingKey}`, message);
  }

  async subscribe(exchange, queue, routingKey, handler) {
    await this.channel.assertExchange(exchange, 'topic', {
      durable: true
    });

    await this.channel.assertQueue(queue, {
      durable: true
    });

    await this.channel.bindQueue(queue, exchange, routingKey);

    this.channel.consume(queue, async (msg) => {
      if (!msg) return;

      try {
        const content = JSON.parse(msg.content.toString());
        console.log(`Received: ${routingKey}`, content);

        await handler(content);

        // Acknowledge message
        this.channel.ack(msg);

      } catch (error) {
        console.error('Error processing message:', error);

        // Reject and requeue (o enviar a DLQ)
        this.channel.nack(msg, false, false);
      }
    });

    console.log(`Subscribed to: ${routingKey}`);
  }

  async close() {
    await this.channel.close();
    await this.connection.close();
  }
}

// order-service: Publicar evento
import { MessageBroker } from './shared/message-broker.js';

const broker = new MessageBroker(process.env.RABBITMQ_URL);
await broker.connect();

app.post('/orders', async (req, res) => {
  const { userId, items } = req.body;

  const order = await createOrder(userId, items);

  // Publicar evento para otros servicios
  await broker.publish('orders', 'order.created', {
    orderId: order.id,
    userId: order.userId,
    total: order.total,
    timestamp: new Date().toISOString()
  });

  res.status(201).json(order);
});

// notification-service: Consumir evento
app.listen(PORT, async () => {
  await broker.subscribe(
    'orders',
    'notification-service-orders',
    'order.*',
    async (event) => {
      // Procesar evento
      if (event.type === 'order.created') {
        await sendOrderConfirmationEmail(event.userId, event.orderId);
      }
    }
  );
});

API Gateway con Express Gateway

API Gateway es el punto de entrada único para todos los microservicios:

// gateway/server.js
import express from 'express';
import httpProxy from 'http-proxy';
import rateLimit from 'express-rate-limit';
import jwt from 'jsonwebtoken';

const app = express();
const proxy = httpProxy.createProxyServer();

// Service registry
const services = {
  users: 'http://user-service:3001',
  orders: 'http://order-service:3002',
  products: 'http://product-service:3003',
  notifications: 'http://notification-service:3004'
};

// Rate limiting global
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutos
  max: 100,
  message: 'Too many requests'
});

app.use(limiter);
app.use(express.json());

// Middleware de autenticación
const authenticate = (req, res, next) => {
  const token = req.headers.authorization?.split(' ')[1];

  if (!token) {
    return res.status(401).json({ error: 'No token provided' });
  }

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded;
    next();
  } catch (error) {
    return res.status(401).json({ error: 'Invalid token' });
  }
};

// Middleware de logging
app.use((req, res, next) => {
  const start = Date.now();

  res.on('finish', () => {
    const duration = Date.now() - start;
    console.log({
      method: req.method,
      path: req.path,
      status: res.statusCode,
      duration: `${duration}ms`,
      user: req.user?.id
    });
  });

  next();
});

// Routing dinámico
app.use('/:service/*', authenticate, (req, res) => {
  const { service } = req.params;
  const target = services[service];

  if (!target) {
    return res.status(404).json({ error: 'Service not found' });
  }

  // Agregar headers de contexto
  req.headers['x-user-id'] = req.user.id;
  req.headers['x-request-id'] = crypto.randomUUID();

  // Proxy request
  proxy.web(req, res, {
    target,
    changeOrigin: true,
    pathRewrite: {
      [`^/${service}`]: ''
    }
  });
});

// Manejo de errores
proxy.on('error', (err, req, res) => {
  console.error('Proxy error:', err);
  res.status(502).json({ error: 'Bad gateway' });
});

app.listen(3000, () => {
  console.log('API Gateway listening on port 3000');
});

Service Discovery con Consul

// shared/service-discovery.js
import Consul from 'consul';

export class ServiceDiscovery {
  constructor(consulHost = 'localhost') {
    this.consul = new Consul({
      host: consulHost,
      promisify: true
    });
  }

  async register(name, port) {
    const serviceId = `${name}-${port}`;

    await this.consul.agent.service.register({
      id: serviceId,
      name,
      address: process.env.HOST || 'localhost',
      port,
      check: {
        http: `http://localhost:${port}/health`,
        interval: '10s',
        timeout: '5s'
      }
    });

    console.log(`Service registered: ${serviceId}`);

    // Deregister on shutdown
    process.on('SIGTERM', async () => {
      await this.consul.agent.service.deregister(serviceId);
      console.log('Service deregistered');
    });
  }

  async discover(serviceName) {
    const result = await this.consul.health.service({
      service: serviceName,
      passing: true
    });

    if (result.length === 0) {
      throw new Error(`No healthy instances of ${serviceName}`);
    }

    // Load balancing: round-robin simple
    const instance = result[Math.floor(Math.random() * result.length)];

    return {
      host: instance.Service.Address,
      port: instance.Service.Port,
      url: `http://${instance.Service.Address}:${instance.Service.Port}`
    };
  }
}

// Uso en el servicio
const discovery = new ServiceDiscovery();

app.listen(PORT, async () => {
  await discovery.register('user-service', PORT);
  console.log(`User service on port ${PORT}`);
});

// Descubrir y llamar otro servicio
const orderServiceInstance = await discovery.discover('order-service');
const response = await axios.get(`${orderServiceInstance.url}/orders/123`);

Deployment con Docker y Kubernetes

# Dockerfile optimizado
FROM node:20-alpine AS builder

WORKDIR /app

# Cache de dependencias
COPY package*.json ./
RUN npm ci --only=production

COPY . .

# Build si es necesario
RUN npm run build

FROM node:20-alpine

WORKDIR /app

# Copiar solo lo necesario
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./

# Seguridad: no correr como root
RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
USER nodejs

EXPOSE 3000

CMD ["node", "dist/server.js"]
# kubernetes/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: user-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: user-service
  template:
    metadata:
      labels:
        app: user-service
    spec:
      containers:
      - name: user-service
        image: user-service:latest
        ports:
        - containerPort: 3001
        env:
        - name: DB_HOST
          valueFrom:
            configMapKeyRef:
              name: app-config
              key: db.host
        - name: DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: app-secrets
              key: db.password
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"
        livenessProbe:
          httpGet:
            path: /health
            port: 3001
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /ready
            port: 3001
          initialDelaySeconds: 10
          periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
  name: user-service
spec:
  selector:
    app: user-service
  ports:
  - port: 80
    targetPort: 3001
  type: ClusterIP

El Futuro de los Microservicios en 2025

Las tendencias más emocionantes:

  • Service Mesh: Istio y Linkerd para observabilidad avanzada
  • Event-driven architectures: Kafka y event sourcing
  • Serverless microservices: AWS Lambda y Cloudflare Workers
  • Edge microservices: Compute distribuido globalmente
  • AI-assisted orchestration: Kubernetes con IA para auto-scaling inteligente

Los microservicios en 2025 se tratan de construir sistemas distribuidos resilientes, observables y fáciles de mantener. Node.js sigue siendo la opción ideal por su performance, ecosistema y naturaleza event-driven.

Si quieres explorar más sobre arquitecturas modernas, recomiendo leer mi artículo sobre Serverless en 2025: Por Qué Node.js Domina (Y Cómo Usar) donde discuto arquitecturas complementarias.

¡Vamos a por ello! 🦅

💻 Domina JavaScript de Verdad

El conocimiento que adquiriste en este artículo es solo el comienzo. Hay técnicas, patrones y prácticas que transforman desarrolladores principiantes en profesionales solicitados.

Formas de pago:

  • $9.90 USD (pago único)

📖 Ver Contenido Completo

Comentarios (0)

Este artículo aún no tiene comentarios 😢. ¡Sé el primero! 🚀🦅

Añadir comentarios