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 🙏

© 2026 – Pkg Stats / Ryan Hefner

inwire

v3.0.0

Published

Type-safe dependency injection for TypeScript. Zero ceremony, full inference, no decorators, no tokens. ~4KB gzip, built-in introspection for AI tooling.

Readme

inwire

Type-safe dependency injection for TypeScript. No decorators. No tokens. No reflect-metadata. Just a fluent builder, a Proxy, and full type inference. ~4 KB gzip, zero runtime dependencies.

NPM Version CI Bundle size NPM Downloads TypeScript License Zero Dependencies

import { container } from 'inwire';

const app = container()
  .add('logger', () => new Logger())
  .add('db', (c) => new Database(c.logger))      // c.logger is typed
  .add('users', (c) => new UserService(c.db))    // c.db is typed
  .build();

app.users.findById('42'); // lazy, singleton, fully typed

Why inwire?

| | inwire | typical DI container | |---|---|---| | Type inference | Full — c.db autocompletes from .add() history | Manual generics or token strings | | Decorators | None | Required (@Injectable, @Inject) | | Runtime metadata | None | reflect-metadata polyfill needed | | Circular deps | Caught with full chain + fix hint | Stack overflow or cryptic crash | | Async lifecycle | First-class preload() with topological parallelism | Manual Promise.all plumbing | | Introspection | inspect() returns JSON graph for LLMs / dashboards | None | | Bundle size | ~4 KB gzip | 10–50 KB | | Runtime | Pure ES2022 — Node, Bun, Deno, Workers, browsers | Often Node-only |

The dependency graph is a side product: a tracking Proxy records which keys each factory accesses, so inspect() returns the real graph without you ever annotating it.


Install

pnpm add inwire   # or npm i inwire / bun add inwire

Requires TypeScript ≥ 5.0 and an ESM-aware bundler / runtime.


Modular Setup (recommended)

For real-world apps, organize bindings per module file with Pinia-style global type augmentation. Each file declares what it provides by augmenting AppDeps; defineModule() types the factory's c against the merged interface — cross-module references resolve regardless of import order.

// modules/persistence.module.ts
import { defineModule } from 'inwire';
import type { IUserRepository } from '../contracts/IUserRepository';
import { DrizzleUserRepository } from '../infrastructure/DrizzleUserRepository';

declare module 'inwire' {
  interface AppDeps {
    IUserRepository: IUserRepository;
  }
}

export const persistenceModule = defineModule()((b) =>
  b.add('IUserRepository', (): IUserRepository => new DrizzleUserRepository()),
);
// modules/auth.module.ts
import { defineModule } from 'inwire';
import type { IAuthProvider } from '../contracts/IAuthProvider';
import { BetterAuthProvider } from '../infrastructure/BetterAuthProvider';
import { SignInUseCase } from '../application/SignInUseCase';

declare module 'inwire' {
  interface AppDeps {
    IAuthProvider: IAuthProvider;
    SignInUseCase: SignInUseCase;
  }
}

export const authModule = defineModule()((b) =>
  b
    .add('IAuthProvider', (): IAuthProvider => new BetterAuthProvider())
    .add('SignInUseCase', (c) => new SignInUseCase(c.IUserRepository, c.IAuthProvider)),
  //                                              ^^^^^^^^^^^^^^^^^^^^
  //                       provided by persistenceModule — typed via merged AppDeps
);
// container.ts — single source of truth
import { container } from 'inwire';
import { persistenceModule } from './modules/persistence.module';
import { authModule } from './modules/auth.module';

export const di = container()
  .addModule(persistenceModule)
  .addModule(authModule)
  .build();

export type Di = typeof di; // derived — never hand-written

Why this scales:

  • Locality. Each module is self-contained: it states what it provides, in its own file. No global shape interface to maintain.
  • Order-independent. authModule references c.IUserRepository even if persistenceModule is added later, in any file.
  • Familiar pattern. Mirrors Pinia's PiniaCustomProperties and Vue's ComponentCustomProperties. Augmentations are erased after type-check — zero runtime cost.
  • Derived types. type Di = typeof di — add a binding, Di grows; remove one, it shrinks. The compiler does the bookkeeping.

