nest
nest copied to clipboard
Dynamic modules with forwardRef in imports doesn't allow app to start
Is there an existing issue for this?
- [X] I have searched the existing issues
Current behavior
When the application graph includes a dynamic module and that dynamic module has a forwardRef in its list of imports, TypeError: metatype is not a constructor
is thrown from the InstanceLoader during app startup.
Minimum reproduction code
https://github.com/moeroach94/dynamic-module-imports
Steps to reproduce
- npm install
- nest start
- see error
Expected behavior
The app should start successfully.
Package
- [ ] I don't know. Or some 3rd-party package
- [ ]
@nestjs/common
- [X]
@nestjs/core
- [ ]
@nestjs/microservices
- [ ]
@nestjs/platform-express
- [ ]
@nestjs/platform-fastify
- [ ]
@nestjs/platform-socket.io
- [ ]
@nestjs/platform-ws
- [ ]
@nestjs/testing
- [ ]
@nestjs/websockets
- [ ] Other (see below)
Other package
No response
NestJS version
9.0.11
Packages versions
{
"@nestjs/common": "9.0.11",
"@nestjs/core": "9.0.11",
"@nestjs/platform-express": "9.0.11",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^6.6.3"
}
Node.js version
14.20.0
In which operating systems have you tested?
- [X] macOS
- [ ] Windows
- [ ] Linux
Other
This issue is similar to what raised an issue in my application code
for reference, this is the stacktrace:
TypeError: metatype is not a constructor
at Injector.instantiateClass (/tmp/dynamic-module-imports/node_modules/@nestjs/core/injector/injector.js:340:19)
at callback (/tmp/dynamic-module-imports/node_modules/@nestjs/core/injector/injector.js:53:45)
at Injector.resolveConstructorParams (/tmp/dynamic-module-imports/node_modules/@nestjs/core/injector/injector.js:132:24)
at Injector.loadInstance (/tmp/dynamic-module-imports/node_modules/@nestjs/core/injector/injector.js:57:13)
at Injector.loadProvider (/tmp/dynamic-module-imports/node_modules/@nestjs/core/injector/injector.js:84:9)
at async Promise.all (index 0)
at InstanceLoader.createInstancesOfProviders (/tmp/dynamic-module-imports/node_modules/@nestjs/core/injector/instance-loader.js:47:9)
at /tmp/dynamic-module-imports/node_modules/@nestjs/core/injector/instance-loader.js:32:13
at async Promise.all (index 3)
at InstanceLoader.createInstances (/tmp/dynamic-module-imports/node_modules/@nestjs/core/injector/instance-loader.js:31:9)
Okay, so this works without the forwardRef
, and your reproduction shows that using a forwardRef
in the dynamic module, does indeed, cause this error. Can you explain your use case where you have a circular dependency on a dynamic module? I've not run into this use case before
So I've got a dynamic module that takes Sequelize entities and other modules as parameters and creates specialized service classes from them and wraps it all up with the necessary providers. I've got a case of SpecialServiceModuleA importing SpecialServiceModuleB which in turn needs to import SpecialServiceModuleA. Then this error Error: Nest cannot create the module instance. Often, this is because of a circular dependency between modules. Use forwardRef() to avoid it
is printed. Then changing SpecialServiceModuleA import list to have forwardRef(() => SepcialServiceModuleB)
results in the metatype error from above.
Here's some simplified snippets below. AccessGroupService and AccessRoleService are the service classes that rely on their counterpart service being injected into their constructors
export class LchemyQuerySequelizeModule {
static forFeature({
entities,
imports = [],
providers = [],
}: ForFeatureModuleOptions): DynamicModule {
const modelServicePairs: ModelServicePair[] = entities.map((entity) =>
({
model: entity,
service: LchemyQueryService(entity),
})
);
const sequelizeModule = SequelizeModule.forFeature(
modelServicePairs.map(({ model }) => model),
);
const serviceProviders: ClassProvider[] = modelServicePairs.map(
({ model, service }) => ({
provide: `${model.name}QueryService`,
useClass: service,
}),
);
return {
module: LchemyQuerySequelizeModule,
imports: [sequelizeModule, ...imports],
providers: [...serviceProviders, ...providers],
exports: [sequelizeModule, ...serviceProviders, ...providers],
};
}
}
const accessGroupLchemyQueryModule = LchemyQuerySequelizeModule.forFeature({
entities: [{ model: AccessGroup, service: AccessGroupService }],
imports: [
forwardRef(() => AccessRolesModule),
],
});
@Module({
imports: [accessGroupLchemyQueryModule],
controllers: [AccessGroupsController],
exports: [accessGroupLchemyQueryModule],
})
export class AccessGroupsModule {}
export const accessRoleLchemyQueryModule =
LchemyQuerySequelizeModule.forFeature({
entities: [
{ model: AccessRole, service: AccessRoleService },
],
imports: [
AccessGroupsModule,
],
});
@Module({
imports: [accessRoleLchemyQueryModule],
controllers: [AccessRolesController],
providers: [AccessRoleSerializerService],
exports: [accessRoleLchemyQueryModule],
})
export class AccessRolesModule {}
I have the same problem!
I am also experiencing the same issue! Are there any recommendations to fixing this? Are there alternatives to forwardRef
that can be used with a Dynamic Module
?
I am also experiencing the same issue! Are there any recommendations to fixing this? Are there alternatives to
forwardRef
that can be used with aDynamic Module
?
Me too! This is an important issue. Hopefully we can get some answers.
@moeroach94
I simplified the example with the 3 modules and the circular dependencies:
import { DynamicModule, forwardRef, Module } from '@nestjs/common';
export class LchemyQuerySequelizeModule {
static forFeature({ imports = [] }): DynamicModule {
return {
module: LchemyQuerySequelizeModule,
imports: [...imports],
};
}
}
const accessGroupLchemyQueryModule = LchemyQuerySequelizeModule.forFeature({
imports: [forwardRef(() => AccessRolesModule)],
});
@Module({
imports: [accessGroupLchemyQueryModule],
exports: [accessGroupLchemyQueryModule],
})
export class AccessGroupsModule {}
export const accessRoleLchemyQueryModule =
LchemyQuerySequelizeModule.forFeature({
imports: [AccessGroupsModule],
});
@Module({
imports: [accessRoleLchemyQueryModule],
exports: [accessRoleLchemyQueryModule],
})
export class AccessRolesModule {}
The forwardRef
should work but throw the error TypeError: metatype is not a constructor
But I found the module instances created with the Dynamic module can also be used with forwardRef
if I move the files and resolve the circular dependency of class, we can create the module instance in the botton of the file and use the forwardRef
Solved:
import { DynamicModule, forwardRef, Module } from '@nestjs/common';
export class LchemyQuerySequelizeModule {
static forFeature({ imports = [] }): DynamicModule {
return {
module: LchemyQuerySequelizeModule,
imports: [...imports],
};
}
}
@Module({
imports: [forwardRef(() => accessGroupLchemyQueryModule)],
})
export class AccessGroupsModule {}
export const accessRoleLchemyQueryModule =
LchemyQuerySequelizeModule.forFeature({
imports: [AccessGroupsModule],
});
@Module({
imports: [accessRoleLchemyQueryModule],
})
export class AccessRolesModule {}
const accessGroupLchemyQueryModule = LchemyQuerySequelizeModule.forFeature({
imports: [AccessRolesModule],
});
Nestjs resolve all dependencies correctly.
Let's track this here https://github.com/nestjs/nest/pull/11031