Course outline

NestJS / Basic: modules and request contracts / Lesson 3

Providers, tokens and constructor injection

Connect capabilities through stable runtime tokens and keep construction separate from domain use.

About 4 min readContent reviewed
On this page

Lesson goals

  • Distinguish a TypeScript interface from a runtime injection token.
  • Explain why manual construction can bypass container wiring.

Register a capability and inject its contract

A provider is a value or instance managed through Nest’s dependency system. A class decorated Injectable can participate in injection, but it still must be registered in a visible module. Constructor injection makes dependencies explicit and lets a test supply a controlled alternative. Avoid importing a configured global source inside every service, which couples domain behavior to startup and credentials. Choose a narrow capability such as reading eligible course cards rather than exposing an unrestricted database handle wherever convenient. The container can connect objects, but only the service contract can state the intended access and failure semantics.

Use a runtime token for an erased interface

TypeScript interfaces disappear at runtime, so the container cannot use an interface name as a runtime identity. The fragment uses an exported symbol token and an explicit Inject decorator. Every registering and consuming module must import the same token object; creating another Symbol with the same description produces a different identity. The implementation is assumed and not constructed here. This token pattern supports substituting a fake without changing the service’s domain logic. Keep tokens in a dependency-light file to avoid circular imports that turn a straightforward registration into an initialization problem.

typescript Illustrative interface-token binding
export const COURSE_READER = Symbol("COURSE_READER");
@Injectable()
export class CatalogService {
  constructor(@Inject(COURSE_READER) private readonly reader: CourseReader) {}
}
// Register in an owning module:
// { provide: COURSE_READER, useClass: StoredCourseReader }

CourseReader is a compile-time interface; COURSE_READER is the shared runtime identity. Decorator imports and implementations are omitted, so this is non-runnable.

Let the container own framework-managed instances

Calling new CatalogService manually does not ask Nest to resolve its dependencies, apply registered scopes or run container lifecycle behavior. Manual construction is useful in a pure unit test when dependencies are supplied deliberately, but it is different from application wiring. Likewise, listing the same service independently in several modules can create separate registrations rather than sharing the intended exported provider. Review the owning module and export/import relationship. A decorator on a class is not global discoverability, and a TypeScript import does not make its provider visible to another module.

Check symbol identity without a container

The local map demonstrates why equal descriptions do not make two symbols interchangeable. An alias referencing the original token resolves the synthetic value, while a newly constructed symbol does not. This is ordinary JavaScript identity behavior, not a Nest container test. It helps isolate a token mismatch before investigating modules or scopes. Do not stringify a symbol and use its description as a replacement key unless that is an intentionally different token scheme. Stable exported token ownership prevents both accidental mismatch and ambiguous duplication of capability names.

javascript Complete offline token-identity model
const token=Symbol("COURSE_READER");
const alias=token; const other=Symbol("COURSE_READER");
const bindings=new Map([[token,{name:"synthetic-reader"}]]);
console.assert(bindings.get(alias).name === "synthetic-reader");
console.assert(bindings.get(other) === undefined);

The alias resolves and the same-description new symbol does not. No provider registration or Nest resolution occurs.

Resolve wiring errors with the graph, not more globals

When construction fails, identify the exact token, constructor position, owning module and consuming module. Check registration, exports and imports before making a module global or duplicating a source client. A factory provider may also fail while obtaining configuration, which is different from an unknown token. Preserve the distinction in diagnostics without logging credentials. Compile a small testing module with explicit fakes when integration is authorized. This course executes only the identity map and does not instantiate decorators, reflection metadata or the Nest container. The limitation is part of the evidence, not an invitation to treat every runtime error as a spelling mistake.

Common errors and fixes

An interface cannot be resolved at runtime

Why it happens
An erased compile-time type was used as an injection identity.
How to fix it
Bind and inject a shared runtime token.
Prevent it next time
Export tokens from a stable dependency-light module.

Two identical token labels do not match

Why it happens
Two distinct Symbol objects were created.
How to fix it
Import the original exported token object.
Prevent it next time
Test token identity and review duplicate declarations.

Manual new misses container dependencies

Why it happens
Construction bypassed dependency resolution and scope management.
How to fix it
Use container registration in the app or explicit fakes in a unit test.
Prevent it next time
Keep construction ownership visible.

Check your understanding

1. Does importing a service class into a file make its provider visible?

Show worked solution

No. The language import makes a value available to the file, while Nest visibility follows module registration and exports/imports. Identify the owning module and consume its exported capability rather than adding duplicate providers to silence a resolution error. Verify actual wiring with a focused container test when authorized.

2. Why can manual construction still be appropriate in a unit test?

Show worked solution

A pure test can supply every dependency directly and verify domain behavior without framework startup. That is intentional control of construction, not proof of container wiring. Keep a separate small integration test for tokens, module visibility and scopes so each test’s claim matches the boundary it actually exercises.

Key takeaways

  • Runtime tokens and module visibility jointly define injection.
  • Manual construction and container construction provide different evidence.

Official references