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' });
}
});
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: ClusterIPEl 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)

