Qué son y cómo evitar dependencias circulares
Las dependencias circulares ocurren cuando dos o más servicios se referencian mutuamente, creando un bucle en el sistema de inyección de dependencias. Este problema surge cuando el servicio A necesita el servicio B, y simultáneamente el servicio B necesita el servicio A, generando una situación donde ninguno puede ser instanciado correctamente.
En NestJS, este escenario provoca errores durante el arranque de la aplicación, ya que el contenedor de inyección de dependencias no puede resolver qué servicio crear primero. El framework detecta automáticamente estas situaciones y lanza excepciones específicas para alertar sobre el problema.
Identificación de dependencias circulares
Cuando existe una dependencia circular, NestJS muestra un error similar a este:
Error: Nest cannot create the UserService instance.
The module key "UserService" is depending on itself.
Please make sure that each side of a bidirectional relationships are using "forwardRef()".
Este mensaje indica claramente que existe un problema de referencia circular entre servicios. Consideremos un ejemplo típico donde esto puede ocurrir:
// user.service.ts
@Injectable()
export class UserService {
constructor(private orderService: OrderService) {}
getUserOrders(userId: number) {
return this.orderService.getOrdersByUser(userId);
}
}
// order.service.ts
@Injectable()
export class OrderService {
constructor(private userService: UserService) {}
getOrdersByUser(userId: number) {
const user = this.userService.findById(userId);
// Lógica para obtener pedidos del usuario
}
}
En este caso, UserService
depende de OrderService
, mientras que OrderService
depende de UserService
, creando el bucle circular que impide la correcta inicialización.
Solución con forwardRef()
NestJS proporciona la función forwardRef()
para resolver dependencias circulares. Esta función permite diferir la resolución de la dependencia hasta que ambos servicios estén disponibles:
// user.service.ts
import { Injectable, Inject, forwardRef } from '@nestjs/common';
@Injectable()
export class UserService {
constructor(
@Inject(forwardRef(() => OrderService))
private orderService: OrderService
) {}
findById(userId: number) {
// Lógica para encontrar usuario
return { id: userId, name: 'Usuario ejemplo' };
}
getUserOrders(userId: number) {
return this.orderService.getOrdersByUser(userId);
}
}
// order.service.ts
import { Injectable, Inject, forwardRef } from '@nestjs/common';
@Injectable()
export class OrderService {
constructor(
@Inject(forwardRef(() => UserService))
private userService: UserService
) {}
getOrdersByUser(userId: number) {
const user = this.userService.findById(userId);
return [
{ id: 1, userId: user.id, product: 'Producto A' },
{ id: 2, userId: user.id, product: 'Producto B' }
];
}
}
El decorador @Inject()
combinado con forwardRef()
instruye a NestJS para resolver la dependencia de forma diferida, permitiendo que ambos servicios se inicialicen correctamente.
Configuración en módulos
Cuando utilizas forwardRef()
en servicios, también debes aplicarlo en la configuración del módulo si los servicios pertenecen a módulos diferentes:
// user.module.ts
import { Module, forwardRef } from '@nestjs/common';
import { UserService } from './user.service';
import { OrderModule } from '../order/order.module';
@Module({
imports: [forwardRef(() => OrderModule)],
providers: [UserService],
exports: [UserService]
})
export class UserModule {}
// order.module.ts
import { Module, forwardRef } from '@nestjs/common';
import { OrderService } from './order.service';
import { UserModule } from '../user/user.module';
@Module({
imports: [forwardRef(() => UserModule)],
providers: [OrderService],
exports: [OrderService]
})
export class OrderModule {}
Esta configuración asegura que los módulos se importen correctamente sin generar dependencias circulares a nivel de módulo.
Alternativas de diseño
Aunque forwardRef()
resuelve el problema técnico, es recomendable reevaluar el diseño de la aplicación para evitar dependencias circulares desde el origen. Algunas estrategias incluyen:
Extracción de lógica común:
// shared.service.ts
@Injectable()
export class SharedService {
validateUser(userId: number): boolean {
// Lógica de validación compartida
return userId > 0;
}
formatOrderData(orders: any[]): any[] {
// Lógica de formateo compartida
return orders.map(order => ({
...order,
formatted: true
}));
}
}
// user.service.ts
@Injectable()
export class UserService {
constructor(private sharedService: SharedService) {}
findById(userId: number) {
if (!this.sharedService.validateUser(userId)) {
throw new Error('Usuario inválido');
}
return { id: userId, name: 'Usuario ejemplo' };
}
}
Uso de eventos para desacoplar servicios:
// user.service.ts
import { Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter';
@Injectable()
export class UserService {
constructor(private eventEmitter: EventEmitter2) {}
createUser(userData: any) {
const user = { id: 1, ...userData };
// Emitir evento en lugar de llamar directamente al servicio
this.eventEmitter.emit('user.created', user);
return user;
}
}
// order.service.ts
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
@Injectable()
export class OrderService {
@OnEvent('user.created')
handleUserCreated(user: any) {
// Crear pedido inicial para el nuevo usuario
console.log(`Creando pedido inicial para usuario ${user.id}`);
}
}
Esta aproximación elimina la dependencia directa entre servicios, utilizando el patrón de eventos para mantener la funcionalidad sin crear referencias circulares.
Mejores prácticas
Para prevenir dependencias circulares en el futuro, considera estas recomendaciones de diseño:
- Principio de responsabilidad única: Cada servicio debe tener una responsabilidad específica y bien definida
- Inyección de interfaces: Utiliza interfaces para definir contratos entre servicios, facilitando el testing y reduciendo el acoplamiento
- Separación de capas: Mantén una arquitectura clara donde las capas superiores dependan de las inferiores, nunca al revés
- Revisión de dependencias: Evalúa regularmente las dependencias entre servicios para identificar posibles mejoras en el diseño
Implementar estas prácticas desde el inicio del desarrollo ayuda a mantener una arquitectura limpia y evita la necesidad de utilizar forwardRef()
como solución a problemas de diseño.
Fuentes y referencias
Documentación oficial y recursos externos para profundizar en Nest
Documentación oficial de Nest
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Nest es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de Nest
Explora más contenido relacionado con Nest y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
- Comprender qué son las dependencias circulares y por qué generan problemas en NestJS.
- Identificar errores comunes causados por dependencias circulares.
- Aprender a utilizar forwardRef() para resolver dependencias circulares en servicios y módulos.
- Conocer alternativas de diseño para evitar dependencias circulares, como la extracción de lógica común y el uso de eventos.
- Aplicar mejores prácticas de diseño para prevenir dependencias circulares en aplicaciones NestJS.