Inicio Arquitectura hexagonal Parte II Conectando una clean architecture con Nestjs
Entrada
Cancelar

Arquitectura hexagonal Parte II Conectando una clean architecture con Nestjs

logo

La segunda parte de esta serie de arquitectura hexagonal o (clean architecture) profundizaremos como configurar la interacción de un framework con el core de nuestra arquitectura, Nestjs nos ofrece un diseño adaptable para poder cubrir casos de uso avanzado. La idea es estar lo menos acoplados al framework y que este se adapte a nuestra capa core la cual contienen los casos de uso, las reglas de negocio y las entidades.

¿Qué es Nestjs? short answer

Nestjs es un framework progresivo de NodeJS desarrollado en TypeScript diseñado para facilitar el desarrollo de aplicaciones backend, aportando a los programadores una buena estructura y metodología inicial. Una de sus características es que puedes desarrollar cualquier tipo de proyecto backend, ya que las integraciones están ya disponibles en el framework y solo necesitaras instalar el módulo para poder trabajar, otro punto importante es que sigue la misma arquitectura que angular podemos desarrollar aplicaciones mediante anotaciones, módulos y podemos hacer uso de la inyección de dependencias para crear nuestras clases de servicios, Resumiendo Nestjs es como el spring de Nodejs.

Custom providers y módulos dinámicos en Nestjs

Custom Providers

Unos de los conceptos principales de nestsjs son los provider que no son nada más que clases de tipo service, objetos o valores que pueden ser inyectados en la aplicación la forma simple es mediante anotaciones, pero como nuestro proyecto necesita ser integrado al framework de una forma limpia emplearemos el uso de custom providers para lograr nuestro objetivo.

Módulos dinámicos

Los módulos básicamente son una pieza de software que agrupa una capa o una funcionalidad de la aplicación esta contienen controllers, providers y toda pieza de software relacionada con la idea del módulo. Los componentes creados bajo el contexto de Nestjs pueden estar aislados o ser exportadas a otros módulos. En Nestjs puedes crear módulos estáticos en los cuales definimos dependencias, providers y controllers y con esto podras reutilizarlo en otros módulos, pero en escenarios más complejos puedes definirlos de forma dinámica, esta característica nos permite crear fácilmente módulos personalizables que pueden registrar y configurar proveedores de forma dinámica. Justo lo que necesitamos para nuestra aplicación.

Manos a la obra integrando nuestra arquitectura hexagonal con Nestjs

La estructura de nuestra aplicación actualmente es esta, ya hay definidos unos archivos XXX.module.ts entonces partiremos por configurar el módulo core.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
├──  core
│   ├──  application
│   │   ├──  ProductApplication.ts
│   │   └──  services
│   │       ├──  ProductApplicationService.spec.ts
│   │       └──  ProductApplicationService.ts
│   ├──  core.module.ts
│   ├──  domain
│   │   ├──  entities
│   │   │   ├──  Category.ts
│   │   │   ├──  Product.ts
│   │   │   └──  Supplier.ts
│   │   ├──  ports
│   │   │   ├──  inbound
│   │   │   │   ├──  CategoryService.ts
│   │   │   │   ├──  ProductService.ts
│   │   │   │   └──  SupplierService.ts
│   │   │   └──  outbound
│   │   │       ├──  CategoryRepository.ts
│   │   │       ├──  ProductRepository.ts
│   │   │       └──  SupplierRepository.ts
│   │   └──  services
│   │       ├──  CategoryDomainService.spec.ts
│   │       ├──  CategoryDomainService.ts
│   │       ├──  ProductDomainService.spec.ts
│   │       ├──  ProductDomainService.ts
│   │       ├──  SupplierDomainService.spec.ts
│   │       └──  SupplierDomainService.ts
│   └──  shared
│       ├──  dto
│       │   └──  NewProductDTO.ts
│       └──  error
│           ├──  ProductApplicationError.ts
│           └──  ProductServiceError.ts
├──  infraestructure
│   ├──  adapters
│   │   ├──  category.repository.adapter.ts
│   │   ├──  product.repository.adapter.ts
│   │   └──  supplier.repository.adapter.ts
│   ├──  http-server
│   │   ├──  controllers
│   │   │   ├──  product.controller.ts
│   │   │   └──  root.controller.ts
│   │   ├──  exception-filters
│   │   │   └──  product-exception.filter.ts
│   │   ├──  http-server.module.ts
│   │   ├──  model
│   │   │   ├──  app.response.ts
│   │   │   └──  create-product.request.ts
│   │   └──  utils
│   │       └──  generate-swagger-docs.ts
│   ├──  infraestructure.module.ts
│   ├──  northwind-database
│   │   ├──  entities
│   │   │   ├──  category.entity.ts
│   │   │   ├──  product.entity.ts
│   │   │   └──  supplier.entity.ts
│   │   └──  northwind-database.module.ts
│   └──  shared
│       ├──  Log.ts
│       └──  shared.module.ts

