nestjs/nest · critical · InvalidClassModuleException

"${metatype.name}" is decorated with @Injectable() and canno

Error message

"${metatype.name}" is decorated with @Injectable() and cannot appear in the "imports" array of a module. Please move "${metatype.name}" to the "providers" array of the importing module instead.

Scope [${scope}]

What it means

Before registering an import the scanner checks watermarks: a class carrying the @Injectable() watermark is a provider, not a module, and putting it in `imports` throws InvalidClassModuleException — '"X" is decorated with @Injectable() and cannot appear in the imports array of a module. Please move X to the providers array of the importing module instead.' DI can only inject providers from modules, so the array would be meaningless anyway.

Source

Thrown at packages/core/scanner.ts:199

    return [moduleInstance].concat(registeredModuleRefs);
  }

  public async insertModule(
    moduleDefinition: any,
    scope: Type<unknown>[],
  ): Promise<
    | {
        moduleRef: Module;
        inserted: boolean;
      }
    | undefined
  > {
    const moduleToAdd = this.isForwardReference(moduleDefinition)
      ? moduleDefinition.forwardRef()
      : moduleDefinition;

    if (this.isInjectable(moduleToAdd)) {
      throw new InvalidClassModuleException(
        moduleDefinition,
        scope,
        'provider',
      );
    }
    if (this.isController(moduleToAdd)) {
      throw new InvalidClassModuleException(
        moduleDefinition,
        scope,
        'controller',
      );
    }
    if (this.isExceptionFilter(moduleToAdd)) {
      throw new InvalidClassModuleException(moduleDefinition, scope, 'filter');
    }

    return this.container.addModule(moduleToAdd, scope);
  }

View on GitHub (pinned to dd75d7bd8c)

Solutions

  1. Move the class from `imports` to the `providers` array of the same module.
  2. If the class depends on providers from other modules, keep it in `providers` and add the required modules to `imports`.
  3. Consume it elsewhere via export: `exports: [TheService]` on the owning module.

Example fix

// before
@Module({
  imports: [CatsService],   // wrong array
})
export class CatsModule {}

// after
@Module({
  providers: [CatsService],
  exports: [CatsService],
})
export class CatsModule {}
Defensive patterns

Strategy: type-guard

Validate before calling

// Wiring test: nothing in imports arrays may be @Injectable()
import { INJECTABLE_WATERMARK } from '@nestjs/common/constants';
import 'reflect-metadata';

function assertNoInjectablesInImports(modules: Function[]) {
  for (const m of modules) {
    for (const imported of Reflect.getMetadata('imports', m) ?? []) {
      const target = imported && 'forwardRef' in (imported as any) ? (imported as any).forwardRef() : imported;
      if (typeof target === 'function' && Reflect.getMetadata(INJECTABLE_WATERMARK, target)) {
        throw new Error(`${m.name}: @Injectable() ${target.name} belongs in providers, not imports`);
      }
    }
  }
}

Type guard

const isInjectableClass = (v: any): boolean =>
  typeof v === 'function' && !!Reflect.getMetadata('__injectable__', v);

Prevention

When it happens

Trigger: Adding a service class to `imports` instead of `providers` (the two arrays are easy to confuse for newcomers); moving entries between arrays during refactoring and dropping them in the wrong one; importing a class decorated with a custom decorator that internally applies @Injectable().

Common situations: First NestJS projects wiring services; copying module templates and pasting service names into the wrong array; code reviews missing array mix-ups because TypeScript accepts any constructor in both arrays without strict typing.

Related errors


AI-assisted analysis of nestjs/nest@dd75d7bd8c (2026-08-21). Data as JSON: /api/errors/e56204ce369e6b0c. Report an issue: GitHub.