nestjs/nest · critical · InvalidClassModuleException

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

Error message

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

Scope [${scope}]

What it means

The controller counterpart of the injectable-in-imports guard: a class bearing the @Controller() watermark in an `imports` array triggers InvalidClassModuleException — '"X" is decorated with @Controller() and cannot appear in the imports array of a module. Please move X to the controllers array of the importing module instead.' Controllers are never importable DI units; only modules are.

Source

Thrown at packages/core/scanner.ts:206

    | {
        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);
  }

  public async scanModulesForDependencies(
    modules: Map<string, Module> = this.container.getModules(),
  ) {
    for (const [token, { metatype }] of modules) {
      await this.reflectImports(metatype, token, metatype.name);
      this.reflectProviders(metatype, token);

View on GitHub (pinned to dd75d7bd8c)

Solutions

  1. Move the class from `imports` to the `controllers` array of the same module.
  2. To use a controller in another module, re-declare it in that module's `controllers` (controllers are scoped to their module) or move it to a shared module that both import.
  3. If shared behavior (not routes) is the goal, extract it into an @Injectable() service and export that instead.

Example fix

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

// after
@Module({
  controllers: [OrdersController],
  providers: [OrdersService],
})
export class OrdersModule {}
Defensive patterns

Strategy: type-guard

Validate before calling

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

function assertNoControllersInImports(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(CONTROLLER_WATERMARK, target)) {
        throw new Error(`${m.name}: @Controller() ${target.name} belongs in controllers, not imports`);
      }
    }
  }
}

Type guard

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

Prevention

When it happens

Trigger: Listing a controller class in `imports` instead of `controllers`; trying to 'share' a controller between modules by importing it (the correct pattern is re-declaring it in each module's controllers or restructuring); paste errors between the arrays.

Common situations: Wanting one controller to serve multiple modules; teams migrating from frameworks where route handlers are registered globally; array mix-ups during module splits.

Related errors


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