Para crear un módulo dinámico debemos crear un método estático que devolverá un objeto DynamicModule del paquete @Nest/common que básicamente es lo mismo que usar la anotación @Module la cual define las propiedades del módulo la única diferencia es que DynamicModule el parámetro module es obligatorio y hace referencia al mismo módulo. El nombre del módulo register() no es mandatorio por convención los nombres utilizados son forRoot(), forAsyncRoot(), register() y asyncRegister() este método puede ser tanto asíncrono como síncrono.

1
2
3
4
5
6
7
8
9
10
11
import { DynamicModule, Module, Type } from '@Nestjs/common';

@Module({})
export class CoreModule {

  static register(): DynamicModule {
    return // my awesome module config
  }

}

El siguiente paso será definir que parámetros recibirá nuestro método, ya que acá es donde la definiremos la configuración de nuestro módulo. entonces creamos la siguiente interface

1
2
3
4
5
6
7
8
export type CoreModuleOptions = {
  modules: Type[];
  adapters: {
    productService: Type<ProductService>;
    categoryService: Type<CategoryService>;
    supplierService: Type<SupplierService>;
  }
}

La propiedad modules recibirá los módulos dependencias que contendrán los servicios que necesitemos inyectar en nuestro módulo core. Esta interface mediante su porpiedad adapters recibirá las implementaciones de las interfaces ProductService, CategoryService y SupplierService las cuales definimos como puertos en nuestra capa core.

Nuestro módulo va tomando esta forma.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { DynamicModule, Module, Type } from '@Nestjs/common';

@Module({})
export class CoreModule {

  static register(options: CoreModuleOptions): DynamicModule {
    
    const { adapters, modules } = options
    const { productService, categoryService, supplierService } = adapters

    return {
      module: CoreModule,
      providers: []
    }

  }

}

Muy bien el próximo paso es definir los providers en este caso necesitamos primero definir los token que son nada más que strings únicos que serán utilizados para identificar que provider queremos inyectar

definimos los siguientes providers:

1
2
3
4
5
6
// Application service reference
export const PRODUCT_APPLICATION = 'PRODUCT_APPLICATION'
// domain services references
export const CATEGORY_SERVICE = 'CATEGORY_SERVICE'
export const PRODUCT_SERVICE = 'PRODUCT_SERVICE'
export const SUPPLIER_SERVICE = 'SUPPLIER_SERVICE'

Ahora crearemos una objeto que definirá nuestro custom provider este deberá tener las siguientes propiedades:

  • provide: es la clase del provider o un string con el nombre de un servicio el cual Nestjs tendrá que inyectar en donde se solicite su uso, en nuestro caso no vamos a inyectar una clase sino una interfaz, por lo tanto, debemos definir un nombre, ya que en Nestjs para inyectar un valor que no es una clase se necesita definir un Injection token que nada más que un string identificatorio
  • useFactrory: es una función que recibe argumentos y devuelve el objeto que queremos definir como un custom provider los argumentos que recibirá esta función en este caso son los servicios que queremos que Nestjs inyecte a nuestro ProductApplicationService (casos de uso de la entidad productos)y para poder inyectar los servicios que necesita se hace mediante la propiedad inject
  • inject: es un array de proveedores existentes en el módulo los cuales serán los parámetros de la función useFactory()

Entonces para crear un Custom Provider para un servicio de dominio será de la siguiente manera:

Como nuestro modelo lo indica para crear el servicio de dominio ProductService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export class ProductDomainService implements ProductService {

    constructor(private repository: ProductRepository) {}

    async save(product: Product): Promise<Product> {
        if (this.validateProductPrice(product)) {
            return this.repository.save(product)
        }
        throw new ProductServiceError('Product price cannot be negative or equal to zero')
    }

    validateProductPrice(product: Product): boolean {
        return product.unitPrice > 0
    }

}

necesitamos de un ProductRepository

1
constructor(private repository: ProductRepository) {}

Donde productRepository proviene del objeto CoreModuleOptions entonces creamos una función ProductServiceProvider donde injectaremos los servicios necesarios para crear nuestro ProductDomainService

1
2
3
4
5
6
7
8
9
10
11
const ProductServiceProvider = {
  provide: PRODUCT_SERVICE, // provider token
  useFactory(repository: ProductRepository) {
    // return a service instance 
    return new ProductDomainService(repository)
  },
  inject:[
    productRepository // get this value from CoreModuleOptions
  ]
}

La función useFactory es sencilla solo recibe los providers inyectados por Nestjs que necesitamos y con eso podemos instanciar un Servicio de forma manual pero adecuado a nuestras necesitades.

En este otro caso nosotros devolvemos la instancia de una implementacion de la interfaz ProductApplication

1
2
3
4
5
  function useFactory(product: ProductDomainService, category: CategoryDomainService, supplier: SupplierDomainService) {
    // ProductApplication implementation
    return new ProductApplicationService(product, category, supplier)
  }

La inyección de nuestro Servicio de applicación será de la siguiente manera con la diferencia que injectamos los token en vez de los adapters que recibiremos:

