Sigitex/bind

stable

A lightweight dependency injection container.

GITHUB ↗

DOCS

bind

A lightweight dependency injection container.

bun add @sigitex/bind

Note: This package currently exports TypeScript sources directly. A TypeScript-compatible runtime or bundler (Bun, etc.) is required.

Quick Start

import { bind, factory, singleton, constructor } from "@sigitex/bind"

const container = bind({
  dbUrl: "postgres://localhost/mydb",
  db: factory(({ dbUrl }) => createPool(dbUrl)),
  userService: factory(({ db }) => new UserService(db)),
})

const userService = container.resolve<UserService>("userService")

API

bind(bindings)

Creates a new Container with the given bindings. This is a shorthand for new Container().bind(bindings).

const container = bind({
  port: 3000,
  host: "localhost",
})

Container

The core class. Manages bindings and resolves dependencies lazily (instances are created on first access and then cached).

container.bind(bindings)

Registers bindings. Plain values are wrapped in a ValueBinding automatically. Returns the container for chaining.

container.bind({
  logger: factory(({ config }) => new Logger(config)),
  config: { level: "debug" },
})

Re-binding a key clears its cached instance, so subsequent resolves will use the new binding.

container.resolve<T>(key)

Resolves a binding by key. Throws if the key is not found.

const logger = container.resolve<Logger>("logger")

The special key "container" always resolves to the container itself.

container.createInjector<T>()

Returns a proxy object that lazily resolves dependencies on property access. This is how dependencies are injected into factories and constructors.

type Deps = { db: Database; logger: Logger }
const deps = container.createInjector<Deps>()
// deps.db triggers resolve("db") on first access

container.call(fn)

Calls a function with an injector as its first argument.

container.call(({ db, logger }) => {
  logger.info("connected")
  return db.query("SELECT 1")
})

container.new(Constructor)

Instantiates a class, passing an injector to the constructor.

class UserService {
  constructor({ db, logger }: Deps) { /* ... */ }
}

const service = container.new(UserService)

container.clone()

Creates a new container with the same bindings (but fresh instance cache). Useful for per-request scoping.

async function handleRequest(request: Request) {
  const requestContainer = container.clone()
  requestContainer.bind({ identity: await authenticate(request) })
  // ...
}

Binding Types

Plain values

Any value that is not a Binding instance is treated as a value binding:

bind({ port: 3000, name: "my-app" })

value(v)

Explicit value binding (equivalent to passing a plain value):

import { value } from "@sigitex/bind"
bind({ port: value(3000) })

factory(fn)

Creates the instance by calling fn with an injector. A new instance is created each time the container resolves the key (though the container caches the result after the first resolve).

import { factory } from "@sigitex/bind"

bind({
  db: factory(({ connectionString }) => new Pool(connectionString)),
})

constructor(Class)

Like factory, but calls new Class(injector) instead.

import { constructor } from "@sigitex/bind"

class EmailService {
  constructor({ smtp, templates }: Deps) { /* ... */ }
}

bind({ emailService: constructor(EmailService) })

singleton(binding)

Wraps any other binding so that its instance persists across container clones and outlives any single container. Useful for expensive resources that should be shared globally (connection pools, etc.).

import { factory, singleton } from "@sigitex/bind"

bind({
  pool: singleton(factory(({ dbUrl }) => new Pool(dbUrl))),
})

Resolution Behavior

Usage with Frameworks

The container is designed to be cloned per-request so each request gets its own scope while sharing the base bindings:

const baseContainer = bind({
  db: singleton(factory(() => createPool())),
  userRepo: factory(({ db }) => new UserRepo(db)),
})

// Per-request:
const container = baseContainer.clone()
container.bind({ identity: currentUser })