npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@computerwwwizards/dependency-injection

v1.5.1

Published

Welcome! This is a super lightweight, callback-based dependency injection container for TypeScript/JavaScript. No decorators, no proxies, no magic — just simple, flexible DI for real-world code.

Readme

@computerwwwizards/dependency-injection

Welcome! This is a super lightweight, callback-based dependency injection container for TypeScript/JavaScript. No decorators, no proxies, no magic — just simple, flexible DI for real-world code.

Why use this?

  • Tiny footprint: No runtime bloat, no decorators, no proxies, no magic. You can actually read the code in one sitting.
  • Elastic API: Works with any value, any type, any pattern. You control how dependencies are resolved.
  • No proxy/decorator overhead: Unlike InversifyJS or tsyringe, this package is browser-friendly and works with module federation setups.
  • No forced patterns: You decide how to wire dependencies, using simple callbacks and context.
  • Explicit lifecycle: Singletons live as long as the container; transients are created on demand.

Motivation

Most DI libraries are heavy, opinionated, and rely on TypeScript decorators or proxies. This causes:

  • Bundle size bloat
  • Browser compatibility issues
  • Problems with module federation (multiple containers, duplicated metadata)
  • Hard-to-debug magic

This package solves those problems by being minimal, explicit, and callback-driven. You get all the flexibility, none of the headaches.

What's included

  • PrimitiveContainer: Basic DI container with bindTo(), get(), unbind()
  • PrimitiveContainerWithUse: Same as above + use() method for plugins
  • PreProcessDependencyContainer: Advanced container with dependency resolution via bind()
  • PreProcessDependencyContainerWithUse: Advanced container + use() method for plugins
  • createWithUse(): Mixin function to add use() method to any container class
  • createAutoResolver: Resolve dependencies as a typed object (most common pattern)
  • createAutoResolveDepsInOrder: Resolve dependencies as an array in specified order
  • Child containers: ChildPrimitiveContainer for inheritance and testing

Quick Start (Copy-paste friendly!)

Basic usage: PrimitiveContainer

import { PrimitiveContainer } from '@computerwwwizards/dependency-injection'

const container = new PrimitiveContainer()

// Register a singleton value
container.bindTo('config', () => ({ apiUrl: 'https://api.example' }), 'singleton')

// Register a transient value (new every time)
container.bindTo('timestamp', () => Date.now(), 'transient')

// Get your stuff
const config = container.get('config')
const now = container.get('timestamp')

Advanced: PreProcessDependencyContainer with auto-resolution

The most powerful pattern - use createAutoResolver to automatically resolve dependencies:

import { PreProcessDependencyContainer, createAutoResolver } from '@computerwwwizards/dependency-injection'

const container = new PreProcessDependencyContainer()

container.bindTo('database', () => ({ connected: true }), 'singleton')
container.bindTo('logger', () => ({ log: console.log }), 'singleton')

// Use createAutoResolver for clean, typed dependency resolution
container.bind('userService', {
  resolveDependencies: createAutoResolver([
    { identifier: 'database' },
    { identifier: 'logger' }
  ]),
  provider: (deps) => ({
    // deps is typed as { database: Database, logger: Logger }
    findUser: (id: string) => {
      deps.logger.log(`Finding user ${id}`)
      return deps.database.connected ? { id, name: 'User' } : null
    }
  })
})

const userService = container.get('userService')
const user = userService.findUser('123')

Plugin/Middleware Pattern: Containers with use()

For modular dependency registration, use the containers with the use() method:

import { PrimitiveContainerWithUse, PreProcessDependencyContainerWithUse } from '@computerwwwizards/dependency-injection'

// Basic container with plugin support
const container = new PrimitiveContainerWithUse<{
  config: { apiUrl: string }
  logger: { log: (msg: string) => void }
  httpClient: { get: (path: string) => Promise<any> }
}>()

// Create modular plugins
const configPlugin = (c) => {
  c.bindTo('config', () => ({ apiUrl: 'https://api.example.com' }), 'singleton')
}

const loggingPlugin = (c) => {
  c.bindTo('logger', () => ({ log: console.log }), 'singleton')
}

const httpPlugin = (c) => {
  c.bindTo('httpClient', (ctx) => {
    const config = ctx.get('config')
    return {
      get: async (path) => fetch(`${config.apiUrl}${path}`).then(r => r.json())
    }
  })
}

