nestjs-doctorGitHub

Performance Rules

7 rules that detect performance anti-patterns and dead code.

RuleSeverityWhat it catches
no-sync-iowarningreadFileSync, writeFileSync, etc.
no-blocking-constructorwarningLoops/await in Injectable/Controller constructors
no-dynamic-requirewarningrequire() with non-literal argument
no-unused-providerswarningProvider never injected anywhere
no-request-scope-abusewarningScope.REQUEST on frequently used providers
no-unused-module-exportsinfoModule exports unused by importers
no-orphan-modulesinfoModule never imported by any other module

no-sync-io

Detects synchronous file system calls like readFileSync, writeFileSync, existsSync, etc.

Why: Sync I/O blocks the Node.js event loop. In a server context, this means no other requests can be processed while the file operation completes. Use async alternatives.

@Injectable()
export class ConfigService {
  loadConfig() {
    const data = readFileSync('config.json', 'utf-8');
    return JSON.parse(data);
  }
}

no-blocking-constructor

Detects loops or await expressions inside Injectable/Controller constructors.

Why: Constructors run during module initialization. Blocking operations (loops over large datasets, async calls) delay application startup and can cause timeouts.

@Injectable()
export class CacheService {
  constructor() {
    // Blocks startup
    for (let i = 0; i < 10000; i++) {
      this.cache.set(i, computeExpensiveValue(i));
    }
  }
}

no-dynamic-require

Detects require() calls with non-literal (dynamic) arguments.

Why: Dynamic requires prevent bundlers from performing static analysis on dependencies, can load unexpected modules, and pose a security risk if the argument originates from user input.

@Injectable()
export class PluginLoader {
  load(name: string) {
    return require(`./plugins/${name}`);
  }
}

no-unused-providers

Scope: Project

Detects @Injectable() providers that are never injected by any other provider or controller.

Why: Unused providers are dead code. They increase module initialization time, obscure the dependency graph, and should be removed.

// Never injected anywhere
@Injectable()
export class LegacyService {
  doOldStuff() { /* ... */ }
}

@Module({ providers: [LegacyService, UserService] })
export class UserModule {}

Note: This rule skips infrastructure classes (Guards, Interceptors, Filters, Middleware, Strategies) since they may be used declaratively via decorators rather than injected.


no-request-scope-abuse

Detects providers using Scope.REQUEST which creates a new instance per HTTP request.

Why: Request-scoped providers disable singleton optimization. Every provider in the dependency chain also becomes request-scoped, causing significant overhead on high-traffic endpoints.

@Injectable({ scope: Scope.REQUEST })
export class UserService {
  // New instance created for EVERY request
}

no-unused-module-exports

Scope: Project

Detects modules that export providers which are never imported by any other module.

Why: Unused exports suggest the module boundary is not being used as intended. Either the export is dead code or the consuming module is missing the import.

@Module({
  providers: [SharedService],
  exports: [SharedService],  // No other module imports SharedModule
})
export class SharedModule {}

no-orphan-modules

Scope: Project

Detects modules that are never imported by any other module.

Why: An orphan module (other than the root AppModule) is likely dead code or a module that was accidentally disconnected from the module tree.

// analytics.module.ts — never imported anywhere
@Module({
  providers: [AnalyticsService],
})
export class AnalyticsModule {}