Node: Arquitectura

Aprende a estructurar proyectos en Node.js adoptando patrones de arquitectura sólidos. Conoce enfoques como microservicios, capas lógicas, principios de diseño y cómo optimizar la escalabilidad y el mantenimiento de tu software.

Aprende Node GRATIS y certifícate

La arquitectura de un proyecto en Node.js influye de manera directa en su escalabilidad, mantenibilidad y rendimiento. Un diseño organizado facilita la incorporación de nuevas funcionalidades y la adaptación a entornos complejos, como microservicios o grandes equipos de desarrollo.

Características generales de Node.js

Node.js ofrece un modelo basado en event loop y callbacks (o promesas), lo cual puede simplificar ciertos aspectos de la arquitectura. Su naturaleza asíncrona y no bloqueante permite manejar grandes cantidades de peticiones simultáneas, siempre que se siga una estructura clara y organizada.

Patrones de arquitectura comunes en Node.js

  1. Arquitectura en capas: Se separan las responsabilidades de la aplicación en capas como la capa de controladores, la capa de servicios o lógica de negocio y la capa de datos. Cada capa se comunica únicamente con la siguiente, reduciendo el acoplamiento.
  2. Modelo de microservicios: Se divide la aplicación en múltiples servicios independientes, cada uno con su propio despliegue y base de datos. Este patrón facilita la escalabilidad horizontal y permite que equipos distintos desarrollen cada servicio por separado.
  3. Arquitectura hexagonal (o puertos y adaptadores): Enfatiza la independencia de la lógica de negocio respecto a la infraestructura. Las entradas (puertos) y salidas (adaptadores) están separados del núcleo, lo que facilita cambios tecnológicos sin afectar la esencia de la aplicación.
  4. Event-driven: Se diseñan sistemas basados en eventos, aprovechando la naturaleza asíncrona de Node.js. Esto permite la comunicación de componentes a través de emisores y receptores de eventos, favoreciendo la extensibilidad.

Organización básica de carpetas

En proyectos de Node.js, la distribución de archivos y directorios influye en la claridad del código. Un ejemplo de estructura:

├── src
│   ├── controllers
│   │   └── usuarioController.js
│   ├── services
│   │   └── usuarioService.js
│   ├── data
│   │   └── usuarioRepository.js
│   ├── routes
│   │   └── usuarioRoutes.js
│   ├── utils
│   │   └── helpers.js
│   └── app.js
├── test
│   └── usuario.test.js
├── package.json
└── README.md

En este modelo, la capa de controladores gestiona la entrada de la petición, la capa de servicios manipula la lógica de negocio y la capa de datos interactúa directamente con la base de datos.

Procesos y workers

Para aprovechar la capacidad de múltiples núcleos en el sistema operativo, Node.js ofrece el módulo cluster y la librería worker_threads. Estos componentes permiten:

  • Ejecutar varias instancias de la aplicación en paralelo.
  • Distribuir la carga de trabajo entre hilos distintos, logrando un mejor rendimiento en equipos con CPU multi-core.

Un ejemplo de uso de cluster:

const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork(); 
  }
} else {
  http.createServer((req, res) => {
    res.writeHead(200);
    res.end('Servidor activo en modo cluster');
  }).listen(3000);
}

Uso de config y entornos

Para mantener la aplicación modular y segura, es habitual separar la configuración en archivos o en variables de entorno:

  • Archivos de configuración con variables específicas para cada entorno (desarrollo, producción, pruebas).
  • Módulos como dotenv o soluciones más avanzadas que carguen estos valores en tiempo de ejecución.
  • Evitar hardcodear claves o passwords directamente en el código.

De este modo, la arquitectura se vuelve flexible para ser desplegada en distintos ambientes sin complicaciones.

Principios de diseño clave

  1. Single Responsibility Principle (SRP): Cada módulo o clase debe tener una única responsabilidad para simplificar su uso y reducir la complejidad.
  2. Open/Closed Principle (OCP): Las entidades de software deben estar abiertas a la extensión pero cerradas a la modificación, lo que promueve la extensibilidad con un mínimo de reescritura.
  3. Liskov Substitution Principle (LSP): Las clases derivadas deben poder reemplazar a sus clases base sin interrumpir la aplicación.
  4. Interface Segregation Principle (ISP): Dividir interfaces grandes en interfaces más específicas y pequeñas para evitar dependencias innecesarias.
  5. Dependency Inversion Principle (DIP): Los componentes de alto nivel no deberían depender de los de bajo nivel, sino de abstracciones que permitan intercambiar implementaciones.

Estos principios ayudan a mantener una arquitectura ordenada, sea en un enfoque monolítico o de microservicios.

Integración de testing y CI

Una arquitectura bien definida facilita la escritura de pruebas unitarias e integradas. Para proyectos con varias capas, cada capa puede probarse por separado, asegurando que los controladores gestionen correctamente los datos y que la capa de servicios aplique la lógica deseada.

En sistemas CI (Continuous Integration), se ejecutan estas pruebas de forma automática ante cada commit, garantizando la calidad del código y detectando errores de arquitectura o regresiones tempranamente.

Patrones de comunicación en microservicios

En entornos distribuidos, la comunicación entre servicios puede realizarse:

  • HTTP/REST: Directo y sencillo, pero puede incrementar la latencia si hay muchos servicios.
  • Mensajería asíncrona: Con sistemas como RabbitMQ o Kafka, se envían eventos y se procesan en segundo plano, mejorando la tolerancia a fallos y el desacoplamiento.
  • GraphQL: Centraliza las peticiones en un único endpoint, ofreciendo flexibilidad en la consulta de datos y mejorando la eficiencia de las respuestas.

Elegir el patrón adecuado depende de los requisitos de escalabilidad y la complejidad de la aplicación.

Monolito modular vs microservicios

  • Monolito modular: Agrupa toda la funcionalidad en una sola aplicación, pero organizada por módulos. Es más sencillo de desplegar, aunque puede crecer en complejidad rápidamente.
  • Microservicios: Se separan las funcionalidades en servicios independientes. Permite escalar solo los componentes que lo requieran, aunque complica la orquestación y la comunicación.

Cada enfoque tiene ventajas e inconvenientes, por lo que conviene valorar factores como el tamaño del equipo, la frecuencia de despliegues y la necesidad de escalado independiente.

Buenas prácticas

  • Aplicar la separación de responsabilidades para que cada capa o módulo tenga un objetivo concreto.
  • Definir interfaces claras de comunicación entre componentes, facilitando cambios tecnológicos en la capa de datos o en la de presentación.
  • Controlar los errores y manejar excepciones de forma centralizada, para no propagar fallos a todo el sistema.
  • Utilizar un sistema de logs y monitorización para investigar incidencias y analizar el comportamiento de la aplicación.
  • Emplear estándares de codificación y revisar periódicamente la arquitectura para ajustarse a las buenas prácticas del ecosistema.

La arquitectura en Node.js puede adaptarse a proyectos de diversa magnitud, desde aplicaciones monolíticas hasta grandes sistemas distribuidos. Siguiendo estos lineamientos de organización, principios de diseño y separación por capas o servicios, se consigue un software mantenible, escalable y confiable a largo plazo.

Empezar curso de Node

Lecciones de este módulo de Node

Lecciones de programación del módulo Arquitectura del curso de Node.