// Use plugins to configure the container
container.use(configPlugin, loggingPlugin, httpPlugin)

// Now everything is ready
const client = container.get('httpClient')
const data = await client.get('/users')

Advanced: PreProcessDependencyContainer with Plugins

import { PreProcessDependencyContainerWithUse, createAutoResolver } from '@computerwwwizards/dependency-injection'

const container = new PreProcessDependencyContainerWithUse<{
  database: { query: (sql: string) => any[] }
  logger: { log: (msg: string) => void }
  userService: { findUser: (id: string) => any }
}>()

const databasePlugin = (c) => {
  c.bindTo('database', () => ({ query: (sql) => [] }), 'singleton')
}

const loggingPlugin = (c) => {
  c.bindTo('logger', () => ({ log: console.log }), 'singleton')  
}

const userServicePlugin = (c) => {
  c.bind('userService', {
    resolveDependencies: createAutoResolver([
      { identifier: 'database' },
      { identifier: 'logger' }
    ]),
    provider: (deps) => ({
      findUser: (id) => {
        deps.logger.log(`Finding user ${id}`)
        return deps.database.query(`SELECT * FROM users WHERE id = ${id}`)[0]
      }
    })
  })
}

// Chain plugins together
container.use(databasePlugin, loggingPlugin, userServicePlugin)

const userService = container.get('userService')
const user = userService.findUser('123')

Custom Mixins: createWithUse

Create your own container classes with plugin support:

import { createWithUse, PrimitiveContainer, PreProcessDependencyContainer } from '@computerwwwizards/dependency-injection'

// Create a custom container class with use() method
const MyCustomContainerWithUse = createWithUse(PrimitiveContainer)

// Or with the advanced container
const MyAdvancedContainerWithUse = createWithUse(PreProcessDependencyContainer)

// Use it like any other container
const container = new MyCustomContainerWithUse<{ service: string }>()
container.use((c) => {
  c.bindTo('service', () => 'Hello World!')
  return c
})

Comprehensive Examples

Example 1: Building a Complete Web Service

Here's how you might structure a real web service with authentication, database, and API endpoints:

import { PreProcessDependencyContainerWithUse, createAutoResolver } from '@computerwwwizards/dependency-injection'

interface WebServiceDeps {
  config: { 
    dbUrl: string
    jwtSecret: string
    port: number
    logLevel: string
  }
  logger: {
    debug: (msg: string) => void
    info: (msg: string) => void
    error: (msg: string) => void
  }
  database: {
    connect: () => Promise<void>
    query: (sql: string, params?: any[]) => Promise<any[]>
    close: () => Promise<void>
  }
  authService: {
    generateToken: (userId: string) => string
    verifyToken: (token: string) => { userId: string } | null
  }
  userRepository: {
    findById: (id: string) => Promise<any>
    create: (user: any) => Promise<any>
    update: (id: string, changes: any) => Promise<any>
  }
  apiServer: {
    start: () => Promise<void>
    stop: () => Promise<void>
  }
}

const container = new PreProcessDependencyContainerWithUse<WebServiceDeps>()

// Configuration - usually loaded from environment
const configPlugin = (c: any) => {
  c.bindTo('config', () => ({
    dbUrl: process.env.DATABASE_URL || 'sqlite://app.db',
    jwtSecret: process.env.JWT_SECRET || 'dev-secret',
    port: Number(process.env.PORT) || 3000,
    logLevel: process.env.LOG_LEVEL || 'info'
  }), 'singleton')
  return c
}

// Logging service
const loggingPlugin = (c: any) => {
  c.bind('logger', {
    resolveDependencies: createAutoResolver([
      { identifier: 'config' }
    ]),
    provider: (deps) => {
      const level = deps.config.logLevel
      return {
        debug: level === 'debug' ? console.log : () => {},
        info: ['debug', 'info'].includes(level) ? console.log : () => {},
        error: console.error
      }
    },
    scope: 'singleton'
  })
  return c
}

