Fundamentals
Dependency injection
Declare dependencies with inject(), register providers in modules, and let Knead create, share and wire every instance at startup.
On this page 19 sections
- Injecting with inject()
- Tokens
- Providers
- Class providers
- Value providers
- Factory providers
- Async factories
- Aliases
- Multi providers
- Invalid providers
- Scopes
- Optional dependencies
- The injection context
- Startup resolution
- Circular dependencies
- Missing providers
- Overriding providers
- Decorators and metadata
- One copy of @knead-js/core
Knead creates the objects your application is made of (services, repositories, controllers,
guards) and hands each one the dependencies it asks for. A class asks with inject(Token) in a
field initializer; a module registers a provider that tells Knead how to produce the value for that
token. Knead builds every singleton at startup, so a missing or misconfigured dependency fails
immediately with an error that names it, never in the middle of a request.
#Injecting with inject()
Call inject() in a field initializer. It returns the instance for the token, typed from the
token:
@Injectable()
export class PriceFormatter {
format(cents: number): string {
return `$${(cents / 100).toFixed(2)}`;
}
}
@Injectable()
export class CartService {
// Field initializers run inside the injection context.
readonly formatter = inject(PriceFormatter);
total(items: number[]): string {
return this.formatter.format(items.reduce((sum, cents) => sum + cents, 0));
}
}Both classes are registered as providers of the same module (providers: [PriceFormatter, CartService]), so CartService can inject PriceFormatter. When Knead creates CartService, it
runs the field initializers inside an injection context, and inject(PriceFormatter) returns the
module's single PriceFormatter.
inject() also works in the constructor body:
export const CURRENCY = new InjectionToken<string>('CURRENCY');
@Injectable()
export class InvoiceService {
readonly prefix: string;
constructor() {
// The constructor body runs in the injection context too.
const currency = inject(CURRENCY);
this.prefix = `INV-${currency}`;
}
}Constructor parameters are not injected. Knead does not use reflect-metadata or
emitDecoratorMetadata, so there is no parameter type information to read; Knead always calls the
constructor with no arguments.
#Tokens
A token identifies a dependency. There are two kinds:
- A class. The class is its own token:
inject(PriceFormatter)returns aPriceFormatter. Abstract classes work too, which lets you inject an interface-like base class and choose the implementation in the module. - An
InjectionToken<T>. For values that are not class instances (configuration objects, strings, functions, interfaces), create a token with a type parameter and a description:
export interface AppConfig {
databaseUrl: string;
port: number;
}
// The type parameter is what inject(APP_CONFIG) returns.
export const APP_CONFIG = new InjectionToken<AppConfig>('APP_CONFIG');inject(APP_CONFIG) returns an AppConfig. The description appears in error messages, and
String(APP_CONFIG) is InjectionToken(APP_CONFIG). Tokens are compared by identity, so two
tokens with the same description are different tokens: create each token once and import it
wherever you need it.
export abstract class Cache {
abstract get(key: string): string | undefined;
abstract set(key: string, value: string): void;
}
@Injectable()
export class MemoryCache extends Cache {
readonly #entries = new Map<string, string>();
get(key: string): string | undefined {
return this.#entries.get(key);
}
set(key: string, value: string): void {
this.#entries.set(key, value);
}
}#Providers
A provider tells a module how to produce the value of a token. The providers array of
@Module() accepts these forms:
| Form | Value of the token | Scope |
|---|---|---|
SomeClass | A SomeClass instance. Shorthand for { provide: SomeClass, useClass: SomeClass }. | From @Injectable(), default singleton |
{ provide, useClass } | An instance of useClass. | scope option, else from @Injectable() on useClass, default singleton |
{ provide, useValue } | useValue itself. | Always singleton |
{ provide, useFactory } | The factory's return value, or what its promise resolves to. | scope option, default singleton |
{ provide, useExisting } | The value of another token: an alias. | Always singleton |
Every object form also accepts multi: true. See multi providers.
@Injectable()
export class Connection {
readonly url = inject(APP_CONFIG).databaseUrl;
}
export const CONNECTION_LABEL = new InjectionToken<string>('CONNECTION_LABEL');
export const LEGACY_CACHE = new InjectionToken<Cache>('LEGACY_CACHE');
export const providers: Provider[] = [
// Value: the object itself, as is.
{ provide: APP_CONFIG, useValue: { databaseUrl: 'postgres://db.internal/app', port: 3000 } },
// Class: inject(Cache) gets a MemoryCache.
{ provide: Cache, useClass: MemoryCache },
// Shorthand for { provide: Connection, useClass: Connection }.
Connection,
// Factory: runs in the injection context.
{ provide: CONNECTION_LABEL, useFactory: () => `db at ${inject(Connection).url}` },
// Alias: the same instance as Cache, under another token.
{ provide: LEGACY_CACHE, useExisting: Cache },
];#Class providers
A class provider creates the instance with new, inside the injection context of the module that
registers it, so its field initializers can call inject(). With useClass, the token and the
class differ: { provide: Cache, useClass: MemoryCache } makes inject(Cache) return a
MemoryCache.
#Value providers
A value provider returns the value as is. Knead does not copy it, and does not call
lifecycle hooks on it, even when it is an object with an onModuleInit
method.
#Factory providers
A factory provider calls useFactory with no arguments, inside the injection context, so the
factory calls inject() for whatever it needs. Call inject() in the factory body itself, not in
a callback the factory returns: the callback runs later, outside the injection context. The
HEALTH_CHECKS example below shows the right way.
#Async factories
A singleton factory may return a promise. During startup, Knead runs every singleton factory and awaits the async ones before it creates the remaining classes, so a class can inject the resolved value directly:
export interface Secrets {
apiKey: string;
}
export const SECRETS = new InjectionToken<Secrets>('SECRETS');
async function fetchSecrets(url: string): Promise<Secrets> {
await Bun.sleep(1);
return { apiKey: `key-for-${url}` };
}
export const secretsProvider = {
provide: SECRETS,
useFactory: async () => {
// inject() first: after the first await there is no injection context.
const config = inject(APP_CONFIG);
return fetchSecrets(config.databaseUrl);
},
};
@Injectable()
export class PaymentsClient {
// Classes can inject an async provider: it has settled before any class is built.
readonly apiKey = inject(SECRETS).apiKey;
}The rules:
- Call
inject()before the firstawait. The injection context ends when the factory first awaits. Aninject()after it throwsInjectionContextError, which rejects startup. - The start of a factory can run more than once. When a factory injects an async provider that
has not settled yet, Knead awaits that provider and then calls the factory again, so the code
before that
inject()runs twice. Keep everything before the firstawaitfree of side effects: onlyinject()calls. - A rejected promise fails startup.
ApplicationContext.create()(andKneadFactory.create()) rejects with the factory's error. - Transient factories cannot be async. A transient factory that returns a promise throws
ProviderConfigError:Transient provider SECRETS cannot use an async factory.
#Aliases
useExisting makes a token resolve to the same value as another token. In the providers above,
inject(LEGACY_CACHE) and inject(Cache) return the same MemoryCache instance. The aliased
token is looked up from the module that registers the alias.
#Multi providers
With multi: true, several providers contribute to one token, and inject() returns an array of
every contribution. The token's type is the array type:
export interface HealthCheck {
name: string;
check(): boolean;
}
export const HEALTH_CHECKS = new InjectionToken<HealthCheck[]>('HEALTH_CHECKS');
export const healthChecks: Provider[] = [
{ provide: HEALTH_CHECKS, multi: true, useValue: { name: 'memory', check: () => true } },
{
provide: HEALTH_CHECKS,
multi: true,
useFactory: () => {
const cache = inject(Cache); // in the factory body, not inside check()
return { name: 'cache', check: () => cache instanceof MemoryCache };
},
},
];
@Injectable()
export class HealthService {
readonly checks = inject(HEALTH_CHECKS);
report(): Record<string, boolean> {
return Object.fromEntries(this.checks.map((c) => [c.name, c.check()]));
}
}Contributions keep their registration order within a module. A token that a module, its imports and global modules all contribute to resolves to the module's own contributions first, then those of its imports (in import order), then those of global modules. Contributions from other modules are only included when those modules export the token.
Registering one token in the same module both with and without multi: true is a
ProviderConfigError:
HEALTH_CHECKS is registered in AppModule both with and without multi: true.The @knead-js/http package uses multi tokens such as APP_MIDDLEWARE and APP_GUARD to
let any module contribute global middleware and guards.
#Invalid providers
A malformed provider is a ProviderConfigError when the module graph is built:
Invalid provider: 42. Expected a class or an object with "provide".Provider for APP_CONFIG must define useClass, useValue, useFactory or useExisting.UsersService is listed in the providers of UsersModule but is not decorated with @Injectable().The provider for MAILER in AppModule uses SmtpMailer, which is not decorated with @Injectable().The provider for MAILER in AppModule: useClass is undefined. This usually means a circular file import; check the import order of your files.The provider for CLOCK in AppModule: useFactory must be a function, got 42.
provide and useExisting must be a class or an InjectionToken, useClass a class and
useFactory a function.
#Scopes
Every provider is a singleton by default: one instance per module that registers it, shared by everything that injects it, created at startup and cached.
A transient provider creates a new instance for every injection. Mark a class with
@Injectable({ scope: 'transient' }), or set scope: 'transient' on a useClass or useFactory
provider (the scope option takes precedence over @Injectable()):
@Injectable({ scope: 'transient' })
export class RequestIdGenerator {
readonly id = crypto.randomUUID();
}
@Injectable()
export class AuditLog {
readonly ids = inject(RequestIdGenerator);
}
@Injectable()
export class AccessLog {
readonly ids = inject(RequestIdGenerator);
}AuditLog and AccessLog each get their own RequestIdGenerator. Transient instances are created
when something injects them, not eagerly at startup.
Every class the container constructs as a provider, whether listed directly (providers: [Cls])
or through useClass, must carry @Injectable(); its only option is scope. Value, factory and
alias providers need no decorator, nor do guard, middleware and filter classes. Controllers and
resolvers are marked by @Controller() and @Resolver(), and exception filters by @Catch(). A
subclass of a marked class is marked too and inherits its scope.
#Optional dependencies
Pass { optional: true } to get undefined instead of an error when no provider is visible:
export interface Tracer {
span(name: string): void;
}
export const TRACER = new InjectionToken<Tracer>('TRACER');
@Injectable()
export class CheckoutService {
readonly tracer = inject(TRACER, { optional: true });
checkout(): string {
this.tracer?.span('checkout');
return 'ok';
}
}The return type becomes Tracer | undefined. The option only covers a missing provider: if a
provider is visible but creating it throws, the error is not swallowed.
#The injection context
inject() only works while Knead is creating something: synchronously inside a field initializer,
a constructor or a provider factory. Anywhere else it throws InjectionContextError:
inject(CLOCK) was called outside an injection context. inject() only works synchronously inside field initializers, constructors and provider factories; call it before the first await.The common mistake is calling inject() in a method:
export class BrokenReminderService {
remindLater(): number {
// Throws InjectionContextError: a method runs outside the injection context.
return inject(CLOCK).now() + 60_000;
}
}Inject in a field instead, and use the field in the method:
export class ReminderService {
readonly #clock = inject(CLOCK);
remindLater(): number {
return this.#clock.now() + 60_000;
}
}In an async factory, the context ends at the first await:
export const START_TIME = new InjectionToken<number>('START_TIME');
export const startTimeProvider = {
provide: START_TIME,
useFactory: async () => {
const clock = inject(CLOCK); // before the first await: works
await Bun.sleep(1);
// inject(CLOCK) here would throw InjectionContextError.
return clock.now();
},
};Two lower-level functions let library code work with the context directly.
runInInjectionContext(injector, fn) runs fn with injector as the current context and returns
its result. isInInjectionContext() reports whether a context is active. A module injector (the
injector field of a ModuleRef, or context.root.injector) can be passed as the injector:
/** Builds a ReminderService that is not registered as a provider, outside the container. */
export function buildReminderService(context: ApplicationContext): ReminderService {
return runInInjectionContext(context.root.injector, () => new ReminderService());
}
export function insideInjectionContext(): boolean {
return isInInjectionContext();
}An instance built this way is not a provider: Knead does not cache it, and it does not receive lifecycle hooks.
#Startup resolution
ApplicationContext.create(), which KneadFactory.create() and @knead-js/testing both use,
resolves the whole application before it returns:
- It builds the module graph and registers every provider, which reports module definition errors and invalid providers.
- It runs every singleton factory, in module order, awaiting async ones.
- It creates every remaining singleton (classes, controllers, aliases).
- It runs the
onModuleInitandonApplicationBootstraplifecycle hooks.
A missing provider, a cycle or a throwing constructor in any singleton therefore fails startup, even if no request would ever reach that code.
#Circular dependencies
Knead does not resolve circular dependencies; there is no forwardRef(). A cycle throws
CircularDependencyError with the full path:
@Injectable()
export class OrdersService {
// OrdersService → InvoicesService → OrdersService
readonly invoices: InvoicesService = inject(InvoicesService);
}
@Injectable()
export class InvoicesService {
readonly orders: OrdersService = inject(OrdersService);
}Circular dependency detected: OrdersService → InvoicesService → OrdersServiceThe error's path property holds the same names as an array. Cycles through aliases and async
factories are detected the same way. To fix one, move the logic both classes need into a third
provider that depends on neither:
// The logic both services needed moves into a third provider that depends on neither.
@Injectable()
export class PricingService {
totalCents(lines: number[]): number {
return lines.reduce((sum, cents) => sum + cents, 0);
}
}
@Injectable()
export class Orders {
readonly pricing = inject(PricingService);
}
@Injectable()
export class Invoices {
readonly pricing = inject(PricingService);
}Modules importing each other are a different error, a ModuleDefinitionError; see
module definition errors.
#Missing providers
When no provider for a token is visible, inject() throws NoProviderError. The message names the
class that asked, its module and the token, followed by one of three hints:
| Situation | Hint |
|---|---|
| No module provides the token. | No module provides Db. Add it to the providers of UsersModule or of a module that UsersModule imports. |
| Another module provides it but does not export it. | Db is provided by DatabaseModule but not exported. |
| Another module exports it, but this module does not import that module. | Db is exported by DatabaseModule; add DatabaseModule to the imports of UsersModule. |
For example:
UsersService (UsersModule) requested Db, but no provider is visible. Db is provided by DatabaseModule but not exported.#Overriding providers
ApplicationContext.create() accepts overrides: pairs of a token and a replacement provider. An
override replaces the provider for that token in every module that registers it, and is resolved in
that module, so a useClass override can inject what the original could:
export class FakeGateway extends PaymentGateway {
override charge(cents: number): string {
return `pretended to charge ${cents}`;
}
}
export async function createTestContext(): Promise<ApplicationContext> {
return ApplicationContext.create(CheckoutModule, {
overrides: [[PaymentGateway, { provide: PaymentGateway, useClass: FakeGateway }]],
logger: new MemoryLogger(),
development: true,
});
}Overrides apply to tokens that some module registers, plus the classes that @knead-js/http
creates on demand (guards, middleware and exception filters). An override for a token that no
module provides has no effect, and multi providers cannot be overridden. In tests, use the
overrideProvider(), overrideGuard() and related methods of @knead-js/testing, which build on
this option; see testing.
#Decorators and metadata
Knead uses standard TC39 decorators, the ones TypeScript compiles without
experimentalDecorators. Leave experimentalDecorators (and with it emitDecoratorMetadata) off
in your tsconfig.json: with it on, decorators use the legacy semantics, which Knead's decorators
do not support.
Decorators record their options with decorator metadata (Symbol.metadata), not
reflect-metadata. Bun 1.4 does not define Symbol.metadata yet, so @knead-js/core installs it
when it loads. Import @knead-js/core (or any @knead-js/* package) before defining decorated
classes; normal imports at the top of a file already guarantee this.
The same helpers are available for your own decorators. A MetadataKey<T> is a typed key, and
the set, append, read and get functions store and read values on the class or per method:
/** Marks a method as audited, with the action name to record. */
export const AUDITED = new MetadataKey<string>('app:audited');
/** Marks a class with the resource name its audit entries use. */
export const AUDIT_RESOURCE = new MetadataKey<string>('app:audit-resource');
export function AuditResource(name: string) {
return (_target: Function, context: ClassDecoratorContext): void => {
setMetadata(context.metadata, AUDIT_RESOURCE, name);
};
}
export function Audited(action: string) {
return (_method: Function, context: ClassMethodDecoratorContext): void => {
setMethodMetadata(context.metadata, context.name, AUDITED, action);
};
}@AuditResource('invoice')
export class InvoiceController {
@Audited('invoice.void')
void(id: string): string {
return `voided ${id}`;
}
}
export function auditAction(cls: Function, method: string): string | undefined {
const resource = getOwnMetadata(cls, AUDIT_RESOURCE);
const action = getMethodMetadata(cls, method, AUDITED);
return resource && action ? `${resource}:${action}` : undefined;
}Metadata follows class inheritance: getMetadata() on a subclass returns its parent's value unless
the subclass sets its own, while getOwnMetadata() ignores inherited values. appendMetadata() on
a subclass copies the parent's list before appending, so the parent's list never changes.
Each MetadataKey owns a unique symbol, so two keys never collide, even with the same
description. The full list of helpers is in the
core reference.
#One copy of @knead-js/core
Tokens, module metadata and inject() rely on module-level identity, so every @knead-js/*
package must load the same copy of @knead-js/core (it is a peer dependency of all of them). If
two different copies load, for example after a version mismatch leaves a nested install, the
second copy prints one warning:
[knead] Multiple copies of @knead-js/core are loaded (...). Injection tokens, module metadata and inject() will not work across them; make sure every @knead-js/* package resolves the same @knead-js/core (it is a peer dependency).Align the versions of your @knead-js/* packages and reinstall to fix it.