Injection Tokens
This guide covers advanced injection token patterns. For basic token creation and usage, see the Getting Started guide.
Foundation: Every Service Has a Token
Injection Tokens are the foundation of Navios DI. Every @Injectable service and @Factory has an Injection Token:
- Auto-created: When you use
@Injectable()without atokenoption, the DI system automatically creates a token from the class - Explicit: You can provide your own token using the
tokenoption in@Injectable()or@Factory()
The token is what identifies the service in the Registry and what the Container uses to resolve services. Services are resolved by token, not by class directly.
Multiple Implementations
You can register multiple implementations for the same token. The one with the highest priority wins:
interface PaymentProcessor {
processPayment(amount: number): Promise<string>
}
const PAYMENT_PROCESSOR_TOKEN =
InjectionToken.create<PaymentProcessor>('PaymentProcessor')
// Stripe implementation
@Injectable({ token: PAYMENT_PROCESSOR_TOKEN, priority: 100 })
class StripePaymentProcessor implements PaymentProcessor {
async processPayment(amount: number) {
return `Processed $${amount} via Stripe`
}
}
// PayPal implementation (higher priority - wins)
@Injectable({ token: PAYMENT_PROCESSOR_TOKEN, priority: 200 })
class PayPalPaymentProcessor implements PaymentProcessor {
async processPayment(amount: number) {
return `Processed $${amount} via PayPal`
}
}
// Usage - PayPalPaymentProcessor will be resolved
const paymentProcessor = await container.get(PAYMENT_PROCESSOR_TOKEN)
await paymentProcessor.processPayment(100)
This pattern is useful for:
- Environment-specific implementations: Different implementations for different environments
- Feature flags: Enable/disable features by overriding services
- Plugin systems: Allow plugins to register implementations
Complex Schemas
Injection Tokens can use complex Zod schemas for type-safe configuration:
import { Injectable, InjectionToken } from '@navios/di'
import { z } from 'zod'
// Complex nested schema
const appConfigSchema = z.object({
database: z.object({
host: z.string(),
port: z.number().min(1).max(65535),
credentials: z.object({
username: z.string(),
password: z.string(),
}),
}),
api: z.object({
baseUrl: z.string().url(),
timeout: z.number().min(1000),
retries: z.number().min(0).max(10),
}),
features: z.object({
enableAnalytics: z.boolean(),
enableCache: z.boolean(),
}),
})
const APP_CONFIG_TOKEN = InjectionToken.create<
AppConfig,
typeof appConfigSchema
>('APP_CONFIG', appConfigSchema)
@Injectable({ token: APP_CONFIG_TOKEN })
class AppConfigService {
constructor(private config: z.infer<typeof appConfigSchema>) {}
getDatabaseHost() {
return this.config.database.host
}
}
Token Composition
You can compose tokens from other tokens:
// Base configuration token
const baseConfigSchema = z.object({
environment: z.enum(['development', 'production']),
debug: z.boolean(),
})
const BASE_CONFIG_TOKEN = InjectionToken.create<
BaseConfig,
typeof baseConfigSchema
>('BASE_CONFIG', baseConfigSchema)
// Extended configuration token
const extendedConfigSchema = baseConfigSchema.extend({
apiKey: z.string(),
apiSecret: z.string(),
})
const EXTENDED_CONFIG_TOKEN = InjectionToken.create<
ExtendedConfig,
typeof extendedConfigSchema
>('EXTENDED_CONFIG', extendedConfigSchema)
Injecting Schema-based Services
You can inject schema-based services with bound arguments:
import { inject, Injectable } from '@navios/di'
import { z } from 'zod'
const dbConfigSchema = z.object({
connectionString: z.string(),
})
@Injectable({ schema: dbConfigSchema })
class DatabaseConfig {
constructor(public readonly config: z.output<typeof dbConfigSchema>) {}
}
@Injectable()
class DatabaseService {
// Inject with bound arguments
private dbConfig = inject(DatabaseConfig, {
connectionString: 'postgres://localhost:5432/myapp',
})
connect() {
return `Connecting to ${this.dbConfig.config.connectionString}`
}
}
Token Groups
Organize related tokens together:
export const DATABASE_TOKENS = {
CONFIG: InjectionToken.create<DatabaseConfig>('DatabaseConfig'),
CONNECTION: InjectionToken.create<DatabaseConnection>('DatabaseConnection'),
REPOSITORY: InjectionToken.create<UserRepository>('UserRepository'),
} as const
// Usage
@Injectable({ token: DATABASE_TOKENS.CONNECTION })
class DatabaseConnection {}
@Injectable({ token: DATABASE_TOKENS.REPOSITORY })
class UserRepository {}
This improves maintainability and makes token relationships clear.
Advanced Bound Token Patterns
Environment-Specific Bound Tokens
const CONFIG_TOKEN = InjectionToken.create<Config, typeof configSchema>(
'APP_CONFIG',
configSchema,
)
// Create environment-specific bound tokens
const PRODUCTION_CONFIG = InjectionToken.bound(CONFIG_TOKEN, {
apiUrl: 'https://api.production.com',
timeout: 10000,
retries: 5,
})
const DEVELOPMENT_CONFIG = InjectionToken.bound(CONFIG_TOKEN, {
apiUrl: 'https://api.dev.com',
timeout: 5000,
retries: 3,
})
// Use the appropriate token based on environment
const configToken = process.env.NODE_ENV === 'production'
? PRODUCTION_CONFIG
: DEVELOPMENT_CONFIG
const config = await container.get(configToken)
Advanced Factory Token Patterns
Dynamic Configuration Based on Dependencies
const DYNAMIC_CONFIG = InjectionToken.factory(CONFIG_TOKEN, async (ctx) => {
// Access other services from the container
const envService = await ctx.container.get(EnvironmentService)
const featureFlags = await ctx.container.get(FeatureFlagsService)
return {
apiUrl: envService.getApiUrl(),
timeout: featureFlags.isEnabled('fast-timeout') ? 1000 : 5000,
retries: envService.isProduction() ? 5 : 3,
}
})
Conditional Factory Tokens
const CONDITIONAL_CONFIG = InjectionToken.factory(CONFIG_TOKEN, async (ctx) => {
const env = process.env.NODE_ENV || 'development'
if (env === 'production') {
return {
apiUrl: 'https://api.production.com',
timeout: 10000,
}
} else if (env === 'staging') {
return {
apiUrl: 'https://api.staging.com',
timeout: 5000,
}
} else {
return {
apiUrl: 'http://localhost:3000',
timeout: 2000,
}
}
})
Best Practices
1. Use Descriptive Token Names
Choose clear, descriptive names that indicate the token's purpose:
// ✅ Good: Descriptive names
const USER_REPOSITORY_TOKEN =
InjectionToken.create<UserRepository>('UserRepository')
const EMAIL_SERVICE_TOKEN = InjectionToken.create<EmailService>('EmailService')
// ❌ Avoid: Generic names
const SERVICE_TOKEN = InjectionToken.create<Service>('Service')
2. Define Schemas for Configuration Tokens
Always use Zod schemas for configuration tokens to ensure type safety and validation:
// ✅ Good: Define schema for configuration
const configSchema = z.object({
apiUrl: z.string().url(),
timeout: z.number().min(1000),
retries: z.number().min(0).max(10),
})
const CONFIG_TOKEN = InjectionToken.create<Config, typeof configSchema>(
'APP_CONFIG',
configSchema,
)
3. Group Related Tokens
Organize related tokens together for better maintainability:
export const DATABASE_TOKENS = {
CONFIG: InjectionToken.create<DatabaseConfig>('DatabaseConfig'),
CONNECTION: InjectionToken.create<DatabaseConnection>('DatabaseConnection'),
REPOSITORY: InjectionToken.create<UserRepository>('UserRepository'),
} as const
4. Use Bound Tokens for Environment-Specific Configuration
Bound tokens are perfect for environment-specific static configuration:
const PRODUCTION_CONFIG = InjectionToken.bound(CONFIG_TOKEN, {
apiUrl: 'https://api.production.com',
timeout: 10000,
retries: 5,
})
5. Use Factory Tokens for Dynamic Default Values
Factory tokens are ideal when configuration depends on runtime values:
const DYNAMIC_CONFIG = InjectionToken.factory(CONFIG_TOKEN, async () => {
const env = process.env.NODE_ENV || 'development'
return {
apiUrl:
env === 'production' ? 'https://api.prod.com' : 'https://api.dev.com',
timeout: env === 'production' ? 10000 : 5000,
retries: env === 'production' ? 5 : 3,
}
})