// Database connection
const databasePlugin = (c: any) => {
  c.bind('database', {
    resolveDependencies: createAutoResolver([
      { identifier: 'config' },
      { identifier: 'logger' }
    ]),
    provider: (deps) => {
      let connection: any = null
      
      return {
        connect: async () => {
          deps.logger.info(`Connecting to database: ${deps.config.dbUrl}`)
          // connection = await createConnection(deps.config.dbUrl)
          connection = { connected: true } // Mock for example
        },
        query: async (sql: string, params?: any[]) => {
          deps.logger.debug(`Executing query: ${sql}`)
          // return connection.query(sql, params)
          return [] // Mock for example
        },
        close: async () => {
          deps.logger.info('Closing database connection')
          // await connection.close()
        }
      }
    },
    scope: 'singleton'
  })
  return c
}

// Authentication service
const authPlugin = (c: any) => {
  c.bind('authService', {
    resolveDependencies: createAutoResolver([
      { identifier: 'config' },
      { identifier: 'logger' }
    ]),
    provider: (deps) => ({
      generateToken: (userId: string) => {
        deps.logger.debug(`Generating token for user ${userId}`)
        // return jwt.sign({ userId }, deps.config.jwtSecret, { expiresIn: '24h' })
        return `token-${userId}` // Mock for example
      },
      verifyToken: (token: string) => {
        deps.logger.debug(`Verifying token`)
        try {
          // return jwt.verify(token, deps.config.jwtSecret)
          const userId = token.replace('token-', '') // Mock for example
          return { userId }
        } catch {
          return null
        }
      }
    }),
    scope: 'singleton'
  })
  return c
}

// User repository
const userRepositoryPlugin = (c: any) => {
  c.bind('userRepository', {
    resolveDependencies: createAutoResolver([
      { identifier: 'database' },
      { identifier: 'logger' }
    ]),
    provider: (deps) => ({
      findById: async (id: string) => {
        deps.logger.debug(`Finding user ${id}`)
        const users = await deps.database.query('SELECT * FROM users WHERE id = ?', [id])
        return users[0]
      },
      create: async (user: any) => {
        deps.logger.info(`Creating user ${user.email}`)
        const result = await deps.database.query(
          'INSERT INTO users (email, name) VALUES (?, ?)',
          [user.email, user.name]
        )
        return { ...user, id: result.insertId }
      },
      update: async (id: string, changes: any) => {
        deps.logger.debug(`Updating user ${id}`)
        await deps.database.query(
          'UPDATE users SET name = ?, email = ? WHERE id = ?',
          [changes.name, changes.email, id]
        )
        return { id, ...changes }
      }
    }),
    scope: 'singleton'
  })
  return c
}

// API server
const serverPlugin = (c: any) => {
  c.bind('apiServer', {
    resolveDependencies: createAutoResolver([
      { identifier: 'config' },
      { identifier: 'logger' },
      { identifier: 'authService' },
      { identifier: 'userRepository' }
    ]),
    provider: (deps) => {
      let server: any = null
      
      const authMiddleware = (req: any) => {
        const token = req.headers.authorization?.replace('Bearer ', '')
        if (!token) throw new Error('No token provided')
        
        const auth = deps.authService.verifyToken(token)
        if (!auth) throw new Error('Invalid token')
        
        return auth
      }
      
      const routes = {
        'GET /users/:id': async (req: any) => {
          const auth = authMiddleware(req)
          deps.logger.info(`User ${auth.userId} requesting user ${req.params.id}`)
          return deps.userRepository.findById(req.params.id)
        },
        'POST /users': async (req: any) => {
          const auth = authMiddleware(req)
          deps.logger.info(`User ${auth.userId} creating new user`)
          return deps.userRepository.create(req.body)
        },
        'POST /login': async (req: any) => {
          // Normally would validate credentials
          const token = deps.authService.generateToken('user123')
          return { token }
        }
      }
      
      return {
        start: async () => {
          deps.logger.info(`Starting server on port ${deps.config.port}`)
          // server = express()
          // setupRoutes(server, routes)
          // server.listen(deps.config.port)
        },
        stop: async () => {
          deps.logger.info('Stopping server')
          // server?.close()
        }
      }
    },
    scope: 'singleton'
  })
  return c
}

// Wire everything together
container.use(
  configPlugin,
  loggingPlugin, 
  databasePlugin,
  authPlugin,
  userRepositoryPlugin,
  serverPlugin
)

// Bootstrap the application
const bootstrap = async () => {
  const database = container.get('database')
  const server = container.get('apiServer')
  
  await database.connect()
  await server.start()
  
  console.log('🚀 Application started successfully!')
}

