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:

  1. Crear una tarea: Permite crear una tarea con un nombre y una descripción, con el estado establecido como 'nuevo'.
  2. Obtener una tarea: Recupera una tarea por su identificador.
  3. 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.
CPU
1 vCPU
MEMORIA
1 GB
ALMACENAMIENTO
10 GB
TRANSFERENCIA
1 TB
PRECIO
$ 4 mes
Para obtener el servidor GRATIS debes de escribir el cupon "LEIFER"

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.

Fuente