Skip to main content

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 a token option, the DI system automatically creates a token from the class
  • Explicit: You can provide your own token using the token option 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,
)

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,
}
})

Next Steps

  • Learn about services for class-based services
  • Explore factories for complex object creation
  • Understand scopes for service lifetime management