bootstrap().catch(console.error)

Example 2: Testing with Child Containers

Use child containers to override dependencies for testing:

import { ChildPreProcessDependencyContainer, createAutoResolver } from '@computerwwwizards/dependency-injection'

// Your main container setup (same as above)
const mainContainer = new PreProcessDependencyContainerWithUse<WebServiceDeps>()
mainContainer.use(/* all your plugins */)

// Create test container that inherits from main but overrides specific services
const createTestContainer = () => {
  const testContainer = new ChildPreProcessDependencyContainer(mainContainer)
  
  // Override database with a mock
  testContainer.bindTo('database', () => ({
    connect: async () => {},
    query: async (sql: string) => {
      if (sql.includes('SELECT * FROM users WHERE id = ?')) {
        return [{ id: '1', name: 'Test User', email: '[email protected]' }]
      }
      return []
    },
    close: async () => {}
  }), 'singleton')
  
  // Override logger to capture logs for assertions
  const logs: string[] = []
  testContainer.bindTo('logger', () => ({
    debug: (msg: string) => logs.push(`DEBUG: ${msg}`),
    info: (msg: string) => logs.push(`INFO: ${msg}`),
    error: (msg: string) => logs.push(`ERROR: ${msg}`)
  }), 'singleton')
  
  return { container: testContainer, logs }
}

// Test example
const testUserRepository = async () => {
  const { container, logs } = createTestContainer()
  
  const userRepo = container.get('userRepository')
  const user = await userRepo.findById('1')
  
  console.assert(user.name === 'Test User')
  console.assert(logs.some(log => log.includes('Finding user 1')))
  console.log('✅ Test passed!')
}

testUserRepository()

Example 3: Plugin Ecosystem with Configuration

Create reusable plugins that can be configured:

// Configurable database plugin
const createDatabasePlugin = (options: { 
  connectionString?: string
  poolSize?: number
  timeout?: number 
} = {}) => (container: any) => {
  container.bind('database', {
    resolveDependencies: createAutoResolver([
      { identifier: 'logger' }
    ]),
    provider: (deps) => {
      const config = {
        connectionString: options.connectionString || process.env.DB_URL || 'sqlite://memory',
        poolSize: options.poolSize || 10,
        timeout: options.timeout || 30000
      }
      
      deps.logger.info(`Initializing database with config: ${JSON.stringify(config)}`)
      
      return {
        query: async (sql: string, params?: any[]) => {
          deps.logger.debug(`Query: ${sql}`)
          // Database implementation here
          return []
        }
      }
    },
    scope: 'singleton'
  })
  return container
}

// Configurable cache plugin
const createCachePlugin = (options: {
  type?: 'memory' | 'redis'
  ttl?: number
  maxSize?: number
} = {}) => (container: any) => {
  container.bind('cache', {
    resolveDependencies: createAutoResolver([
      { identifier: 'logger' }
    ]),
    provider: (deps) => {
      const config = {
        type: options.type || 'memory',
        ttl: options.ttl || 3600000, // 1 hour
        maxSize: options.maxSize || 1000
      }
      
      deps.logger.info(`Initializing ${config.type} cache`)
      
      const store = new Map()
      
      return {
        get: async (key: string) => {
          deps.logger.debug(`Cache GET: ${key}`)
          return store.get(key)
        },
        set: async (key: string, value: any, ttl?: number) => {
          deps.logger.debug(`Cache SET: ${key}`)
          store.set(key, value)
          // In real implementation, handle TTL
        },
        delete: async (key: string) => {
          deps.logger.debug(`Cache DELETE: ${key}`)
          store.delete(key)
        }
      }
    },
    scope: 'singleton'
  })
  return container
}

// Use configured plugins
const prodContainer = new PreProcessDependencyContainerWithUse()
prodContainer.use(
  loggingPlugin,
  createDatabasePlugin({ 
    connectionString: 'postgres://prod-db',
    poolSize: 20 
  }),
  createCachePlugin({ 
    type: 'redis',
    ttl: 7200000 // 2 hours
  })
)

const devContainer = new PreProcessDependencyContainerWithUse()
devContainer.use(
  loggingPlugin,
  createDatabasePlugin({ 
    connectionString: 'sqlite://dev.db' 
  }),
  createCachePlugin({ 
    type: 'memory',
    maxSize: 100 
  })
)

