Voltar para o Blog
Anúncio

Microservices com Node.js: Arquitetura Moderna em 2025

Olá HaWkers, microservices não são mais buzzword - em 2025, são a arquitetura padrão para aplicações escaláveis. Empresas como Netflix, Uber e Amazon rodam milhares de microservices em produção. E Node.js? Continua sendo a escolha #1 para construir esses serviços devido à sua natureza assíncrona e ecosistema rico.

Mas construir microservices não é apenas quebrar um monolito em pedaços menores. É sobre comunicação, resiliência, observabilidade e deployment coordenado. É sobre arquitetura distribuída que escala sem virar caos.

Vamos explorar como construir microservices modernos com Node.js, do básico aos padrões avançados usados em produção.

Anatomia de um Microservice Node.js

Um microservice bem projetado é pequeno, focado e independente:

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

// Metrics para Prometheus
const register = new Registry();
collectDefaultMetrics({ register });

// Database connection
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 (essencial 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 se serviço está pronto para receber tráfego
  const isReady = await checkDependencies();

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

// Metrics endpoint
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 {
    // Tentar cache primeiro
    const cached = await redis.get(`user:${id}`);
    if (cached) {
      return res.json(JSON.parse(cached));
    }

    // Buscar do 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 (pattern: 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 aceitar novas conexões
  server.close(async () => {
    // Fechar conexões
    await redis.quit();
    await sql.end();

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

  // Force shutdown após 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 no message broker (RabbitMQ, Kafka, etc)
  // Implementação simplificada
  await redis.publish('events', JSON.stringify({ type, data }));
}
Anúncio

Comunicação Entre Microservices

Existem dois padrões principais: síncrono (HTTP/gRPC) e assíncrono (mensageria).

Comunicação HTTP com Circuit Breaker

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

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

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

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

    // Eventos do 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 em outro serviço
import { ServiceClient } from './shared/http-client.js';

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

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

  try {
    // Chamar múltiplos serviços
    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 vai abrir automaticamente se muitos erros
    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

Comunicação Assíncrona com 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');

    // Handling de erros
    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 (ou enviar para 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 outros serviços
  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) => {
      // Processar evento
      if (event.type === 'order.created') {
        await sendOrderConfirmationEmail(event.userId, event.orderId);
      }
    }
  );
});
Anúncio

API Gateway com Express Gateway

API Gateway é o ponto de entrada único para todos os microservices:

// 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());

// Authentication middleware
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' });
  }
};

// Logging middleware
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();
});

// Dynamic routing
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' });
  }

  // Adicionar 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}`]: ''
    }
  });
});

// Error handling
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 com 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 simples
    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 no serviço
const discovery = new ServiceDiscovery();

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

// Descobrir e chamar outro serviço
const orderServiceInstance = await discovery.discover('order-service');
const response = await axios.get(`${orderServiceInstance.url}/orders/123`);
Anúncio

Deployment com Docker e Kubernetes

# Dockerfile otimizado
FROM node:20-alpine AS builder

WORKDIR /app

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

COPY . .

# Build se necessário
RUN npm run build

FROM node:20-alpine

WORKDIR /app

# Copiar apenas o necessário
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./

# Security: não rodar 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

O Futuro dos Microservices em 2025

As tendências mais empolgantes:

  • Service Mesh: Istio e Linkerd para observabilidade avançada
  • Event-driven architectures: Kafka e event sourcing
  • Serverless microservices: AWS Lambda e Cloudflare Workers
  • Edge microservices: Compute distribuído globalmente
  • AI-assisted orchestration: Kubernetes com AI para auto-scaling inteligente

Microservices em 2025 são sobre construir sistemas distribuídos resilientes, observáveis e fáceis de manter. Node.js continua sendo a escolha ideal pela sua performance, ecosistema e natureza event-driven.

Se você quer explorar mais sobre arquiteturas modernas, recomendo ler meu artigo sobre Serverless em 2025: Por Que Node.js Domina (E Como Usar) onde discuto arquiteturas complementares.

Bora pra cima! 🦅

💻 Domine JavaScript de Verdade

O conhecimento que você adquiriu neste artigo é só o começo. Há técnicas, padrões e práticas que transformam desenvolvedores iniciantes em profissionais requisitados.

Formas de pagamento:

  • 3x de R$34,54 sem juros
  • ou R$97,90 à vista

📖 Ver Conteúdo Completo

Anúncio
Post anteriorPróximo post

Comentários (0)

Esse artigo ainda não possui comentários 😢. Seja o primeiro! 🚀🦅

Adicionar comentário