NestJS / Basic: modules and request contracts / Lesson 4
Module visibility, exports and composition
Make capability ownership explicit and share providers through deliberate module boundaries.
On this page
Lesson goals
- Explain imports, providers, controllers and exports as different metadata roles.
- Diagnose visibility failures without creating duplicate resources.
A module defines a public capability surface
Nest modules encapsulate provider registration by default. A provider used inside one module is not automatically injectable everywhere in the application. Exports define capabilities other modules can consume, and imports bring those exported capabilities into a consumer’s graph. Keep controllers near the use cases they adapt, but avoid exporting every internal helper as a public dependency. This boundary supports independent review and replacement. A folder name alone does not enforce ownership; the metadata and actual imports must reflect the intended architecture rather than silently bypassing it with globals.
Export the owner’s service instead of redeclaring it
The fragment exports CatalogService from CatalogModule and imports that module into ReportingModule. ReportsService can then depend on the exported capability under the normal container rules. Adding CatalogService separately to ReportingModule.providers would describe another registration and can create an unintended instance with different dependencies. Share the module contract instead. The snippet assumes class and decorator definitions and is not a compiled application. A real source pool owned behind CatalogService should have one deliberate lifecycle rather than multiplying whenever a new consumer copies its provider declaration.
@Module({providers:[CatalogService],exports:[CatalogService]})
export class CatalogModule {}
@Module({imports:[CatalogModule],providers:[ReportsService]})
export class ReportingModule {}
ReportingModule consumes the exported CatalogService through its import. No constructor resolution, pool or module bootstrap was run.
Global modules change visibility, not domain ownership
Global registration can be appropriate for a small set of cross-cutting capabilities, but it should not be the default remedy for an unknown dependency. It hides a consumer’s requirements and makes reuse or testing depend on ambient application composition. Prefer explicit imports for domain modules and document exceptional global dependencies. Re-exporting another module is another deliberate composition choice; it does not mean every transitive implementation detail becomes a stable public API. Review which token a consumer actually needs and whether that token belongs to the exporting module’s supported surface.
Model the declared export boundary
The local set model represents a module’s public token names, not a Nest container. It accepts the advertised course-reader capability and rejects an internal helper name. The check makes the architectural intention reproducible, but real visibility also depends on imports, token identity and registration. A code review should compare this intent with actual module metadata rather than trusting a diagram. Do not expose internal write capabilities merely to make a reporting module compile; choose a narrow read contract so the consumer receives only the authority it needs.
const advertised=new Set(["COURSE_READER"]);
function visible(token) {return advertised.has(token);}
console.assert(visible("COURSE_READER"));
console.assert(!visible("INTERNAL_REVIEW_WRITER"));
Only the advertised token is visible in this teaching set. This does not inspect decorators or compile modules.
Treat cycles as a design signal before applying a workaround
A provider cycle and a module import cycle are different graphs, and either can complicate construction. Extract a shared policy or orchestration capability when two domains depend on each other’s internals. forwardRef can defer references in supported scenarios, but it does not improve a confused ownership model or guarantee a desired initialization order. Keep lifecycle work out of constructors when order matters. Test a small composed module with fakes to verify the actual graph when authorized; this course executes only the public-surface set and does not run container cycle handling.
Common errors and fixes
A consumer cannot inject an internal provider
- Why it happens
- The owner did not export the capability or the consumer did not import its module.
- How to fix it
- Repair the deliberate export/import relationship.
- Prevent it next time
- Compile the minimal consumer graph with a fake source.
Several consumers create several source clients
- Why it happens
- The provider was redeclared instead of consuming the owner’s export.
- How to fix it
- Share the owning module contract and review lifecycle.
- Prevent it next time
- Track provider ownership and instance expectations.
A global module hides missing prerequisites
- Why it happens
- Ambient visibility replaced explicit domain dependencies.
- How to fix it
- Use clear imports and document exceptional global tokens.
- Prevent it next time
- Test modules outside the full application shell.
Check your understanding
1. Why is copying CatalogService into every providers array risky?
Show worked solution
It can create independent registrations with separate state and resource lifetimes, while obscuring ownership. Import the module that exports the intended service. If separate instances are genuinely required, make that a deliberate named capability and test the distinction instead of inheriting it accidentally from repeated metadata.
2. Does forwardRef make a cyclic domain design well structured?
Show worked solution
No. It addresses reference timing in supported wiring scenarios, not business responsibility. Identify whether the cycle belongs to providers, modules or file imports and consider extracting shared policy. Verify actual container behavior separately, and do not rely on a workaround to establish constructor or initialization order.
Key takeaways
- Exports are a module’s supported capability surface.
- Visibility fixes should preserve ownership and resource lifetimes.