Auto-Resolution Helper Functions

The library provides two powerful helper functions to simplify dependency resolution patterns:

createAutoResolver - Object-based dependency resolution

This function automatically resolves dependencies and returns them as an object mapped by their identifiers. Perfect when you want to access dependencies by name.

import { createAutoResolver, PreProcessDependencyContainer } from '@computerwwwizards/dependency-injection'

type Services = {
  database: { query: (sql: string) => any[] }
  logger: { log: (msg: string) => void }
  cache: { get: (key: string) => any; set: (key: string, value: any) => void }
  userService: { findUser: (id: string) => any }
}

const container = new PreProcessDependencyContainer<Services>()

// Register individual services
container.bindTo('database', () => ({ query: (sql) => [] }))
container.bindTo('logger', () => ({ log: console.log }))
container.bindTo('cache', () => ({ get: () => null, set: () => {} }))

// Use createAutoResolver to get dependencies as an object
container.bind('userService', {
  resolveDependencies: createAutoResolver([
    { identifier: 'database' },
    { identifier: 'logger' },
    { identifier: 'cache' }
  ]),
  provider: (deps) => {
    // deps is typed as { database: Database, logger: Logger, cache: Cache }
    return {
      findUser: (id: string) => {
        deps.logger.log(`Finding user ${id}`)
        
        // Check cache first
        const cached = deps.cache.get(`user:${id}`)
        if (cached) return cached
        
        // Query database
        const result = deps.database.query(`SELECT * FROM users WHERE id = '${id}'`)[0]
        deps.cache.set(`user:${id}`, result)
        
        return result
      }
    }
  }
})

const userService = container.get('userService')
const user = userService.findUser('123')

createAutoResolveDepsInOrder - Array-based dependency resolution

This function resolves dependencies in the specified order and returns them as an array. Useful for positional dependency injection patterns.

import { createAutoResolveDepsInOrder, PreProcessDependencyContainer } from '@computerwwwizards/dependency-injection'

type Services = {
  firstNumber: number
  secondNumber: number
  thirdNumber: number
  calculator: { sum: (a: number, b: number, c: number) => number }
}

const container = new PreProcessDependencyContainer<Services>()

container.bindTo('firstNumber', () => 10)
container.bindTo('secondNumber', () => 20)  
container.bindTo('thirdNumber', () => 30)

// Use createAutoResolveDepsInOrder for positional dependencies
container.bind('calculator', {
  resolveDependencies: createAutoResolveDepsInOrder([
    { identifier: 'firstNumber' },
    { identifier: 'secondNumber' },
    { identifier: 'thirdNumber' }
  ]),
  provider: (numbers) => {
    // numbers is [10, 20, 30] - array in the order specified
    const [first, second, third] = numbers
    return {
      sum: (a = first, b = second, c = third) => a + b + c
    }
  }
})

const calc = container.get('calculator')
console.log(calc.sum()) // 60 (10 + 20 + 30)

Optional Dependencies with Helper Functions

Both helper functions support optional dependencies that won't throw if missing:

// With createAutoResolver
container.bind('service', {
  resolveDependencies: createAutoResolver([
    { identifier: 'requiredDep' },
    { identifier: 'optionalDep', dontThrowIfNull: true }
  ]),
  provider: (deps) => {
    // deps.optionalDep might be undefined
    return new Service(deps.requiredDep, deps.optionalDep)
  }
})

// With createAutoResolveDepsInOrder  
container.bind('service', {
  resolveDependencies: createAutoResolveDepsInOrder([
    { identifier: 'requiredDep' },
    { identifier: 'optionalDep', dontThrowIfNull: true }
  ]),
  provider: ([required, optional]) => {
    // optional might be undefined
    return new Service(required, optional)
  }
})

Real-World Example: HTTP Service with Auto-Resolution

import { PreProcessDependencyContainerWithUse, createAutoResolver } from '@computerwwwizards/dependency-injection'

interface AppServices {
  config: { apiBaseUrl: string; timeout: number }
  logger: { info: (msg: string) => void; error: (msg: string) => void }
  httpClient: { get: (url: string) => Promise<any>; post: (url: string, data: any) => Promise<any> }
  userApi: { getUser: (id: string) => Promise<any>; createUser: (user: any) => Promise<any> }
}