Other patterns are supported when this one doesn't fit — see Modules reference.


Core Concepts

The container is a Proxy

container() returns a fluent builder. Each .add(key, factory) accumulates the type so the next factory's c argument is typed with everything declared so far. .build() wraps the factories in an ES Proxy: property access triggers lazy resolution and caches the result.

const app = container()
  .add('db', () => new Database())
  .build();

app.db; // first access → factory runs, instance cached
app.db; // subsequent access → cached instance returned

Auto-tracked dependency graph

The c argument passed to each factory is itself a tracking Proxy. Every property access is recorded — that's how inspect() returns the real graph without you annotating it.

const app = container()
  .add('db', () => new Database())
  .add('repo', (c) => new UserRepo(c.db))   // c.db touched → graph: repo → [db]
  .build();

app.inspect();
// { providers: { db: { deps: [], ... }, repo: { deps: ['db'], ... } } }

Singleton by default, transient on demand

const app = container()
  .add('db', () => new Database())                 // singleton (cached)
  .addTransient('requestId', () => crypto.randomUUID())  // transient (fresh each access)
  .build();

app.db === app.db;               // true
app.requestId === app.requestId; // false

For scope() and extend(), use the transient() wrapper:

import { transient } from 'inwire';

const scoped = app.extend({
  timestamp: transient(() => Date.now()),
});

Eager instances

A non-function value passed to .add() is registered eagerly (wrapped in () => value):

container()
  .add('config', { port: 3000 })            // eager — `{ port: 3000 }` is the value
  .add('db', (c) => new Database(c.config)) // lazy — function = factory
  .build();

To register a function as a value, wrap it: .add('handler', () => myFunction).

Lifecycle (duck-typed)

Implement onInit() / onDestroy() on any class. inwire detects them at runtime — no base class required.

import type { OnInit, OnDestroy } from 'inwire';

class Database implements OnInit, OnDestroy {
  async onInit()    { await this.connect(); }
  async onDestroy() { await this.disconnect(); }
}

CRITICAL gotcha — sync property access cannot await. When you access app.db, onInit() is called but not awaited. Async errors are silently captured as health().warnings. To safely await async startup, use preload().


Cookbook

Async startup — preload()

