nestjs-doctorGitHub

Architecture Rules

10 rules that enforce clean layering, dependency injection patterns, and module boundaries.

RuleSeverityWhat it catches
no-business-logic-in-controllerserrorLoops, branches, data transforms in HTTP handlers
no-repository-in-controllerserrorRepository injection in controllers
no-orm-in-controllerserrorPrismaService / EntityManager / DataSource in controllers
no-circular-module-depserrorCycles in @Module() import graph — names the providers to extract
no-manual-instantiationerrornew SomeService() for injectable classes
no-orm-in-serviceswarningServices using ORM directly (should use repositories)
no-service-locatorwarningModuleRef.get()/resolve() usage
prefer-constructor-injectionwarning@Inject() property injection
require-module-boundariesinfoDeep imports into other modules' internals
no-barrel-export-internalsinfoRe-exporting repositories from barrel files

no-business-logic-in-controllers

Detects loops, conditional branches, and data transformations inside HTTP handler methods.

Why: Controllers should delegate business logic to services — they receive a request, call the appropriate service method, and return the response. Business logic in controllers cannot be reused, is harder to test, and violates the single responsibility principle.

@Controller('orders')
export class OrderController {
  @Post()
  create(@Body() dto: CreateOrderDto) {
    const items = [];
    for (const item of dto.items) {  // Logic in controller
      if (item.quantity > 0) {
        items.push({ ...item, total: item.price * item.quantity });
      }
    }
    return this.orderRepo.save({ items });
  }
}

no-repository-in-controllers

Detects direct repository injection in controller constructors.

Why: Controllers should depend on services, not repositories. Direct repository access bypasses business logic validation and creates tight coupling to the data layer.

@Controller('users')
export class UserController {
  constructor(
    @InjectRepository(User)
    private readonly userRepo: Repository<User>,
  ) {}
}

no-orm-in-controllers

Detects ORM types (PrismaService, EntityManager, DataSource, etc.) injected directly in controllers.

Why: Applies the same layering constraint as no-repository-in-controllers, extended to other ORM access patterns. Controllers should access data through a service layer.

@Controller('users')
export class UserController {
  constructor(private readonly prisma: PrismaService) {}

  @Get()
  findAll() {
    return this.prisma.user.findMany();
  }
}

no-circular-module-deps

Scope: Project

Detects cycles in the @Module() import graph and names the specific providers causing each edge.

Why: Circular module dependencies produce unpredictable initialization ordering, complicate the dependency graph, and make the codebase harder to reason about.

// user.module.ts
@Module({ imports: [OrderModule] })
export class UserModule {}

// order.module.ts
@Module({ imports: [UserModule] })  // Circular!
export class OrderModule {}

Smart suggestions

Instead of a generic "use forwardRef" message, this rule traces provider-level dependencies to identify why each module edge exists. It then suggests which specific providers to extract into a shared module to break the cycle at the weakest edge.

Example output:

AuthModule → UserModule: AuthService (in AuthModule) injects UserService (from UserModule)
UserModule → OrderModule: UserNotifier (in UserModule) injects OrderService (from OrderModule)
Consider extracting AuthService into a shared module — it would break the weakest edge (1 dependency).

The rule uses traceProviderEdges to walk each module's providers and controllers, checking which constructor parameters resolve to providers from the target module. The edge with the fewest provider dependencies is flagged as the best candidate for extraction.


no-manual-instantiation

Detects new SomeService() for classes that should be injected via NestJS DI.

Why: Manually instantiating injectable classes bypasses the DI container, so constructor dependencies are not resolved. It also breaks scoping and lifecycle hooks.

@Injectable()
export class OrderService {
  processOrder() {
    const validator = new OrderValidator();  // Manual instantiation!
    validator.validate(order);
  }
}

no-orm-in-services

Detects services using ORM types directly instead of through a repository layer.

Why: When services depend on ORM types directly, switching ORMs or data sources requires changing every service. A repository layer provides a clean abstraction boundary.

@Injectable()
export class UserService {
  constructor(private readonly prisma: PrismaService) {}
}

no-service-locator

Detects ModuleRef.get() or ModuleRef.resolve() calls that dynamically look up providers.

Why: The service locator pattern hides dependencies — the constructor signature no longer reflects the true dependency set. Use constructor injection instead.

@Injectable()
export class TaskService {
  constructor(private readonly moduleRef: ModuleRef) {}

  async run() {
    const service = this.moduleRef.get(SomeService);
    service.doWork();
  }
}

prefer-constructor-injection

Detects @Inject() used on class properties instead of constructor parameters.

Why: Constructor injection makes dependencies explicit and immutable. Property injection obscures dependencies and permits them to be undefined before initialization.

@Injectable()
export class UserService {
  @Inject()
  private configService: ConfigService;
}

require-module-boundaries

Detects deep imports that reach into another module's internal files.

Why: Importing from ../other-module/services/internal.service creates tight coupling to another module's internal structure. Import from the module's public API (barrel file) instead.

// In user module:
import { OrderValidator } from '../order/validators/order.validator';

no-barrel-export-internals

Detects barrel files (index.ts) that re-export internal implementation details like repositories.

Why: Barrel files define a module's public API. Re-exporting repositories or internal services allows consumers to bypass the service layer.

// user/index.ts
export { UserService } from './user.service';
export { UserRepository } from './user.repository';  // Internal!