const container = new PreProcessDependencyContainerWithUse<AppServices>()

// Configuration plugin
const configPlugin = (c: any) => {
  c.bindTo('config', () => ({
    apiBaseUrl: 'https://api.myapp.com',
    timeout: 5000
  }))
  return c
}

// Logging plugin
const loggingPlugin = (c: any) => {
  c.bindTo('logger', () => ({
    info: (msg: string) => console.log(`[INFO] ${msg}`),
    error: (msg: string) => console.error(`[ERROR] ${msg}`)
  }))
  return c
}

// HTTP client plugin - depends on config and logger
const httpPlugin = (c: any) => {
  c.bind('httpClient', {
    resolveDependencies: createAutoResolver([
      { identifier: 'config' },
      { identifier: 'logger' }
    ]),
    provider: (deps) => ({
      get: async (url: string) => {
        deps.logger.info(`GET ${url}`)
        try {
          const response = await fetch(`${deps.config.apiBaseUrl}${url}`, {
            timeout: deps.config.timeout
          })
          return response.json()
        } catch (error) {
          deps.logger.error(`GET ${url} failed: ${error}`)
          throw error
        }
      },
      post: async (url: string, data: any) => {
        deps.logger.info(`POST ${url}`)
        try {
          const response = await fetch(`${deps.config.apiBaseUrl}${url}`, {
            method: 'POST',
            body: JSON.stringify(data),
            headers: { 'Content-Type': 'application/json' },
            timeout: deps.config.timeout
          })
          return response.json()
        } catch (error) {
          deps.logger.error(`POST ${url} failed: ${error}`)
          throw error
        }
      }
    })
  })
  return c
}

// User API plugin - depends on httpClient and logger
const userApiPlugin = (c: any) => {
  c.bind('userApi', {
    resolveDependencies: createAutoResolver([
      { identifier: 'httpClient' },
      { identifier: 'logger' }
    ]),
    provider: (deps) => ({
      getUser: async (id: string) => {
        deps.logger.info(`Fetching user ${id}`)
        return deps.httpClient.get(`/users/${id}`)
      },
      createUser: async (user: any) => {
        deps.logger.info(`Creating user ${user.name}`)
        return deps.httpClient.post('/users', user)
      }
    })
  })
  return c
}

// Wire everything together
container.use(configPlugin, loggingPlugin, httpPlugin, userApiPlugin)

// Use the fully configured service
const userApi = container.get('userApi')
const user = await userApi.getUser('123')
const newUser = await userApi.createUser({ name: 'John', email: '[email protected]' })

When to use which helper function

  • Use createAutoResolver when you need dependencies as a named object and want clear, readable access to each dependency by name
  • Use createAutoResolveDepsInOrder when you have a small number of dependencies and prefer positional destructuring, or when working with legacy code that expects arrays

Both functions help eliminate boilerplate while maintaining type safety and clear dependency declarations.

Best Practices and Patterns

1. Use Type-Safe Service Registries

Define your service types upfront for better IntelliSense and type checking:

interface AppServices {
  // Configuration
  config: AppConfig
  secrets: SecretManager
  
  // Infrastructure  
  logger: Logger
  database: Database
  cache: CacheService
  queue: QueueService
  
  // Business logic
  userService: UserService
  orderService: OrderService
  paymentService: PaymentService
  
  // External integrations
  emailService: EmailService
  paymentGateway: PaymentGateway
}

const container = new PreProcessDependencyContainerWithUse<AppServices>()

2. Organize Dependencies into Layers

Structure your services in logical layers to avoid circular dependencies:

// Infrastructure layer (no dependencies on business logic)
const infrastructurePlugin = (c: any) => {
  c.bindTo('config', configFactory, 'singleton')
  c.bindTo('logger', loggerFactory, 'singleton')  
  c.bindTo('database', databaseFactory, 'singleton')
  return c
}

// Repository layer (depends on infrastructure)
const repositoryPlugin = (c: any) => {
  c.bind('userRepository', {
    resolveDependencies: createAutoResolver([
      { identifier: 'database' },
      { identifier: 'logger' }
    ]),
    provider: userRepositoryFactory
  })
  return c
}

