Getting Started
April 26, 2026 ยท View on GitHub
This guide will walk you through setting up Illuma and creating your first dependency injection container.
Table of contents
Installation
Install Illuma using your preferred package manager:
npm install @illuma/core
# or
pnpm add @illuma/core
# or
yarn add @illuma/core
# or
bun add @illuma/core
TypeScript configuration (for decorators if using)
To use decorators like @NodeInjectable(), enable experimental decorators in your tsconfig.json:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
Alternative: Without decorators
If you prefer not to use decorators, use makeInjectable to mark classes as injectable:
import { makeInjectable } from '@illuma/core';
class _UserService {
public getUser() {
return { id: 1, name: "John Doe" };
}
}
export type UserService = _UserService;
export const UserService = makeInjectable(_UserService);
Basic setup
Illuma uses three core concepts:
- Container - Manages all your dependencies
- Providers - Register services and values
- Injection - Retrieve dependencies when needed
Your first container
import { NodeContainer, NodeInjectable, nodeInject } from '@illuma/core';
// 1. Define injectable services
@NodeInjectable()
class Logger {
public log(message: string) {
console.log(`[LOG]: ${message}`);
}
}
@NodeInjectable()
class UserService {
private readonly logger = nodeInject(Logger);
public getUser(id: string) {
this.logger.log(`Fetching user ${id}`);
return { id, name: 'John Doe' };
}
}
// 2. Create and configure container
const container = new NodeContainer();
// 3. Register providers
container.provide([Logger, UserService]);
// 4. Bootstrap the container
container.bootstrap();
// 5. Use your services
const userService = container.get(UserService);
const user = userService.getUser('123');
Injecting dependencies
Use nodeInject() to inject dependencies into your services:
@NodeInjectable()
class EmailService {
private readonly logger = nodeInject(Logger);
private readonly config = nodeInject(CONFIG_TOKEN);
public sendEmail(to: string, message: string) {
this.logger.log(`Sending email to ${to}`);
// Use this.config...
}
}
Or you can inject via factory functions:
container.provide({
provide: EMAIL_SERVICE,
factory: () => {
const logger = nodeInject(Logger);
return new EmailService(logger);
}
});
Optional dependencies
Mark dependencies as optional to handle cases where they may not be provided:
@NodeInjectable()
class MyService {
private readonly optionalLogger = nodeInject(Logger, { optional: true });
// ^? Logger | null โ infers nullability!
public doSomething() {
this.optionalLogger?.log('Doing something');
}
}
Using tokens
Tokens let you inject values that aren't classes, like configuration objects:
import { NodeToken, NodeContainer } from '@illuma/core';
// Define a token
interface Config {
apiUrl: string;
timeout: number;
}
const CONFIG_TOKEN = new NodeToken<Config>('CONFIG');
// Provide a value
const container = new NodeContainer();
container.provide({
provide: CONFIG_TOKEN,
value: {
apiUrl: 'https://api.example.com',
timeout: 5000
}
});
// Or use the helper method
container.provide(
CONFIG_TOKEN.withValue({
apiUrl: 'https://api.example.com',
timeout: 5000
})
);
container.bootstrap();
const config = container.get(CONFIG_TOKEN);
Root-scoped singletons
Use { singleton: true } to make an injectable class behave like Angular's providedIn: 'root'.
import { NodeContainer, NodeInjectable } from '@illuma/core';
@NodeInjectable({ singleton: true })
class GlobalService {
public readonly id = Math.random();
}
const root = new NodeContainer();
const childA = new NodeContainer({ parent: root });
const childB = new NodeContainer({ parent: root });
root.bootstrap();
childA.bootstrap();
childB.bootstrap();
const a = childA.get(GlobalService);
const b = childB.get(GlobalService);
const r = root.get(GlobalService);
console.log(a === b && b === r); // true
How it works
- The singleton class stores metadata on its token.
- On first resolution from any child/root container, Illuma attaches that provider to the root container tree.
- The same root instance is then reused across descendants.
Important behavior
- Child overrides (using
.provide()) still work locally and do not replace the root singleton globally. - Root singletons can only inject dependencies visible from root (not child-only providers).
- Circular dependency detection works the same as for normal providers.
Next steps
Now that you understand the basics, explore these topics:
- Providers Guide - Learn about different provider types (value, factory, class, alias)
- Tokens Guide - Deep dive into NodeToken and MultiNodeToken
- Async Injection Guide - Lazy loading and sub-containers
- Lifecycle Guide - Container hooks and destruction
- Testing Guide - Testing with the Illuma testkit
- API Reference - Complete API documentation
- Error Reference - Troubleshooting common issues