En los últimos años, he desarrollado numerosas aplicaciones utilizando NestJS, que han sido utilizadas por cientos, miles e incluso millones de clientes en toda Europa.
Estas aplicaciones fueron construidas en equipos de diferentes tamaños, incluyendo startups, scaleups y organizaciones corporativas.
Desde monolitos modulares hasta microservicios basados en eventos, GraphQL y REST, todos se utilizaron y desarrollaron con NestJS. Pero con cada aspecto bueno, también hay una parte mala e incluso fea.
En este artículo, voy a compartir mis pensamientos después de trabajar en NestJS durante este período. Mi objetivo es proporcionar a los desarrolladores, jefes técnicos y jefes de equipo las herramientas que necesitan para anticipar y abordar cualquier problema potencial que pueda surgir durante el uso de NestJS.
Lo bueno
He trabajado en equipos de diferentes tamaños, con diferentes opiniones sobre el código limpio y las definiciones de cuándo una aplicación está terminada.
Esto varía de una empresa a otra, de un equipo a otro e incluso de una persona a otra. Las opiniones de las personas en los equipos suelen ser subjetivas, y eso puede dificultar la formación de directrices de desarrollo y estilos de codificación. De hecho, todos los equipos siguen tarde o temprano las mismas fases de la curva de aprendizaje, como señala el diagrama.
Aquí es donde Nest realmente demuestra su valor al guiar a los equipos en ciertas direcciones y ofrece un patrón de diseño que ya hace el trabajo pesado por ti. Nest es muy crítico, y eso es bueno.
El valor de Nest para los equipos realmente se demuestra cuando se trata de la coherencia en y sobre los equipos y la coherencia en sus bases de código. Cuando uno utiliza el marco, puede por ejemplo saltar realmente en una aplicación GraphQL, sin necesidad de adquirir un profundo conocimiento acerca de los entresijos.
Nest hace esto posible ofreciendo ejemplos de código realmente bien documentados.
Dado que las empresas se mueven con rapidez, es una gran ventaja utilizar un marco de desarrollo que sea flexible y pueda moverse realmente con la dirección de la empresa.
Al utilizar Nest, su equipo puede centrarse en la entrega, en lugar de en los procesos de incorporación. Los nuevos desarrolladores pueden incorporarse fácilmente, e incluso los desarrolladores junior pueden empezar a contribuir desde el primer momento, gracias a la excelente documentación y a la próspera comunidad que hay detrás del framework.
Lo malo
Como todo lo bueno, también hay algo malo. Y para ser sincero, no siempre es el marco el responsable directo de ello, sino que realmente a menudo son los equipos o los individuos los que hacen un mal uso o malinterpretan los conceptos dentro de un marco. Aunque me gustaría señalar algunas áreas de Nest en las que los equipos con los que he trabajado han tenido problemas una y otra vez.
El problema de las dependencias circulares
Tarde o temprano cada proyecto NestJS se enfrentará al momento en que se introducen dependencias circulares. Nest explica este problema común y el paquete creado por la comunidad nestjs-spelunker
también identifica problemas similares (aunque se centra un poco más en el árbol de inyección de dependencias en general).
El problema de las dependencias circulares es bastante desagradable, y podría ralentizar a todo el equipo de desarrollo a largo plazo si no se resuelve adecuadamente. Afortunadamente, hace poco Trilon publicó un artículo sobre dependencias circulares, en el que un colaborador principal de Nest señala una herramienta, llamada Madge, para identificar las dependencias circulares en una fase temprana.
Registros tragados al iniciar la aplicación
Otro problema que ocurre a menudo junto con las dependencias circulares es que los registros se tragan cuando se produce un error al iniciar la aplicación. Esto hace que sea bastante difícil para los desarrolladores entender lo que realmente sucedió.
Un enfoque común para identificar el error que fue lanzado es deshabilitar los abortos en un error y volver a lanzar el mensaje de error.
const app = await NestFactory.create(AppModule, express, {
bufferLogs: true,
abortOnError: false, // create a nest module and rethrow an error, instead of aborting the startup
}).catch(console.error) as INestApplication
Esto registrará el error real en la consola.
Lo feo
Unit testing
En Nest las pruebas unitarias se integran realmente con el propio framework. La diferencia entre una unidad y un equipo de integración varía de un equipo a otro e incluso de una persona a otra. En Nest, probar la unidad más pequeña requiere bastante código repetitivo y conocimientos de distintas técnicas. Especialmente para los nuevos desarrolladores, escribir pruebas puede ser complicado, ya que les exige al menos algunos conocimientos sobre cómo Nest resuelve su árbol de inyección de dependencias.
Para probar una aplicación simple, realmente a menudo se tropezará con un archivo de prueba que se ve similar como:
import { Test } from '@nestjs/testing';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
describe('CatsController', () => {
let catsController: CatsController;
let catsService: CatsService;
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
controllers: [CatsController],
providers: [CatsService],
}).compile();
catsService = moduleRef.get<CatsService>(CatsService);
catsController = moduleRef.get<CatsController>(CatsController);
});
describe('findAll', () => {
it('should return an array of cats', async () => {
const result = ['test'];
jest.spyOn(catsService, 'findAll').mockImplementation(() => result);
expect(await catsController.findAll()).toBe(result);
});
});
});
En la mayoría de las aplicaciones del mundo real, sin embargo, habrá múltiples dependencias para un único proveedor y eso aumentará mucho la complejidad de una prueba unitaria. Después de un tiempo, esas pruebas pueden incluso convertirse en un cuello de botella, ya que los equipos se centran cada vez más en cómo escribir una prueba y su árbol de inyección de dependencias, en lugar de probar realmente la unidad en sí.
¿Podría hacerse de otra manera? Claro, he visto equipos que abordan la complejidad de las pruebas implementando la lógica en funciones separadas en lugar de implementarla en un método de clase.
La ventaja de este enfoque es que las pruebas se convierten en una obviedad, y los nuevos desarrolladores son más fáciles de incorporar, ya que todos saben JavaScript. Considere lo siguiente:
// cats.service.ts
import { Injectable, LoggerService } from '@nestjs/common';
import { DatabaseService } from './database.service';
import { findOne } from './cats.utils.ts'
@Injectable()
export class CatsService {
constructor(
private loggerService: LoggerService,
private dbService: DatabaseService
){}
findOne(id: string) {
return findOne(this.dbService, this.loggerService, id)
}
// more methods, with other dependencies
...
}
// cats.utils.ts
export const findOne = (dbService: DatabaseService, id: string) => dbService.findOne(id)
// cats.spec.ts
import { findOne } from './cats.utils.ts'
describe('Cats Utils', () => {
test('should find one cat', async () => {
const cat = { 'sound': 'meow' }
const databaseService = {
findOne: jest.fn().mockResolvedValue(cat)
}
expect(await findOne(databaseService, 'id')).toEqual(cat)
})
})
Aunque todo buen aspecto tiene sus inconvenientes, la aplicación de este enfoque puede abordar la complejidad de las pruebas unitarias, por un lado, pero conlleva sus propias contrapartidas, por otro.
La falta de controladores dinámicos
Nest viene con un potente concepto de proveedores. Estos proveedores pueden ser personalizados y pueden ser cualquier cosa. Tales proveedores se vuelven útiles una vez que un proyecto se vuelve más maduro. Aunque requiere que los desarrolladores adquieran bastante conocimiento de cómo Nest resuelve su árbol de inyección de dependencias.
De todos modos, lo que realmente falta es la contraparte de estos proveedores personalizados, que son los controladores personalizados. A pesar de que estos controladores, según el creador de Nest son completamente contrarios a las ideas de Nest, son realmente útiles cuando un proyecto crece.
Para superar las limitaciones de Nest aquí, uno podría implementar un enfoque de fábrica como se describe en este hilo de GitHub:
// dynamic.controller.ts
export const createDynamicController = ({ customPath }): Type<any> {
@Controller()
class MyController {
constructor(private service: MyService) { }
@Get([customPath])
async getSomething(@Res() res: Response) {
//...
}
}
return MyController
}
Se puede consumir la fábrica a través de (pero no sólo) Módulos Dinámicos:
@Module({})
export class MyDynamicModule {
static register(config): DynamicModule {
const { customPath } = config;
const MyController = createDynamicController({ customPath })
return {
module: MyDynamicModule,
providers: [
MyService
],
exports: [MyService],
controllers: [MyController]
};
}
}
Aunque con todo lo bueno hay un mal, y la aplicación de un enfoque de este tipo hace frente a los controladores dinámicos, por un lado, va a dejar los patrones de opinión Nest para los controladores en el otro.
Conclusión
Nest es un framework realmente maduro y bien documentado dentro del panorama Node. Aunque con cada bien, hay un mal y puede ser incluso feo. Así que cuando tomes la decisión de un framework como NestJS, asegúrate de identificar los posibles problemas a los que se enfrentará tu equipo.
La última pregunta que surge es si elegiría NestJS para mi próximo proyecto. Bueno, como siempre, ¡depende ;)!