// Service layer (depends on repositories and infrastructure)
const servicePlugin = (c: any) => {
  c.bind('userService', {
    resolveDependencies: createAutoResolver([
      { identifier: 'userRepository' },
      { identifier: 'emailService' },
      { identifier: 'logger' }
    ]),
    provider: userServiceFactory
  })
  return c
}

// Apply in correct order
container.use(infrastructurePlugin, repositoryPlugin, servicePlugin)

3. Use Factory Functions for Complex Service Creation

Keep your provider functions clean by extracting complex logic into factory functions:

// ❌ Inline complexity
container.bind('complexService', {
  resolveDependencies: createAutoResolver([...]),
  provider: (deps) => {
    // 50 lines of complex initialization...
    return new ComplexService(/* ... */)
  }
})

// ✅ Extract to factory function
const createComplexService = (deps: Dependencies) => {
  // Complex initialization logic
  const config = processConfiguration(deps.config)
  const connections = setupConnections(deps.database, config)
  const middleware = createMiddleware(deps.logger, config)
  
  return new ComplexService(connections, middleware, config)
}

container.bind('complexService', {
  resolveDependencies: createAutoResolver([...]),
  provider: createComplexService
})

4. Handle Optional Dependencies Gracefully

Use the dontThrowIfNull option for optional dependencies:

container.bind('emailService', {
  resolveDependencies: createAutoResolver([
    { identifier: 'config' },
    { identifier: 'logger' },
    { identifier: 'emailProvider', dontThrowIfNull: true } // Optional
  ]),
  provider: (deps) => {
    if (!deps.emailProvider) {
      // Fallback to console logging
      return {
        send: (to: string, subject: string, body: string) => {
          deps.logger.info(`Would send email to ${to}: ${subject}`)
        }
      }
    }
    
    return new EmailService(deps.emailProvider, deps.logger)
  }
})

5. Use Environment-Specific Containers

Create different container configurations for different environments:

const createContainer = (env: 'development' | 'test' | 'production') => {
  const container = new PreProcessDependencyContainerWithUse<AppServices>()
  
  // Common services
  container.use(loggingPlugin, authPlugin)
  
  // Environment-specific services
  switch (env) {
    case 'development':
      container.use(
        createDatabasePlugin({ connectionString: 'sqlite://dev.db' }),
        createCachePlugin({ type: 'memory' }),
        mockEmailPlugin // Don't send real emails in dev
      )
      break
      
    case 'test':
      container.use(
        mockDatabasePlugin,
        mockCachePlugin, 
        mockEmailPlugin
      )
      break
      
    case 'production':
      container.use(
        createDatabasePlugin({ connectionString: process.env.DATABASE_URL }),
        createCachePlugin({ type: 'redis', connectionString: process.env.REDIS_URL }),
        prodEmailPlugin
      )
      break
  }
  
  return container
}

const container = createContainer(process.env.NODE_ENV as any)

6. Handle Service Cleanup

Use the unbind method to clean up services when needed:

// Remove a specific service registration
container.unbind('temporaryService')

// For services that need cleanup, implement disposal pattern
interface DisposableService {
  dispose(): Promise<void>
}

const createService = (): DisposableService => {
  const connections = new Map()
  
  return {
    // service methods...
    dispose: async () => {
      for (const conn of connections.values()) {
        await conn.close()
      }
      connections.clear()
    }
  }
}

7. Avoid Common Anti-Patterns

❌ Don't capture external state in closures:

// Bad - captures external variable
const externalData = loadSomeData()
container.bindTo('service', () => new Service(externalData))

✅ Register dependencies explicitly:

// Good - explicit dependency
container.bindTo('data', () => loadSomeData(), 'singleton')
container.bind('service', {
  resolveDependencies: createAutoResolver([{ identifier: 'data' }]),
  provider: (deps) => new Service(deps.data)
})

❌ Don't create circular dependencies:

// Bad - A depends on B, B depends on A
container.bind('serviceA', {
  resolveDependencies: createAutoResolver([{ identifier: 'serviceB' }]),
  provider: (deps) => new ServiceA(deps.serviceB)
})

✅ Break circular dependencies with events or interfaces:

// Good - use event emitter or shared interface
container.bindTo('eventBus', () => new EventEmitter(), 'singleton')
container.bind('serviceA', {
  resolveDependencies: createAutoResolver([{ identifier: 'eventBus' }]),
  provider: (deps) => new ServiceA(deps.eventBus)
})