preload() is the only way to safely await async onInit(). It runs independent branches in parallel using a topological sort (Kahn's BFS), levels sequentially:

Level 0:  [config]            ← no deps
Level 1:  [db] [cache]        ← parallel, both depend on config
Level 2:  [api]               ← depends on db + cache
await app.preload('db', 'cache'); // specific keys
await app.preload();              // everything

Errors from onInit() propagate as a single AggregateError if multiple fail. Wrap in try/catch for startup validation.

Canonical boot sequence. For any non-trivial app with async init (DB connections, queue workers, cache warmers), the recommended pattern is:

// 1. Build the container — no I/O happens here, just factory registration.
const app = container()
  .add('config', () => loadConfig())
  .add('db', (c) => new Database(c.config))         // implements OnInit (connect())
  .add('cache', (c) => new Redis(c.config))         // implements OnInit (connect())
  .add('queue', (c) => new QueueConsumer(c.db))     // implements OnInit (start consuming)
  .build();

// 2. Preload — runs onInit() in parallel where possible, surfaces errors.
try {
  await app.preload();
} catch (err) {
  console.error('Boot failed', err); // AggregateError if multiple onInit() rejected
  process.exit(1);
}

// 3. Wire shutdown — LIFO onDestroy() in reverse resolution order.
for (const sig of ['SIGTERM', 'SIGINT'] as const) {
  process.on(sig, async () => {
    await app.dispose();
    process.exit(0);
  });
}

// 4. App is ready.
app.queue.start();

Without preload(), async onInit() errors are silently captured as health().warnings — fine for hot reloads, dangerous for production boot.

Per-request scopes

scope() creates a child container with extra bindings. The child inherits parent singletons via a parent-resolver chain; scoped bindings are isolated per scope.

const request = app.scope(
  {
    requestId: () => crypto.randomUUID(),
    handler: (c) => new Handler(c.logger, c.requestId), // c is typeof app
  },
  { name: 'request-123' }, // optional, surfaces in inspect()/toString()
);

request.requestId; // unique per scope
request.logger;    // shared from parent

Test overrides

Two patterns — pick what fits your test:

From scratch — build a parallel container with mocks:

function createTestContainer() {
  return container()
    .add('logger', () => ({ log: () => {} }))    // silent
    .add('db',     () => new InMemoryDatabase()) // mock
    .add('users',  (c) => new UserService(c.db, c.logger))
    .build();
}

From a real container — override specific keys via .extend():

// production container is the source of truth
const testApp = realApp.extend({
  db: () => new InMemoryDatabase(),     // override
  emailService: () => ({ send: vi.fn() }), // mock
});

// All other bindings (loggers, repos, services...) come from realApp untouched.
testApp.users.signup({ email: '[email protected]' });

.extend() shares the parent's singleton cache, so already-resolved real instances are reused. Overridden keys get fresh factories. This is the most ergonomic way to mock a slice of a real container without rebuilding the whole graph.

Plugin system — extend()

extend() returns a new container with additional bindings. Unlike scope(), the existing singleton cache is shared — already-resolved instances are reused.

const withCsv = core.extend({
  csvParser: (c) => new CsvParser(c.logger),
});

const app = withCsv.extend({
  jobRunner: transient((c) => new JobRunner(c.csvParser)),
});

| | scope() | extend() | |---|---|---| | Topology | Parent-child chain | Flat merged container | | Cache | Independent per-scope cache | Shares parent's resolved cache | | Use for | Per-request isolation | Additive composition / plugins |

Graceful shutdown — dispose()

Calls onDestroy() on all resolved instances in LIFO order. Resilient: continues on errors, collects them into AggregateError.

process.on('SIGTERM', async () => {
  await app.dispose();
  process.exit(0);
});

ES2023 explicit resource management — every container implements [Symbol.asyncDispose], so await using auto-disposes when the binding leaves scope:

async function handleRequest(req: Request) {
  await using request = app.scope({
    requestId: () => crypto.randomUUID(),
    handler: (c) => new Handler(c.logger, c.requestId),
  });
  return request.handler.run(req);
} // request.dispose() fires automatically here, even on throw

Requires TypeScript ≥ 5.2 and a runtime with Symbol.asyncDispose (Node ≥ 20.4, Bun, Deno).

Resetting cached singletons

app.db;             // creates instance
app.reset('db');    // invalidates cache for a specific key
app.db;             // creates a NEW instance (factory re-runs, onInit re-fires)

app.reset();        // no args → invalidates ALL cached singletons in this scope

reset() is scope-local — it doesn't affect parent caches. The no-arg variant also clears initState, the recorded dependency graph, and any captured warnings — useful for fully rebuilding state in long-running tests.

Introspection for AI / observability

app.inspect();         // ContainerGraph — full dependency graph (JSON)
app.describe('users'); // ProviderInfo for one binding
app.health();          // { totalProviders, resolved, unresolved, warnings }
String(app);           // human-readable one-liner
const graph = JSON.stringify(app.inspect(), null, 2);
// Pipe to an LLM, render in a dashboard, diff in CI.

Modules reference

inwire offers four ways to compose modules. Pinia-style is the recommended default; the rest fit specific situations.

Pinia-style augmentation — recommended

See Modular Setup above for the full recipe. TL;DR:

declare module 'inwire' {
  interface AppDeps { IUserRepository: IUserRepository }
}

export const persistenceModule = defineModule()((b) =>
  b.add('IUserRepository', (): IUserRepository => new DrizzleUserRepository()),
);
  • Each module declares what it provides.
  • c is typed as the merged AppDeps.
  • Cross-module forward references work, order-independent.

defineModule<TDeps>() — locally-declared prerequisites

When a module's prereqs are a tight, fixed surface and you'd rather not augment a global, declare what the module consumes inline. c is typed locally as TDeps:

const dbModule = defineModule<{ logger: Logger }>()((b) =>
  b
    .add('db',    (c) => new Database(c.logger))
    .add('cache', (c) => new Redis(c.logger)),
);

Trade-offs vs Pinia-style:

| Pattern | Declares | Cross-module forward ref | Global state | |---|---|---|---| | Pinia-style (defineModule() + declare module) | what the module provides | yes — order-independent | augments inwire's AppDeps | | Local (defineModule<TDeps>()) | what the module consumes | no — prereqs added first | none |

Both modes coexist: passing <TDeps> always overrides the global mode for that module.

Why the double-call signature defineModule<TDeps>()(fn)? TypeScript's generic inference is all-or-nothing — specifying <TDeps> in a flat single-call signature would force you to write <TBuilt> by hand too, defeating the inference of the .add() chain. The curry splits the two: first call fixes TDeps (or defaults to AppDeps), second call infers TBuilt from the factory return. Same workaround used by zod, TanStack Query, RTK. Tracking microsoft/TypeScript#26242.

addModule() does not enforce prereq satisfaction at the type level — missing keys raise ProviderNotFoundError at resolution time. This relaxation is what makes Pinia-style forward references possible.

.merge() — fuse standalone builders

When a module has no prerequisites, define it as a plain builder and merge it:

const dbModule = container()
  .add('db', () => new Database())
  .add('cache', (c) => new Redis(c.db));

const app = container()
  .add('logger', () => new Logger())
  .merge(dbModule)
  .add('api', (c) => new Api(c.db, c.logger))
  .build();

Cross-builder dependencies are resolved at build time. Duplicate keys override (last write wins). Reserved keys throw.

Post-build — container.module()

Compose post-build using the same builder DX. Each .add() in the callback types c incrementally:

const core = container().add('logger', () => new Logger()).build();

const withDb = core.module((b) =>
  b.add('db', (c) => new Database(c.logger)),
);

const full = withDb.module((b) =>
  b.add('users', (c) => new UserService(c.db, c.logger)),
);

module() works on scope() and extend() results too. Internally it delegates to extend() after building the typed factory record.

Anti-pattern (avoid)

Older code may show this manual generic — verbose, couples the module to a global AppDeps, forces redeclaring prerequisites:

// ✗ Don't do this — use defineModule() instead.
function dbModule<T extends { logger: Logger }>(
  b: ContainerBuilder<AppDeps, T>,
) {
  return b.add('db', (c) => new Database(c.logger));
}

Contract Mode (single-file containers)

For monolithic, single-file containers (no modules), pass an interface to container<T>() to constrain keys and return types at compile time:

interface AppDeps {
  ILogger: Logger;
  IDatabase: Database;
  IUserService: UserService;
}

const app = container<AppDeps>()
  .add('ILogger',      () => new ConsoleLogger())          // key: keyof AppDeps
  .add('IDatabase',    (c) => new PgDatabase(c.ILogger))   // return must match Database
  .add('IUserService', (c) => new UserService(c.IDatabase, c.ILogger))
  .build();

app.ILogger; // typed as Logger (interface), not ConsoleLogger

The string key acts as a token (à la NestJS) but is type-safe at compile time. For multi-module apps, use Pinia-style instead — it scales across files; Contract Mode does not.


Errors & Diagnostics

Every error extends ContainerError and carries:

  • hint: string — actionable fix suggestion
  • details: Record<string, unknown> — structured context for programmatic consumption

Designed to be parsed by both humans and LLMs.

Fuzzy missing-key suggestions

app.userServce; // typo
// ProviderNotFoundError: Cannot resolve 'userServce'.
//   Registered: [userService, logger, db]
//   Did you mean 'userService'?
//   hint: Add 'userServce' to your container, or fix the typo.

Powered by Levenshtein distance (≥ 50% similarity threshold).

Circular dependency — full chain

// CircularDependencyError: Circular dependency detected while resolving 'authService'.
//   Cycle: authService → userService → authService

No stack overflow, no cryptic crash — just the resolution chain.

Reserved keys

scope, extend, module, preload, reset, inspect, describe, health, dispose, toString cannot be used as dependency keys.

container().add('inspect', () => 'foo');
// ReservedKeyError: 'inspect' is a reserved container method.
//   hint: Rename, e.g. 'inspectService' or 'myInspect'.

Scope mismatch detection (warning)

A singleton depending on a transient freezes the transient value. Surface via health():

app.health().warnings;
// [{
//   type: 'scope_mismatch',
//   message: "Singleton 'userService' depends on transient 'requestId'.",
//   details: { singleton: 'userService', transient: 'requestId' },
// }]

Async-init errors (warning)

When onInit() rejects during lazy access (no preload()), the rejection is captured as a warning rather than crashing your app.

app.health().warnings;
// [{ type: 'async_init_error', message: "onInit() for 'db' rejected: connection refused", ... }]

Use preload() to surface these as proper errors.

Duplicate keys

.add() and .addTransient() throw DuplicateKeyError if the key is already registered — no silent overwrites:

container()
  .add('logger', () => new ConsoleLogger())
  .add('logger', () => new FileLogger()); // throws DuplicateKeyError

For intentional overrides (test doubles, plugins, environment-specific bindings), use .extend() or .scope() on a built container — both are documented override mechanisms.

All error types

| Error | Thrown when | |---|---| | ContainerError | Base class for all errors. Every subclass carries hint + details. | | ContainerConfigError | Non-function value passed to scope() / extend() deps | | ReservedKeyError | Reserved method name used as a key | | DuplicateKeyError | .add() or .addTransient() called twice with the same key | | ProviderNotFoundError | Key not registered (with fuzzy suggestion) | | CircularDependencyError | Cycle detected during resolution | | UndefinedReturnError | Factory returned undefined | | FactoryError | Factory threw (wraps original error) | | ScopeMismatchWarning | Singleton depends on transient (surfaced via health().warnings). Carries hint with refactor suggestions. | | AsyncInitErrorWarning | Async onInit() rejected during lazy access (surfaced via health().warnings). Carries hint pointing to preload(). |


Examples

| Example | Run | Showcases | |---|---|---| | 06-pinia-augmentation.ts ★ | npm run example:pinia | Recommended modular pattern. declare module 'inwire' per file, order-independent cross-module typing | | 05-zod-style-typing.ts | npm run example:typing | type Di = typeof di derivation, Clean Arch contracts | | 04-modules.ts | npm run example:modules | defineModule<TDeps>(), .merge(), module() post-build | | 03-plugin-system.ts | npm run example:plugin | Extend chain, scoped jobs, JSON graph for LLM | | 02-modular-testing.ts | npm run example:test | Free mode, instance values, test overrides | | 01-web-service.ts | npm run example:web | Contract mode, lifecycle, dependency inversion |


API Reference

Functions & classes

| Export | Kind | Description | |---|---|---| | container<T?>() | function | Creates a ContainerBuilder. Pass T for Contract Mode. | | ContainerBuilder | class | Fluent builder class (rarely instantiated directly — container() is the entry point). Exported for type-only use and advanced composition. | | defineModule<TDeps?>()(fn) | function | Defines a typed reusable module. See Modules reference. | | transient(factory) | function | Marks a factory as transient (for scope() / extend()). |

Builder methods

| Method | Description | |---|---| | .add(key, factoryOrInstance) | Register a binding. Function = lazy factory; non-function = eager instance. | | .addTransient(key, factory) | Register a transient binding (fresh each access). | | .addModule(module) | Apply a Module (typically from defineModule()). | | .merge(otherBuilder) | Fuse a standalone builder's factories into this one. | | .build() | Build and return the container. |

Container methods

| Method | Description | |---|---| | .scope(extra, options?) | Child container with additional deps. Inherits parent singletons via parent chain. | | .extend(extra) | New container with additional deps. Shares singleton cache. | | .module(fn) | Post-build ContainerBuilder for typed c accumulation. Delegates to extend(). | | .preload(...keys) | Eagerly resolve and await onInit(). No args = preload all. | | .reset(...keys) | Invalidate cached singletons. Scope-local. | | .inspect() | Full dependency graph (ContainerGraph). | | .describe(key) | Single binding info (ProviderInfo). | | .health() | Health snapshot + warnings (ContainerHealth). | | .dispose() | LIFO onDestroy() on all resolved instances. | | [Symbol.asyncDispose]() | Alias of .dispose() — enables await using container = ... (ES2023). | | .size | readonly number — count of registered providers. | | .toJSON() | Plain object of currently resolved (cached) deps. Does not trigger lazy resolution. Makes JSON.stringify(container) work. | | [Symbol.iterator]() | Yields [key, value] pairs for every registered provider. Triggers lazy resolution. Enables for...of, spread, Array.from. |

Types

| Type | Description | |---|---| | AppDeps | Augmentable global interface for Pinia-style typing. | | Container<T> | T & IContainer<T> — resolved deps + container methods. | | ContainerBuilder<TContract, TBuilt> | Fluent builder (also passed to module() callbacks). | | IContainer<T> | Container methods interface. | | Module<TDeps, TBuilt> | Module shape returned by defineModule(). | | InferModuleDeps<M> / InferModuleBuilt<M> | Extract a module's prereqs / full output. | | Factory<T> | Raw factory signature (c: unknown) => T. | | OnInit / OnDestroy | Lifecycle interfaces (duck-typed). | | ContainerGraph | Return of inspect(){ name?, providers }. | | ContainerHealth | Return of health(){ totalProviders, resolved, unresolved, warnings }. | | ContainerWarning | { type: 'scope_mismatch' \| 'async_init_error', message, details }. | | ProviderInfo | Return of describe(){ key, resolved, deps, scope }. | | ScopeOptions | { name?: string }. |


Architecture

Clean Architecture with an enforced one-way dependency rule.

src/
  index.ts                       # public barrel — only file consumers see
  domain/                        # pure contracts — no framework deps
    types.ts                     # barrel re-exporting types/public.ts + types/internal.ts
    types/public.ts              # Container, IContainer, IContainerBuilder, AppDeps, helpers
    types/internal.ts            # IResolver, ICycleDetector, IDependencyTracker, IValidator
    errors.ts                    # 7 error classes + 2 warnings, each with hint + details
    lifecycle.ts                 # OnInit / OnDestroy (duck-typed)
    validation.ts                # Validator (configurable similarity threshold), Levenshtein
  infrastructure/                # mechanisms — depends on domain/ only
    resolver.ts                  # lazy resolution, singleton cache, parent chain
    cycle-detector.ts            # circular dependency detection
    dependency-tracker.ts        # tracking Proxy + auto-built dependency graph
    transient.ts                 # transient() marker (Symbol.for-based)
  application/                   # orchestration — depends on domain/ + infrastructure/
    container-builder.ts         # ContainerBuilder + container() factory  ▸ Composition Root
    container-proxy.ts           # Proxy construction + dispatch            ▸ Composition Root
    scoper.ts                    # builds child resolvers for .scope()     ▸ Composition Root
    extender.ts                  # builds merged resolvers for .extend()   ▸ Composition Root
    define-module.ts             # defineModule() — both modes
    preloader.ts                 # topological sort (Kahn) + parallel onInit
    disposer.ts                  # reverse-order onDestroy + resilient errors
    introspection.ts             # inspect / describe / health / toString

The Resolver receives its collaborators via constructor injection — no internal new, no hidden coupling. Application code depends on IResolver, never on the concrete class. The four Composition Roots (container-builder.ts, container-proxy.ts, scoper.ts, extender.ts) are the only files allowed to instantiate concrete infrastructure (Resolver, CycleDetector, DependencyTracker).


LLM / AI Integration

This package ships llms.txt files for AI-assisted development:

  • llms.txt — Concise index following the llms.txt standard
  • llms-full.txt — Complete API reference optimized for LLM context windows

Compatible with Context7 and any tool that supports the llms.txt standard. The inspect() output is also designed to be piped directly into an LLM for architecture analysis.


License

MIT