Introducción a Microservicios
Microservicios es un patrón arquitectónico donde una aplicación se construye como conjunto de servicios pequeños, independientes y desplegables de forma autónoma, cada uno ejecutándose en su propio proceso y comunicándose mediante mecanismos ligeros (generalmente HTTP/REST o mensajería).
Monolito vs Microservicios
Monolito:
┌─────────────────────────────────┐
│ │
│ APLICACIÓN MONOLÍTICA │
│ │
│ ┌──────────┐ ┌─────────────┐ │
│ │ Auth │ │ Products │ │
│ └──────────┘ └─────────────┘ │
│ ┌──────────┐ ┌─────────────┐ │
│ │ Orders │ │ Payments │ │
│ └──────────┘ └─────────────┘ │
│ │
│ Base de Datos │
└─────────────────────────────────┘
Microservicios:
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ Auth Service │ │Product Service│ │ Order Service │
│ │ │ │ │ │
│ DB Auth │ │ DB Product │ │ DB Orders │
└───────────────┘ └───────────────┘ └───────────────┘
│ │ │
└───────────────────┴───────────────────┘
│
┌───────────────┐
│ API Gateway │
└───────────────┘
Cuándo Usar Microservicios
✅ Usar microservicios cuando:
- Equipo grande (>20 developers)
- Dominios de negocio claramente separados
- Necesitas escalar componentes independientemente
- Equipos autónomos con diferentes tecnologías
- Alto nivel de madurez DevOps
❌ NO usar microservicios cuando:
- Startup/MVP en fase temprana
- Equipo pequeño (<5 developers)
- Dominios de negocio no claros
- Infraestructura y DevOps limitados
- Preferir time-to-market rápido
Regla de oro: Empieza con monolito bien modularizado. Migra a microservicios cuando el dolor justifique la complejidad.
Patrones de Comunicación
1. Comunicación Síncrona (HTTP/REST)
// services/order-service/src/index.js
import express from 'express';
import axios from 'axios';
const app = express();
app.use(express.json());
const PRODUCT_SERVICE_URL = process.env.PRODUCT_SERVICE_URL || 'http://product-service:3001';
const USER_SERVICE_URL = process.env.USER_SERVICE_URL || 'http://user-service:3002';
app.post('/orders', async (req, res) => {
try {
const { userId, productId, quantity } = req.body;
// 1. Validar usuario
const userResponse = await axios.get(`${USER_SERVICE_URL}/users/${userId}`);
const user = userResponse.data;
if (!user) {
return res.status(404).json({ error: 'Usuario no encontrado' });
}
// 2. Verificar disponibilidad de producto
const productResponse = await axios.get(`${PRODUCT_SERVICE_URL}/products/${productId}`);
const product = productResponse.data;
if (product.stock < quantity) {
return res.status(400).json({ error: 'Stock insuficiente' });
}
// 3. Crear orden
const order = {
id: generateId(),
userId,
productId,
quantity,
total: product.price * quantity,
status: 'pending',
createdAt: new Date()
};
await db.orders.create(order);
// 4. Reducir stock (llamada a product-service)
await axios.patch(`${PRODUCT_SERVICE_URL}/products/${productId}/stock`, {
decrement: quantity
});
res.status(201).json(order);
} catch (error) {
console.error('Error creating order:', error);
res.status(500).json({ error: 'Error interno del servidor' });
}
});
app.listen(3000, () => {
console.log('Order Service running on port 3000');
});
Problema: ¿Qué pasa si falla la reducción de stock después de crear la orden?
2. Comunicación Asíncrona (Message Queue)
// services/order-service/src/index.js
import express from 'express';
import amqp from 'amqplib';
const app = express();
let channel;
// Conectar a RabbitMQ
async function connectRabbitMQ() {
const connection = await amqp.connect(process.env.RABBITMQ_URL || 'amqp://localhost');
channel = await connection.createChannel();
await channel.assertExchange('orders', 'topic', { durable: true });
await channel.assertQueue('order.created', { durable: true });
console.log('Connected to RabbitMQ');
}
connectRabbitMQ();
app.post('/orders', async (req, res) => {
try {
const { userId, productId, quantity } = req.body;
// 1. Crear orden
const order = {
id: generateId(),
userId,
productId,
quantity,
status: 'pending',
createdAt: new Date()
};
await db.orders.create(order);
// 2. Publicar evento (fire and forget)
channel.publish(
'orders',
'order.created',
Buffer.from(JSON.stringify(order)),
{ persistent: true }
);
res.status(201).json(order);
} catch (error) {
console.error('Error creating order:', error);
res.status(500).json({ error: 'Error interno del servidor' });
}
});
app.listen(3000);
// services/inventory-service/src/consumer.js
import amqp from 'amqplib';
async function startConsumer() {
const connection = await amqp.connect(process.env.RABBITMQ_URL);
const channel = await connection.createChannel();
await channel.assertExchange('orders', 'topic', { durable: true });
const queue = await channel.assertQueue('inventory.order.created', { durable: true });
await channel.bindQueue(queue.queue, 'orders', 'order.created');
console.log('Inventory Service waiting for order.created events...');
channel.consume(queue.queue, async (msg) => {
if (msg) {
const order = JSON.parse(msg.content.toString());
console.log('Received order:', order.id);
try {
// Reducir stock
await db.products.update(
{ id: order.productId },
{ $inc: { stock: -order.quantity } }
);
console.log(`Stock reduced for product ${order.productId}`);
// Confirmar procesamiento
channel.ack(msg);
// Publicar evento de confirmación
channel.publish(
'orders',
'inventory.reserved',
Buffer.from(JSON.stringify({
orderId: order.id,
productId: order.productId,
quantity: order.quantity
}))
);
} catch (error) {
console.error('Error processing order:', error);
// Reencolar mensaje
channel.nack(msg, false, true);
}
}
});
}
startConsumer();
Ventajas:
- Desacoplamiento total
- Resiliencia (si un servicio está caído, los mensajes se encolan)
- Escalabilidad independiente
Patrón Saga para Transacciones Distribuidas
Problema: Transacciones en Microservicios
En monolito:
// Todo en una transacción
db.transaction(async (trx) => {
await trx.orders.create(order);
await trx.inventory.decrement(productId, quantity);
await trx.payments.charge(userId, total);
});
// Si algo falla, todo hace rollback
En microservicios: NO hay transacciones distribuidas nativas. Solución: Pattern Saga.
Saga Coreografiada (Event-Driven)
// order-service: Paso 1
async function createOrder(orderData) {
const order = await db.orders.create({
...orderData,
status: 'pending'
});
publishEvent('order.created', order);
return order;
}
// inventory-service: Paso 2
subscribeToEvent('order.created', async (order) => {
try {
await reserveInventory(order.productId, order.quantity);
publishEvent('inventory.reserved', { orderId: order.id });
} catch (error) {
publishEvent('inventory.reservation.failed', {
orderId: order.id,
reason: error.message
});
}
});
// payment-service: Paso 3
subscribeToEvent('inventory.reserved', async (data) => {
try {
await chargePayment(data.orderId);
publishEvent('payment.completed', { orderId: data.orderId });
} catch (error) {
publishEvent('payment.failed', {
orderId: data.orderId,
reason: error.message
});
}
});
// order-service: Compensación si falla payment
subscribeToEvent('payment.failed', async (data) => {
await db.orders.update(
{ id: data.orderId },
{ status: 'cancelled', cancelReason: data.reason }
);
// Trigger compensación en inventory
publishEvent('order.cancelled', { orderId: data.orderId });
});
// inventory-service: Compensación
subscribeToEvent('order.cancelled', async (data) => {
const order = await getOrder(data.orderId);
await releaseInventory(order.productId, order.quantity);
publishEvent('inventory.released', { orderId: data.orderId });
});
Saga Orquestada (Orchestrator)
// services/order-orchestrator/src/saga.js
class OrderSaga {
constructor(orderId) {
this.orderId = orderId;
this.steps = [
{ name: 'reserve_inventory', service: 'inventory-service', compensate: 'release_inventory' },
{ name: 'process_payment', service: 'payment-service', compensate: 'refund_payment' },
{ name: 'notify_user', service: 'notification-service', compensate: null }
];
this.completedSteps = [];
}
async execute() {
try {
for (const step of this.steps) {
console.log(`Executing step: ${step.name}`);
const result = await this.executeStep(step);
if (!result.success) {
throw new Error(`Step ${step.name} failed: ${result.error}`);
}
this.completedSteps.push(step);
}
// Saga completada exitosamente
await this.markOrderComplete();
return { success: true };
} catch (error) {
console.error('Saga failed, initiating compensation:', error);
await this.compensate();
return { success: false, error: error.message };
}
}
async executeStep(step) {
try {
const response = await axios.post(
`http://${step.service}/${step.name}`,
{ orderId: this.orderId },
{ timeout: 5000 }
);
return { success: true, data: response.data };
} catch (error) {
return { success: false, error: error.message };
}
}
async compensate() {
// Compensar en orden inverso
for (const step of this.completedSteps.reverse()) {
if (step.compensate) {
console.log(`Compensating: ${step.compensate}`);
try {
await axios.post(
`http://${step.service}/${step.compensate}`,
{ orderId: this.orderId }
);
} catch (error) {
console.error(`Compensation failed for ${step.name}:`, error);
// Log to dead letter queue para revisión manual
}
}
}
await this.markOrderFailed();
}
async markOrderComplete() {
await db.orders.update(
{ id: this.orderId },
{ status: 'completed' }
);
}
async markOrderFailed() {
await db.orders.update(
{ id: this.orderId },
{ status: 'failed' }
);
}
}
// Endpoint
app.post('/orders', async (req, res) => {
const order = await createOrder(req.body);
const saga = new OrderSaga(order.id);
const result = await saga.execute();
if (result.success) {
res.status(201).json({ orderId: order.id, status: 'completed' });
} else {
res.status(400).json({ orderId: order.id, status: 'failed', error: result.error });
}
});
API Gateway
¿Por Qué API Gateway?
Sin gateway:
Mobile App ──┬──> Auth Service
├──> Product Service
├──> Order Service
└──> Payment Service
Problemas:
- Cliente necesita conocer múltiples endpoints
- CORS por todos lados
- Autenticación en cada servicio
- Sin rate limiting centralizado
Con gateway:
Mobile App ──> API Gateway ──┬──> Auth Service
├──> Product Service
├──> Order Service
└──> Payment Service
Implementación con Express
// api-gateway/src/index.js
import express from 'express';
import { createProxyMiddleware } from 'http-proxy-middleware';
import jwt from 'jsonwebtoken';
import rateLimit from 'express-rate-limit';
const app = express();
// Middleware de autenticación
const authenticateToken = (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Token requerido' });
}
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) {
return res.status(403).json({ error: 'Token inválido' });
}
req.user = user;
next();
});
};
// Rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutos
max: 100, // límite de 100 requests por ventana
message: 'Demasiadas peticiones, intenta más tarde'
});
app.use(limiter);
// Rutas públicas (sin autenticación)
app.use('/api/auth', createProxyMiddleware({
target: 'http://auth-service:3001',
changeOrigin: true,
pathRewrite: { '^/api/auth': '' }
}));
// Rutas protegidas
app.use('/api/products', authenticateToken, createProxyMiddleware({
target: 'http://product-service:3002',
changeOrigin: true,
pathRewrite: { '^/api/products': '' },
onProxyReq: (proxyReq, req) => {
// Inyectar user info en headers para servicios downstream
proxyReq.setHeader('X-User-Id', req.user.id);
proxyReq.setHeader('X-User-Role', req.user.role);
}
}));
app.use('/api/orders', authenticateToken, createProxyMiddleware({
target: 'http://order-service:3003',
changeOrigin: true,
pathRewrite: { '^/api/orders': '' },
onProxyReq: (proxyReq, req) => {
proxyReq.setHeader('X-User-Id', req.user.id);
}
}));
// Agregación de datos (Backend for Frontend pattern)
app.get('/api/dashboard', authenticateToken, async (req, res) => {
try {
const [userResponse, ordersResponse, productsResponse] = await Promise.all([
axios.get(`http://user-service:3001/users/${req.user.id}`),
axios.get(`http://order-service:3003/orders?userId=${req.user.id}`),
axios.get(`http://product-service:3002/products/recommended?userId=${req.user.id}`)
]);
res.json({
user: userResponse.data,
recentOrders: ordersResponse.data.slice(0, 5),
recommendedProducts: productsResponse.data
});
} catch (error) {
console.error('Dashboard aggregation error:', error);
res.status(500).json({ error: 'Error loading dashboard' });
}
});
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date() });
});
app.listen(3000, () => {
console.log('API Gateway running on port 3000');
});
Service Discovery y Load Balancing
Problema: Hardcoded URLs
// ❌ Frágil
const PRODUCT_SERVICE = 'http://product-service:3002';
// ¿Qué pasa si el servicio se mueve?
// ¿Cómo balanc ear carga entre múltiples instancias?
Solución: Service Discovery con Consul
// shared/service-discovery.js
import Consul from 'consul';
const consul = new Consul({
host: process.env.CONSUL_HOST || 'consul',
port: process.env.CONSUL_PORT || 8500
});
// Registrar servicio
export async function registerService(name, port, healthCheckPath = '/health') {
const serviceId = `${name}-${process.env.HOSTNAME || 'local'}`;
await consul.agent.service.register({
id: serviceId,
name: name,
address: process.env.SERVICE_IP || 'localhost',
port: port,
check: {
http: `http://${process.env.SERVICE_IP}:${port}${healthCheckPath}`,
interval: '10s',
timeout: '5s'
}
});
console.log(`Service ${name} registered with Consul`);
// Deregister on exit
process.on('SIGINT', async () => {
await consul.agent.service.deregister(serviceId);
process.exit(0);
});
}
// Descubrir servicio
export async function discoverService(name) {
const result = await consul.health.service({
service: name,
passing: true // Solo servicios healthy
});
if (result.length === 0) {
throw new Error(`No healthy instances of ${name} found`);
}
// 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}`
};
}
// Wrapper para axios con service discovery
export async function callService(serviceName, path, options = {}) {
const service = await discoverService(serviceName);
const url = `${service.url}${path}`;
return axios({
url,
...options
});
}
Uso:
// En cualquier servicio
import { registerService, callService } from './shared/service-discovery.js';
// Al iniciar
await registerService('product-service', 3002);
// Al llamar a otros servicios
const products = await callService('product-service', '/products', {
method: 'GET',
params: { category: 'electronics' }
});
Containerización con Docker
Dockerfile para Servicio Node.js
# services/product-service/Dockerfile
FROM node:18-alpine AS builder
WORKDIR /app
# Copiar package files
COPY package*.json ./
# Instalar dependencias
RUN npm ci --only=production
# Copiar código
COPY . .
# Multi-stage para imagen más pequeña
FROM node:18-alpine
WORKDIR /app
# Copiar solo lo necesario desde builder
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package*.json ./
COPY --from=builder /app/src ./src
# Usuario no-root por seguridad
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
USER nodejs
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
CMD ["node", "src/index.js"]
Docker Compose para Desarrollo
# docker-compose.yml
version: '3.8'
services:
# API Gateway
api-gateway:
build:
context: ./services/api-gateway
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
- NODE_ENV=development
- JWT_SECRET=your-secret-key
- AUTH_SERVICE_URL=http://auth-service:3001
- PRODUCT_SERVICE_URL=http://product-service:3002
- ORDER_SERVICE_URL=http://order-service:3003
depends_on:
- auth-service
- product-service
- order-service
networks:
- microservices-network
# Auth Service
auth-service:
build:
context: ./services/auth-service
dockerfile: Dockerfile
ports:
- "3001:3001"
environment:
- NODE_ENV=development
- DATABASE_URL=postgresql://user:password@postgres:5432/auth_db
- JWT_SECRET=your-secret-key
depends_on:
- postgres
- redis
networks:
- microservices-network
# Product Service
product-service:
build:
context: ./services/product-service
dockerfile: Dockerfile
ports:
- "3002:3002"
environment:
- NODE_ENV=development
- DATABASE_URL=postgresql://user:password@postgres:5432/product_db
depends_on:
- postgres
networks:
- microservices-network
# Order Service
order-service:
build:
context: ./services/order-service
dockerfile: Dockerfile
ports:
- "3003:3003"
environment:
- NODE_ENV=development
- DATABASE_URL=postgresql://user:password@postgres:5432/order_db
- RABBITMQ_URL=amqp://rabbitmq:5672
depends_on:
- postgres
- rabbitmq
networks:
- microservices-network
# PostgreSQL
postgres:
image: postgres:15-alpine
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=password
volumes:
- postgres-data:/var/lib/postgresql/data
- ./init-databases.sql:/docker-entrypoint-initdb.d/init.sql
networks:
- microservices-network
# Redis (para caching)
redis:
image: redis:7-alpine
ports:
- "6379:6379"
networks:
- microservices-network
# RabbitMQ (para messaging)
rabbitmq:
image: rabbitmq:3-management-alpine
ports:
- "5672:5672"
- "15672:15672" # Management UI
environment:
- RABBITMQ_DEFAULT_USER=admin
- RABBITMQ_DEFAULT_PASS=admin
networks:
- microservices-network
volumes:
postgres-data:
networks:
microservices-network:
driver: bridge
-- init-databases.sql
CREATE DATABASE auth_db;
CREATE DATABASE product_db;
CREATE DATABASE order_db;
Comandos:
# Levantar todos los servicios
docker-compose up -d
# Ver logs
docker-compose logs -f product-service
# Escalar servicio
docker-compose up -d --scale product-service=3
# Detener todo
docker-compose down
# Rebuild
docker-compose up -d --build
Observabilidad
Logging Centralizado
// shared/logger.js
import winston from 'winston';
import ecsFormat from '@elastic/ecs-winston-format';
const logger = winston.createLogger({
format: ecsFormat({ convertReqRes: true }),
transports: [
new winston.transports.Console(),
new winston.transports.File({
filename: 'logs/error.log',
level: 'error'
}),
new winston.transports.File({
filename: 'logs/combined.log'
})
]
});
// Middleware para request logging
export const requestLogger = (req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
logger.info('HTTP Request', {
method: req.method,
url: req.url,
status: res.statusCode,
duration,
userAgent: req.get('user-agent'),
ip: req.ip,
userId: req.user?.id
});
});
next();
};
export default logger;
Distributed Tracing con OpenTelemetry
// shared/tracing.js
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
import { registerInstrumentations } from '@opentelemetry/instrumentation';
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express';
import { JaegerExporter } from '@opentelemetry/exporter-jaeger';
import { Resource } from '@opentelemetry/resources';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
export function initTracing(serviceName) {
const provider = new NodeTracerProvider({
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: serviceName,
}),
});
const exporter = new JaegerExporter({
endpoint: process.env.JAEGER_ENDPOINT || 'http://localhost:14268/api/traces',
});
provider.addSpanProcessor(new SimpleSpanProcessor(exporter));
provider.register();
registerInstrumentations({
instrumentations: [
new HttpInstrumentation(),
new ExpressInstrumentation(),
],
});
console.log(`Tracing initialized for ${serviceName}`);
}
// En cada servicio
import { initTracing } from './shared/tracing.js';
initTracing('product-service');
Métricas con Prometheus
// shared/metrics.js
import client from 'prom-client';
const register = new client.Registry();
// Métricas default (memoria, CPU, etc.)
client.collectDefaultMetrics({ register });
// Métricas custom
export const httpRequestDuration = new client.Histogram({
name: 'http_request_duration_seconds',
help: 'Duration of HTTP requests in seconds',
labelNames: ['method', 'route', 'status_code'],
buckets: [0.1, 0.5, 1, 2, 5]
});
export const httpRequestTotal = new client.Counter({
name: 'http_requests_total',
help: 'Total number of HTTP requests',
labelNames: ['method', 'route', 'status_code']
});
export const activeConnections = new client.Gauge({
name: 'active_connections',
help: 'Number of active connections'
});
register.registerMetric(httpRequestDuration);
register.registerMetric(httpRequestTotal);
register.registerMetric(activeConnections);
// Middleware
export const metricsMiddleware = (req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = (Date.now() - start) / 1000;
httpRequestDuration.observe(
{
method: req.method,
route: req.route?.path || req.path,
status_code: res.statusCode
},
duration
);
httpRequestTotal.inc({
method: req.method,
route: req.route?.path || req.path,
status_code: res.statusCode
});
});
next();
};
// Endpoint de métricas
export async function metricsEndpoint(req, res) {
res.set('Content-Type', register.contentType);
res.end(await register.metrics());
}
Deployment en Kubernetes
Deployment y Service
# k8s/product-service-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: product-service
labels:
app: product-service
spec:
replicas: 3
selector:
matchLabels:
app: product-service
template:
metadata:
labels:
app: product-service
spec:
containers:
- name: product-service
image: myregistry/product-service:latest
ports:
- containerPort: 3000
env:
- name: NODE_ENV
value: "production"
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: product-service-secrets
key: database-url
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 3000
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: product-service
spec:
selector:
app: product-service
ports:
- protocol: TCP
port: 80
targetPort: 3000
type: ClusterIP
Horizontal Pod Autoscaler
# k8s/product-service-hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: product-service-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: product-service
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
Mejores Prácticas
1. Database per Service
❌ Compartir base de datos
Service A ──┐
└──> Shared DB
Service B ──┘
✅ Base de datos por servicio
Service A ──> DB A
Service B ──> DB B
2. Circuit Breaker Pattern
import CircuitBreaker from 'opossum';
const options = {
timeout: 3000,
errorThresholdPercentage: 50,
resetTimeout: 30000
};
const breaker = new CircuitBreaker(async (productId) => {
return await axios.get(`http://product-service/products/${productId}`);
}, options);
breaker.fallback(() => ({
id: null,
name: 'Producto no disponible',
cached: true
}));
breaker.on('open', () => {
logger.warn('Circuit breaker opened for product-service');
});
// Uso
try {
const product = await breaker.fire(productId);
} catch (error) {
// Fallback already applied
}
3. API Versioning
// v1
app.use('/api/v1/products', productsV1Router);
// v2 con cambios breaking
app.use('/api/v2/products', productsV2Router);
// Deprecation headers
app.use('/api/v1/*', (req, res, next) => {
res.setHeader('X-API-Deprecated', 'true');
res.setHeader('X-API-Sunset', '2025-12-31');
next();
});
4. Graceful Shutdown
const server = app.listen(3000);
let isShuttingDown = false;
const gracefulShutdown = async () => {
if (isShuttingDown) return;
isShuttingDown = true;
console.log('Received shutdown signal, closing server gracefully...');
// Dejar de aceptar nuevas conexiones
server.close(async () => {
console.log('HTTP server closed');
try {
// Cerrar conexiones a bases de datos
await db.close();
console.log('Database connections closed');
// Cerrar conexiones a message queues
await rabbitmq.close();
console.log('RabbitMQ connection closed');
process.exit(0);
} catch (error) {
console.error('Error during shutdown:', error);
process.exit(1);
}
});
// Force shutdown después de 30 segundos
setTimeout(() => {
console.error('Forced shutdown after timeout');
process.exit(1);
}, 30000);
};
process.on('SIGTERM', gracefulShutdown);
process.on('SIGINT', gracefulShutdown);
Próximos Pasos
Esta Semana
- Identifica servicios en un proyecto monolítico existente
- Implementa un API Gateway simple con Express
- Containeriza un servicio con Docker
- Configura RabbitMQ para mensajería básica
Próximos 30 Días
- Implementa pattern Saga para transacciones distribuidas
- Agrega observabilidad (logging, tracing, métricas)
- Configura CI/CD para deployment automatizado
- Implementa service mesh (Istio/Linkerd)
Recursos Adicionales
- Libro: "Building Microservices" - Sam Newman
- Curso: "Microservices with Node.js and React" en Udemy
- Patterns: microservices.io/patterns
- Tools: Kubernetes, Istio, Consul, Jaeger
Continúa Aprendiendo
- Infraestructura: Aprende Kubernetes en profundidad
- Complementa con: Event-Driven Architecture patterns
- Monitoreo: Implementa observability stack completo (ELK, Prometheus, Grafana)
Reflexión final: Microservicios añaden complejidad operacional significativa. Asegúrate de que tu organización tiene la madurez técnica y de procesos antes de adoptar esta arquitectura. No es una bala de plata - es un trade-off.