Siro Ramírez Senior Software Architect

Diseño de Clean Architecture en NodeJS

NodeJS es una de las tecnologías centrales que usamos en Izertis para dar vida a nuestras aplicaciones. Pero como en todas las tecnologías, la parte crítica no es la propia tecnología sino como la usamos para conseguir lo que necesitamos. Por lo tanto, en este post vamos a recopilar toda la experiencia que hemos ganado trabajando con NodeJS para definir una buena arquitectura, que sabemos que funciona y basada sobre todo en conceptos como ‘Clean Architecture‘ y ‘Arquitectura Hexagonal‘ para que podáis seguirla y diseñar vuestro backend de manera que se pueda adaptar a la mayoría de proyectos y escalar de manera apropiada.

No se trata de una guía definitiva para diseñar la aplicación perfecta. Posiblemente se puedan aplicar muchas mejoras, sobre todo en función de las necesidades de cada proyecto. Pero puedes aprovecharte de nuestro trabajo previo y varios años de testear esta arquitectura para ahorrarte problemas y dolores de cabeza cuando te encuentres en medio de un proyecto que ha evolucionado de manera imprevista.

Problemas ocultos en algunas arquitecturas NodeJS

Empecemos diseñando una arquitectura NodeJS por nuestra cuenta. Muchos de los conceptos pueden ser extrapolados a otras tecnologías, pero centrémonos en esta. Escogeremos Express como framework para el API (basado en REST) y MongoDB (con Mongoose) para la base de datos, ya que el stack MEAN se ha popularizado bastante en los últimos años. Sin embargo, muchos de los conceptos utilizados en esta sección serán transparentes al stack tecnológico utilizado

Podemos ver un ejemplo de servidor básico NodeJS en este link, el cual básicamente tiene un recurso que nos permite crear un ‘usuario’:

https://github.com/siro47/CornerCleanArchitectureNode/tree/v1 

Este servidor tiene algunos buenos principio detrás. Tiene capas separadas para gestionar las peticiones del API y las consultas de base de datos. Ficheros separados para módulos de los recursos REST, y también para módulos de bd (aunque para este ejemplo solo se ha añadido un módulo de gestión de usuarios). Parece que podría escalar sin ningún problema según aumente la cantidad y complejidad de los recursos, pero esto no es así del todo.

Supongamos que hay que añadir un nuevo recurso en nuestro server para crear un grupo. Un grupo está diseñado para contener usuarios. Pero nuestra lógica de negocio determina que un grupo debe crear un usuario por defecto que se encargará de gestionar dicho grupo. Los primeros pasos son sencillos de implementar, tal y como podemos ver aquí:

https://github.com/siro47/CornerCleanArchitectureNode/tree/v2

¿Pero como vamos a poder reutilizar nuestro código previo para la creación del usuario?

Importar el método de la capa ‘routes’ causa problemas, ya que vamos a necesitar pasar objetos de express (‘req’, ‘res’, …) de un sitio a otro de nuestra aplicación. Importar el método de la capa ‘db’ no es del todo preciso, ya que estamos perdiendo funcionalidad estratégicamente añadida en la capa de ‘routes’ (crear la variable completeName del usuario).

Además, vamos a considerar que queremos añadir test unitarios (porque efectivamente, queremos test unitarios en nuestro código) a nuestra funcionalidad actual. Los test de la capa ‘routes’, ¿no van a estar demasiado ligados a express? Si cambiásemos a, por ejemplo, un API en GraphQL, ¿tendríamos que reescribirlos de nuevo?

¿Cómo resolveríais estos problemas? ¿Dividiendo los métodos actuales? ¿Creando una capa nueva? ¿Muchas capas nuevas, quizás?

No reinventemos la rueda. Clean Architecture + Arquitectura Hexagonal

No vamos a descubrir una solución innovadora. Simplemente hemos evaluado los mejores modelos de arquitectura disponibles actualmente y los hemos adaptado a nuestras necesidades.

Comenzamos analizando uno de los patrones de arquitectura más conocidos, propuesto por Uncle Bob hace unos años y llamado Clean Architecture. Se basa en la idea de hacer el modelo independiente del framework, librerías, bds… (suena lógico, no?), creando una capa intermedia llamada ‘adaptadores de interfaz‘. Hace incapié también en la necesidad de que el código sea fácilmente testable, y como este tipo de arquitectura nos facilita la creación de test unitarios que no están ligados a ningún elemento tecnológico externo.

Adicionalmente, analizamos otro modelo de arquitectura bastante utilizado, la Arquitectura Hexagonal, propuesta por Alistair Cockburn en 2005. Hablan de diferentes formas geométricas, pero sus conceptos son muy similares. Encapsular la funcionalidad de tu aplicación para ser usada por cualquier elemento externo a través de un sistema de puertos y adaptadores.

Ergo siguiendo estos principios, separamos la lógica de negocio de nuestro framework de API, creando una nueva capa ‘domain’:

https://github.com/siro47/CornerCleanArchitectureNode/tree/v3 

Consiguiendo de esta manera:

  1. Nuestro código del dominio es reusable e independiente del framework del API
  2. El código es fácil de testear y los tests pueden perdurar a través de diferentes frameworks y cambios de librerías

Como podemos ver en la versión final del ejemplo, la funcionalidad central permanece invariable a pesar de que hemos añadido una API basada en GraphQL que utiliza los mismos métodos del dominio. No solo eso, sino que hemos creado test unitarios que aseguran el correcto funcionamiento de nuestro lógica de negocio de manera autónoma, usemos el framework de API que usemos.

Mejoras adicionales

Como hemos dicho previamente, esta es simplemente una arquitectura base sobre la que vosotros deberíais adaptar las necesidades de vuestro proyecto. Además, para evitar que este post se volviera demasiado extenso, no hemos entrado en detalle de ninguna de las siguientes mejoras, pero es deber del lector avanzado tener en cuenta también los siguientes puntos. (Y podéis preguntarnos por más información de cada uno en la sección de comentarios de debajo, si queréis)

  • Mejorar la definición de los adaptadores de API: Nuestro adaptador de API REST separa nuestra lógica de negocio de los recursos, peticiones, respuestas y demás cosas de REST. Pero una capa más completa debería incluir también mapeado de códigos http, mensages internacionalizados y todo lo necesario para sacar el máximo partido de nuestro protocolo de API.
  • Añadir adaptador a la capa de bd: De la misma manera que hemos dividido nuestra tecnología de API de nuestro código, deberíamos hacer lo mismo con la capa de base de datos, para hacer que nuestra aplicación sea totalmente independiente de la tecnología de bd que estemos usando. De manera análoga, deberíamos añadir un adaptar para cada uno de los frameworks externos que queramos aislar de nuestro código de dominio.
  • Definir entidades de dominio: Algunos modelos de Clean Architecture hablan sobre definir entidades núcleo en nuestro lógica de negocio para abstraerlas en una capa más interna. Eso depende de la aplicación, pero es algo que deberías tener en cuenta sobre todo si tienes entidades bien definidas que se usan en toda la lógica de tu aplicación.

¿Listo para enfrentarte al desafío de tener un servidor NodeJS escalable y listo para adaptarse a cada situación?

¿Hay algo más que piensas que es imprescindible en una buena arquitectura NodeJS?