1
2
3
4
5
6
7
8
9
10
11
const ProductApplicationProvider = {
      provide: PRODUCT_APPLICATION,
      useFactory(product: ProductDomainService, category: CategoryDomainService, supplier: SupplierDomainService) {
        return new ProductApplicationService(product, category, supplier)
      },
      inject: [
        PRODUCT_SERVICE,
        CATEGORY_SERVICE,
        SUPPLIER_SERVICE
      ]
    }

Finalmente nuestro módulo core es el siguiente

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
/**
 * Options for core module 
 */
export type CoreModuleOptions = {
  modules: Type[];
  adapters?: {
    productRepository: Type<ProductRepository>;
    categoryRepository: Type<CategoryRepository>;
    supplierRepository: Type<SupplierRepository>;
  }
}

/**
 * Providers token for netsjs injection
 */
export const PRODUCT_APPLICATION = 'PRODUCT_APPLICATION'
export const CATEGORY_SERVICE = 'CATEGORY_SERVICE'
export const PRODUCT_SERVICE = 'PRODUCT_SERVICE'
export const SUPPLIER_SERVICE = 'SUPPLIER_SERVICE'


@Module({})
export class CoreModule {

  static register({ modules, adapters }: CoreModuleOptions): DynamicModule {

    const { categoryRepository, productRepository, supplierRepository } = adapters
    /**
     * ApplicationService Provider
     * */
    const ProductApplicationProvider = {
      provide: PRODUCT_APPLICATION,
      useFactory(product: ProductDomainService, category: CategoryDomainService, supplier: SupplierDomainService) {
        return new ProductApplicationService(product, category, supplier)
      },
      inject: [
        PRODUCT_SERVICE,
        CATEGORY_SERVICE,
        SUPPLIER_SERVICE
      ]
    }

    /**
     * DomainService Providers
     * */
    const CategoryServiceProvider = {
      provide: CATEGORY_SERVICE,
      useFactory(repository: CategoryRepository) {
        return new CategoryDomainService(repository)
      },
      inject:[
        categoryRepository
      ]
    }

    const SupplierServiceProvider = {
      provide: PRODUCT_SERVICE,
      useFactory(repository: ProductRepository) {
        return new ProductDomainService(repository)
      },
      inject:[
        productRepository
      ]
    }

    const ProductServiceProvider = {
      provide: SUPPLIER_SERVICE,
      useFactory(repository: SupplierRepository) {
        return new SupplierDomainService(repository)
      },
      inject:[
        supplierRepository
      ]
    }

    return {
      module: CoreModule,
      global: true,
      imports: [
        ...modules
      ],
      providers: [
        ProductApplicationProvider,
        CategoryServiceProvider,
        SupplierServiceProvider,
        ProductServiceProvider
      ],
      exports: [
        PRODUCT_APPLICATION
      ],
    }
  }

}

Ahora lo siguiente será registrar a core en la raiz de todos módulos en app.module llamando al método register().

1
2
3
4
5
6
7
8
9
10
CoreModule.register({
  modules: [
      InfraestructureModule
  ],
  adapters: {
    productRepository: ProductRepositoryAdapter,
    categoryRepository: CategoryRepositoryAdapter,
    supplierRepository: SupplierRepositoryAdapter
  }
}),

El módulo principal de la aplicación registrará a core y shared` de forma global es decir, estará disponible para todos los módulos que definamos en nuestra aplicación sin necesidad de hacer excesivos imports en nuestras definiciones de módulos.

Para lograr esto se hace mediante la propiedad global: true definiendo en el decorador @Module() o en el objeto DynamicModule.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Module({
  global: true,
  ...otherprops
})

@Module({})
export class CoreModule {

  static register(options: CoreModuleOptions): DynamicModule {
    // ...ommited 
    return {
      module: CoreModule,
      global: true,
      // other props omitted
  }

}

Por último registramos el módulo infraestructure para terminar de conectar todas las capas de nuestra aplicación. El resultado final es el siguiente en AppModule:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Module({
  imports: [
    InfraestructureModule,
    SharedModule,
    CoreModule.register({
      modules: [
        InfraestructureModule
      ],
      adapters: {
        productRepository: ProductRepositoryAdapter,
        categoryRepository: CategoryRepositoryAdapter,
        supplierRepository: SupplierRepositoryAdapter
      }
    }),
  ]
})
export class AppModule { }

Finalmente integramos nuestra arquitectura hexagonal con Nestjs de una manera totalmente desacoplada del framework. La versatilidad de los módulos dinámicos y custom providers nos permiten crear aplicaciones mantenibles y con lógicas desconectadas de frameworks o las librerías de moda. Este mismo enfoque de los módulos dinámicos es una buena práctica al momento de definir grupos de piezas de software que queremos que sean flexibles y configurables de una forma centralizada.

Github repository

Puedes ver los demás artículos de arquitectura hexagonal acá 😉

Conclusión

No soy de conclusiones estas siempre van por parte de ti y si intentas poner en práctica lo expuesto podrás tener un concepto más amplio del tema e incluso poder aplicarlo en otros escenarios.

meme

Esta entrada está licenciada bajo CC BY 4.0 por el autor.