En este artículo, voy a demostrar cómo construir un microservicio utilizando Node.js, utilizando un servicio web de gestión de tareas como ejemplo. Este microservicio proporcionará una API sencilla pero potente para crear, recuperar y actualizar tareas. Nos centraremos en hacer el microservicio escalable, robusto, fiable y de alto rendimiento utilizando componentes populares de Node.js.
Descripción general de la API
Nuestra API de gestión de tareas soportará las siguientes operaciones:
- Crear una tarea: Permite crear una tarea con un nombre y una descripción, con el estado establecido como 'nuevo'.
- Obtener una tarea: Recupera una tarea por su identificador.
- Actualizar una tarea: Actualiza el estado, el nombre y la descripción de la tarea.
Requisitos de la aplicación
- Creación de tareas: Las tareas nuevas deben crearse con el estado "nuevo".
- Transiciones de estado:
- 'nuevo' → 'activo'
- nueva" → "cancelada
- activa" → "completada
- activa" → "cancelada
- Evitación de condiciones de carrera: Garantiza actualizaciones coherentes sin condiciones de carrera.
Requisitos no funcionales
- Escalabilidad: Gestión eficaz de solicitudes crecientes.
- Elasticidad: Gestionar los picos de tráfico sin problemas.
- Rendimiento: Proporcionar respuestas rápidas para mejorar la experiencia del usuario.
- Resistencia: Ser tolerante a fallos y capaz de recuperarse con elegancia.
- Supervisión y observabilidad: Seguimiento del estado, registros y métricas.
- Comprobabilidad: Garantizar que el servicio sea fácil de probar.
- Sin estado: Almacenar el contexto del cliente en la base de datos, no en el servicio.
- Implantación: Fácil de desplegar y actualizar.
Stack tecnológica
Node.js
Se elige Node.js por su velocidad, gran comunidad y adecuación tanto para el desarrollo frontend como backend.
Base de datos
MongoDB: Una base de datos NoSQL orientada a documentos que ofrece:
- Almacenamiento sin esquemas: Las colecciones pueden albergar documentos con diferentes esquemas.
- Escalabilidad: Diseñada para escalado horizontal.
- Rendimiento: Optimizado para cargas de trabajo de lectura intensiva.
Estructura web
Express: Un framework de aplicaciones web Node.js mínimo y flexible.
Validación
Joi: Una potente librería de descripción de esquemas y validación de datos. Express-mongo-sanitize: Middleware para prevenir la inyección de operadores MongoDB.
Configuración
Dotenv: Un módulo para cargar variables de entorno desde un fichero .env
.
Análisis estático
ESLint: Una herramienta para identificar e informar sobre patrones encontrados en código ECMAScript/JavaScript, con un enfoque en la calidad del código.
Testing
Jest: Un marco de pruebas de JavaScript para garantizar la corrección del código mediante pruebas unitarias y de integración.
Registro
Winston: Una biblioteca de registro versátil con soporte para múltiples transportes.
Métricas
Prometheus Middleware: Para recopilar métricas estándar de aplicaciones web.
Stack de monitorización
- Prometheus: Conjunto de herramientas de supervisión y alerta.
- Promtail: Recopilador y envío de registros.
- Loki: Sistema de agregación de registros.
- Grafana: Plataforma de visualización y análisis.
Infraestructura local
Docker: Para crear un entorno local similar al de producción.
Docker Compose: Para definir y gestionar aplicaciones Docker multicontenedor.
Integración continua
Acciones de GitHub: Para CI/CD, asegurando que los nuevos commits no rompan la compilación.
Desarrollo de aplicaciones
Crearemos una aplicación Node.js usando el stack anterior, con un backend MongoDB.
Estructura del Proyecto
Aquí hay un ejemplo de la estructura de directorios:
.
├── src
│ ├── config
│ │ ├── config.js
│ │ ├── logger.js
│ ├── controllers
│ │ └── task.js
│ ├── middlewares
│ │ ├── validate.js
│ │ └── error.js
│ ├── models
│ │ └── task.js
│ ├── routes
│ │ ├── v1
│ │ │ └── task.js
│ ├── services
│ │ └── task.js
│ ├── utils
│ │ └── pick.js
│ ├── validations
│ │ └── task.js
│ ├── app.js
│ ├── index.js
├── tests
│ ├── integration
│ │ └── task.test.js
│ ├── unit
│ │ └── task.test.js
├── .env
├── .eslintrc.json
├── docker-compose.yml
├── Dockerfile
├── jest.config.js
└── package.json
Fragmentos de código de ejemplo
Modelo de tarea
Definir el esquema de tareas utilizando Mongoose:
const mongoose = require('mongoose');
const { Schema } = mongoose;
const TaskSchema = new Schema(
{
name: {
type: String,
required: true,
},
description: {
type: String,
required: false,
},
status: {
type: String,
enum: ['new', 'active', 'completed', 'cancelled'],
default: 'new',
},
createdAt: {
type: Date,
default: Date.now,
},
updatedAt: Date,
},
{ optimisticConcurrency: true }
);
module.exports = mongoose.model('task', TaskSchema);
Controlador de tareas
Implemente el controlador para gestionar las actualizaciones de tareas:
const updateTaskById = catchAsync(async (req, res) => {
const result = await taskService.updateTaskById(req.params.id, req.body);
if (result.error) {
switch (result.code) {
case taskService.errorCodes.AT_LEAST_ONE_UPDATE_REQUIRED_CODE:
res.status(400).json({ success: false, message: 'at least one update required' });
return;
case taskService.errorCodes.INVALID_STATUS_CODE:
res.status(400).json({ success: false, message: 'invalid status' });
return;
case taskService.errorCodes.INVALID_STATUS_TRANSITION_CODE:
res.status(404).json({ success: false, message: 'task not found' });
return;
case taskService.errorCodes.TASK_NOT_FOUND_CODE:
res.status(400).json({ success: false, message: result.error });
return;
case taskService.errorCodes.CONCURRENCY_ERROR_CODE:
res.status(500).json({ success: false, message: 'concurrency error' });
return;
default:
res.status(500).json({ success: false, message: 'internal server error' });
return;
}
}
res.status(200).json({
success: true,
task: toDto(result),
});
});
Servicio de tareas
Implementa la lógica del servicio para manejar las reglas de negocio y la persistencia de datos:
async function updateTaskById(id, { name, description, status }) {
if (!name && !description && !status) {
return { error: 'at least one update required', code: AT_LEAST_ONE_UPDATE_REQUIRED_CODE };
}
if (status && !(status in availableUpdates)) {
return { error: 'invalid status', code: INVALID_STATUS_CODE };
}
for (let retry = 0; retry < 3; retry += 1) {
const task = await Task.findById(id);
if (!task) {
return { error: 'task not found', code: TASK_NOT_FOUND_CODE };
}
if (status) {
const allowedStatuses = availableUpdates[task.status];
if (!allowedStatuses.includes(status)) {
return {
error: `cannot update from '${task.status}' to '${status}'`,
code: INVALID_STATUS_TRANSITION_CODE,
};
}
}
task.status = status ?? task.status;
task.name = name ?? task.name;
task.description = description ?? task.description;
task.updatedAt = Date.now();
try {
await task.save();
} catch (error) {
if (error.name === 'VersionError') {
continue;
}
}
return task;
}
return { error: 'concurrency error', code: CONCURRENCY_ERROR_CODE };
}
Routes
Registrar las rutas y aplicar el middleware de validación:
const { Router } = require('express');
const taskController = require('../../../controllers/task');
const taskValidation = require('../../../validation/task');
const validate = require('../../../middlewares/validate');
const router = Router();
router.get('/:id', validate(taskValidation.getTaskById), taskController.getTaskById);
router.put('/', validate(taskValidation.createTask), taskController.createTask);
router.post('/:id', validate(taskValidation.updateTaskById), taskController.updateTaskById);
module.exports = router;
Testing
Implemente pruebas de integración para verificar la funcionalidad de la API:
describe('Task API', () => {
setupServer();
it('should create and update a task', async () => {
let response = await fetch('/v1/tasks', {
method: 'put',
body: JSON.stringify({
name: 'Test Task',
description: 'Task description',
}),
headers: { 'Content-Type': 'application/json' },
});
expect(response.status).toEqual(201);
const result = await response.json();
expect(result.success).toBe(true);
const taskId = result.task.id;
response = await fetch(`/v1/tasks/${taskId}`, {
method: 'post',
body: JSON.stringify({ status: 'active' }),
headers: { 'Content-Type': 'application/json' },
});
expect(response.status).toEqual(200);
const updateResult = await response.json();
expect(updateResult.success).toBe(true);
expect(updateResult.task.status).toBe('active');
});
});
Despliegue
Despliega el microservicio utilizando Docker
y Docker Compose
. Aquí hay un ejemplo docker-compose.yml
:
version: '3.8'
services:
mongo:
image: mongo
container_name: mongodb
ports:
- '27017:27017'
volumes:
- mongo-data:/data/db
api:
build: .
container_name: task_api
ports:
- '3000:3000'
depends_on:
- mongo
environment:
- MONGO_URL=mongodb://mongo:27017/taskdb
volumes:
- .:/usr/src/app
volumes:
mongo-data:
Conclusión
Siguiendo esta guía, puedes crear un microservicio robusto, escalable y fiable para gestionar tareas. Esta arquitectura garantiza que el microservicio pueda gestionar actualizaciones simultáneas sin problemas, facilita la implementación y ofrece escalabilidad y supervisión sencillas.