Strengths

  • Minimal code, easy to audit: The entire codebase is small and readable
  • No external dependencies: Zero npm deps beyond dev tools
  • Works everywhere: Browser, Node, and with module federation
  • No forced class patterns or decorators: Use functions, objects, whatever you want
  • You control dependency graph and lifecycle: Explicit is better than implicit
  • Modular plugin system: Use the use() method to create reusable, composable dependency modules
  • Flexible mixin pattern: Create custom container classes with createWithUse()

Child containers: ChildPrimitiveContainer

Create a child container that can fall back to a parent container for lookups. This is useful for scoping overrides (tests, feature flags, request-local data) while still inheriting common registrations from a shared parent.

Behavior highlights:

  • Lookups first check the child container's own registry.
  • If not found, the child will attempt to read from the parent container (if provided).
  • Child values can override parent registrations.
  • If a value is not found on either side, the child will throw unless the optional doNotThrowIfNull flag is passed.
import { ChildPrimitiveContainer, PrimitiveContainerWithUse } from '@computerwwwizards/dependency-injection'

// Parent with shared services
const parent = new PrimitiveContainerWithUse<{
  config: { apiUrl: string }
  logger: { log: (msg: string) => void }
}>()

parent.bindTo('config', () => ({ apiUrl: 'https://api.example.com' }), 'singleton')
parent.bindTo('logger', () => ({ log: console.log }), 'singleton')

// Child that inherits from parent but can override
const child = new ChildPrimitiveContainer(parent)

// Child override
child.bindTo('config', () => ({ apiUrl: 'https://staging.example.com' }), 'singleton')

// lookups prefer child
console.log(child.get('config').apiUrl) // 'https://staging.example.com'
console.log(child.get('logger')) // falls back to parent and returns logger

// optional lookup without throwing
console.log(child.get('nonExisting' as any, true)) // undefined

For test isolation, create a child container with test-specific overrides and assert behavior without mutating the parent.

Common patterns and tips

Modular plugins for reusability

// ✅ Create reusable plugins for common functionality
const databasePlugin = (dbUrl: string) => (container) => {
  container.bindTo('database', () => createDatabaseConnection(dbUrl), 'singleton')
  return container
}

const loggerPlugin = (level: string) => (container) => {
  container.bindTo('logger', () => createLogger(level), 'singleton')
  return container
}

// Use across different containers
const devContainer = new PrimitiveContainerWithUse()
devContainer.use(
  databasePlugin('sqlite://dev.db'),
  loggerPlugin('debug')
)

const prodContainer = new PrimitiveContainerWithUse()
prodContainer.use(
  databasePlugin('postgres://prod-db'),
  loggerPlugin('error')
)

Memory-friendly approach: Use the container context

// ❌ Problematic: captures external refs in closure
const largeData = loadHugeDataset()
container.bindTo('service', () => new Service(largeData))

// ✅ Better: register data in container, resolve via context
container.bindTo('data', () => loadHugeDataset(), 'singleton')
container.bind('service', {
  resolveDependencies: ctx => ({ data: ctx.get('data') }),
  provider: deps => new Service(deps.data)
})

Error handling

// The container throws if a dependency isn't found
try {
  const missing = container.get('notRegistered')
} catch (error) {
  console.log(error.message) // "Could not resolve notRegistered, did you register it?"
}

// Or use the optional flag to get undefined instead
const maybeValue = container.get('notRegistered', true) // returns undefined

Roadmap

  • [x] Plugin/middleware pattern with use() method
  • [x] Mixin creator function createWithUse()
  • [ ] Error handling strategies (circular dep detection, error callbacks)
  • [ ] Lifecycle hooks (init/dispose, lazy activation)
  • [ ] Child container creation (inherit/override bindings)
  • [ ] WeakMap support for stateless dep reuse
  • [ ] Benchmarks and performance comparisons
  • [ ] Garbage collection inspections
  • [ ] Document antipatterns that lead to memory leaks
  • [ ] Add more examples and recipes

Development

Run tests and checks from the package directory:

pnpm --filter @computerwwwizards/dependency-injection test
pnpm --filter @computerwwwizards/dependency-injection check

License

MIT