Providers
April 26, 2026 · View on GitHub
This guide covers the different ways to register dependencies in your Illuma container.
Table of contents
- Providers
Overview
Providers tell the container how to create or retrieve instances of your dependencies. Each provider specifies:
- What to provide (the token)
- How to provide it (value, factory, class, or alias)
Provider types
Class provider
The simplest way to register a service - just pass the class directly:
@NodeInjectable()
class UserService implements iUserService {
public getUser(id: string) {
return { id, name: 'John' };
}
}
// Direct class registration
container.provide(UserService);
Or with explicit configuration:
// This approach is equivalent to the above but enables swapping implementations
const USER_SERVICE = new NodeToken<iUserService>('USER_SERVICE');
container.provide({
provide: USER_SERVICE,
useClass: UserService
});
Value provider
Provide a pre-created value:
interface Config {
apiUrl: string;
debug: boolean;
}
const CONFIG = new NodeToken<Config>('CONFIG');
container.provide({
provide: CONFIG,
value: {
apiUrl: 'https://api.example.com',
debug: true
}
});
Using the token helper:
container.provide(
CONFIG.withValue({
apiUrl: 'https://api.example.com',
debug: true
})
);
Using the generic helper:
container.provide(
CONFIG.implement({
value: {
apiUrl: 'https://api.example.com',
debug: true
}
})
);
Factory provider
Use a factory function for custom instantiation logic:
const DATABASE = new NodeToken<Database>('DATABASE');
container.provide({
provide: DATABASE,
factory: () => {
const config = nodeInject(CONFIG);
return new Database({
connectionString: config.databaseUrl,
maxConnections: 10
});
}
});
Using the token helper:
container.provide(
DATABASE.withFactory(() => {
const config = nodeInject(CONFIG);
return new Database(config.databaseUrl);
})
);
Using the generic helper:
container.provide(
DATABASE.implement({
factory: () => {
const config = nodeInject(CONFIG);
return new Database(config.databaseUrl);
}
})
);
Note: Factory functions run during container bootstrap, not when the dependency is requested.
Class reference provider
Use a different implementation class for a token:
interface Logger {
log(message: string): void;
}
class ConsoleLogger implements Logger {
public log(message: string) {
console.log(message);
}
}
const LOGGER = new NodeToken<Logger>('LOGGER');
container.provide({
provide: LOGGER,
useClass: ConsoleLogger
});
Using the token helper:
container.provide(LOGGER.withClass(ConsoleLogger));
Using the generic helper:
container.provide(
LOGGER.implement({ useClass: ConsoleLogger })
);
Alias provider
Create an alias to another token, making both resolve to the same instance:
@NodeInjectable()
class PostgresDatabase { }
const PRIMARY_DB = new NodeToken<PostgresDatabase>('PRIMARY_DB');
const DB = new NodeToken<PostgresDatabase>('DB');
container.provide(PostgresDatabase);
container.provide({
provide: PRIMARY_DB,
alias: PostgresDatabase
});
container.provide({
provide: DB,
alias: PRIMARY_DB
});
container.bootstrap();
// All resolve to the same instance
const db1 = container.get(PostgresDatabase);
const db2 = container.get(PRIMARY_DB);
const db3 = container.get(DB);
// db1 === db2 === db3
Using the token helper:
container.provide(DB.withAlias(PRIMARY_DB));
Using the generic helper:
container.provide(
DB.implement({ alias: PRIMARY_DB })
);
Generic helper
Generic helpers like implement can be used for any provider type, allowing you to choose the most concise syntax for your use case, primarily when creating provider functions (e.g. provideDatabase()) that return multiple providers by scope or environment. For example:
function provideDatabase(cfg: ImplementationShape<>): Provider[] {
return [
DatabaseService,
DATABASE_CONFIG.implement(cfg),
];
}
Registering providers
Single provider
container.provide(UserService);
container.provide({
provide: CONFIG,
value: { apiUrl: 'https://api.example.com' }
});
Array providers
Group related providers together (recommended):
container.provide([
UserService,
DatabaseService,
{
provide: CONFIG,
value: { apiUrl: 'https://api.example.com' }
},
LOGGER.withClass(ConsoleLogger)
]);
Nested arrays
Organize complex provider configurations:
const coreProviders = [
Logger,
CONFIG.withValue({ debug: true })
];
const databaseProviders = [
Database,
UserRepository,
ProductRepository
];
const serviceProviders = [
UserService,
ProductService,
OrderService
];
container.provide([
coreProviders,
databaseProviders,
serviceProviders
]);
Best practices
1. Group related providers
// ✅ Good: Organized by feature
const authProviders = [
AuthService,
TokenService,
AUTH_CONFIG.withValue({ tokenExpiry: 3600 })
];
const userProviders = [
UserService,
UserRepository
];
container.provide([authProviders, userProviders]);
2. Use token helpers
// ✅ Good: Type-safe and concise
container.provide([
CONFIG.withValue({ apiUrl: 'https://api.example.com' }),
LOGGER.withClass(ConsoleLogger),
DATABASE.withFactory(() => createConnection())
]);
// Also valid, but more verbose
container.provide([
{ provide: CONFIG, value: { apiUrl: 'https://api.example.com' } },
{ provide: LOGGER, useClass: ConsoleLogger },
{ provide: DATABASE, factory: () => createConnection() }
]);
3. Register all providers before bootstrap
// ✅ Correct order
container.provide([/* all providers */]);
container.bootstrap();
container.get(SomeService);
// ❌ Wrong: Cannot provide after bootstrap
container.bootstrap();
container.provide(AnotherService); // Throws [i301]
4. Use factories for complex initialization
// ✅ Good: Factory handles complex setup
container.provide({
provide: DATABASE,
factory: () => {
const config = nodeInject(CONFIG);
const logger = nodeInject(LOGGER);
const db = new Database(config.connectionString);
db.on('error', (err) => logger.error(err));
return db;
}
});
Related documentation
- Tokens Guide - Learn about NodeToken and MultiNodeToken
- Resolution Modifiers - Modifying dependency resolution
- API Reference - Complete API documentation
- Troubleshooting - Common errors and solutions