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

@romikus/dimpl

v1.0.0

Published

Simple dependency-injection library

Readme

dimpl

Simple dependency injection for TypeScript and JavaScript.

  • Works with functions and classes.
  • Dependencies stay visible in plain code.
  • No decorators.
  • No metadata reflection.
  • No class-only container model.

dimpl treats a function or class as the dependency key. The first time you ask for it, the container creates an instance and caches it. Every later lookup in the same container returns the same instance.

import { dimpl } from "dimpl";

const di = dimpl();

const Config = () => ({
  dbUrl: process.env.DATABASE_URL ?? "postgres://localhost/app",
});

const Db = (config = di.get(Config)) => {
  return connect(config.dbUrl);
};

const Users = ({ db } = di.deps({ db: Db })) => {
  return {
    findById(id: string) {
      return db.user.findUnique({ where: { id } });
    },
  };
};

const users = di.get(Users);

Installation

npm install @romikus/dimpl

Motivation

With dimpl, dependencies are just default parameters:

const UsersService = (
  { db, logger } = di.deps({
    db: Db,
    logger: Logger,
  }),
) => {
  return {
    list() {
      logger.info("Listing users");
      return db.query("select * from users");
    },
  };
};

Single dependency:

const StripeClient = (config = di.get(Config)) => {
  return new Stripe(config.stripeSecretKey);
};

Class dependency:

class Clock {
  now() {
    return new Date();
  }
}

const Tokens = ({ clock } = di.deps({ clock: Clock })) => {
  return {
    issue() {
      return { issuedAt: clock.now() };
    },
  };
};

Startup side effects:

const app = di.get(App);

di.wire(HealthController, AuthController, UsersController);

serve({ fetch: app.fetch, port: 3000 });

Controllers can register routes when they are instantiated. Services can stay lazy until something needs them.

request-scoped lifetime

dimpl is designed only around a singleton runtime. Every dependency resolved by a container is cached as one instance for that container.

Disregarding of a DI library, AsyncLocalStorage is a much more convenient way of handling request-scoped or more granular scoped dependencies anyway.

You can use a dimpl singleton instance of AsyncLocalStorage to retrieve request-scoped dependencies from it.

For example, imagine a pino logger is request-scoped. When a request starts, you create a child logger with userId. Services use that child logger without becoming request-scoped themselves.

import { AsyncLocalStorage } from "node:async_hooks";
import pino, { type Logger } from "pino";

type RequestScope = {
  logger: Logger;
};

const RootLogger = () => pino();

const RequestScope = () => {
  return new AsyncLocalStorage<RequestScope>();
};

const RequestLogger = (
  { rootLogger, requestScope } = di.deps({
    rootLogger: RootLogger,
    requestScope: RequestScope,
  }),
) => {
  return {
    get() {
      return requestScope.getStore()?.logger ?? rootLogger;
    },
  };
};

const OrdersService = (
  { logger } = di.deps({
    logger: RequestLogger,
  }),
) => {
  return {
    createOrder(userId: string) {
      logger.get().info({ userId }, "Creating order");
      // ...
    },
  };
};

const requestScope = di.get(RequestScope);
const rootLogger = di.get(RootLogger);
const ordersService = di.get(OrdersService);

app.use(async (req, res, next) => {
  const userId = await getUserId(req);
  const logger = rootLogger.child({ userId });

  requestScope.run({ logger }, () => {
    next();
  });
});

app.post("/orders", async (req, res) => {
  const userId = await getUserId(req);
  ordersService.createOrder(userId);
  res.sendStatus(201);
});

The container still has singleton services. The request-specific value lives in AsyncLocalStorage, and services read it at call time.

Async Constructors

dimpl does not support async constructors in any special way for now. This keeps the library simple, and async dependency construction is rare enough that it may not be worth adding container-level machinery for it.

When a dependency must be awaited before use, do that in your app setup and seed the resolved instance with di.set.

For example, imagine an AMQP library requires an async connection before anything can publish messages:

import { connect, type Channel } from "amqplib";

export const Amqp = (): Channel => {
  throw new Error("amqp instance must be set in the setup");
};

export const NotificationsService = (amqp = di.get(Amqp)) => {
  return {
    async sendWelcomeEmail(userId: string) {
      await amqp.sendToQueue("emails", Buffer.from(JSON.stringify({ type: "welcome", userId })));
    },
  };
};

export async function setup() {
  const connection = await connect(process.env.AMQP_URL);
  const channel = await connection.createChannel();

  di.set(Amqp, channel);

  return {
    notificationsService: di.get(NotificationsService),
  };
}

The throwing factory is just the dependency key and type source. If setup forgets to provide the real instance, the app fails with a clear error.

Unit Testing

Because dependencies are normal function parameters, the simplest test does not need a container at all. Pass every dependency explicitly.

import { describe, expect, it, vi } from "vitest";

const AuthService = (
  { users, tokens } = di.deps({
    users: UsersRepo,
    tokens: TokenService,
  }),
) => {
  return {
    async login(email: string, password: string) {
      const user = await users.verifyPassword(email, password);
      return tokens.sign(user.id);
    },
  };
};

it("logs in a user", async () => {
  const users = {
    verifyPassword: vi.fn().mockResolvedValue({ id: "user_1" }),
  };

  const tokens = {
    sign: vi.fn().mockReturnValue("jwt"),
  };

  const auth = AuthService({ users, tokens });

  await expect(auth.login("[email protected]", "secret")).resolves.toBe("jwt");
  expect(users.verifyPassword).toHaveBeenCalledWith("[email protected]", "secret");
  expect(tokens.sign).toHaveBeenCalledWith("user_1");
});

Use di.set when you want most of the dependency graph to stay real, but one or two dependencies should be mocked. di.deps will keep resolving everything normally, and the dependencies you seeded with di.set will be returned instead of being created.

it("uses a real dependency graph with one mocked dependency", async () => {
  const di = dimpl();

  const Db = () => connect(process.env.DATABASE_URL);

  const Logger = () => ({
    info: vi.fn(),
  });

  const UsersRepo = (
    { db, logger } = di.deps({
      db: Db,
      logger: Logger,
    }),
  ) => ({
    async findById(id: string) {
      logger.info(`Loading user ${id}`);
      return db.users.findById(id);
    },
  });

  const fakeDb = {
    users: {
      findById: vi.fn().mockResolvedValue({ id: "user_1" }),
    },
  };

  di.set(Db, fakeDb);

  const users = di.get(UsersRepo);

  await expect(users.findById("user_1")).resolves.toEqual({ id: "user_1" });
  expect(fakeDb.users.findById).toHaveBeenCalledWith("user_1");
  expect(di.get(Logger).info).toHaveBeenCalledWith("Loading user user_1");
});

Example App

See example/ for a sample API using dimpl.

It shows:

  • example/src/di.ts: exporting one app container.
  • example/src/server.ts: getting the app and wiring controllers.
  • example/src/modules/auth/auth.controller.ts: injecting multiple dependencies with di.deps.
  • example/src/infrastructure/db.ts: injecting a single dependency with di.get.
import { dimpl } from "dimpl";

Creating a Container

import { dimpl } from "dimpl";

export const di = dimpl();

Each call to dimpl() creates an independent container with its own cache.

const appDi = dimpl();
const testDi = dimpl();

const Db = () => ({ connected: true });

appDi.get(Db) === appDi.get(Db); // true
appDi.get(Db) === testDi.get(Db); // false

Use this when:

  • You want one container for the whole app.
  • You want a fresh container per test.
  • You want distinct containers for multiple apps, tenants, workers, or integration-test scenarios.

API

dimpl()

Creates a new DI container.

const di = dimpl();

The returned container has four methods:

  • get
  • deps
  • set
  • wire

It also owns its own instance cache. No state is shared between containers.

di.get(fnOrClass)

Creates or returns the cached instance for one dependency.

const Config = () => ({ port: 3000 });

const config = di.get(Config);

Use get when:

  • You need one dependency.
  • You are bootstrapping the app.
  • A factory depends on one other dependency.

With classes:

class Logger {
  info(message: string) {
    console.log(message);
  }
}

const logger = di.get(Logger);

get is lazy. The function or class is called only once per container, then cached.

di.deps({ name: fnOrClass })

Creates or returns multiple dependencies as a typed object.

const Service = (
  { db, logger } = di.deps({
    db: Db,
    logger: Logger,
  }),
) => {
  return {
    run() {
      logger.info("Running");
      return db.query("select 1");
    },
  };
};

Use deps when:

  • A factory needs more than one dependency.
  • You want a readable dependency list at the top of a function.
  • You want TypeScript to infer a named object of dependency instances.

The object keys are yours. The values are dependency factories or classes. The returned object has the same keys, but each value is the resolved instance.

deps can also handle circular dependencies when access is delayed until after construction. See Circular Dependencies.

di.set(fnOrClass, instance)

Stores an instance for a dependency key.

const fakeLogger = {
  info() {},
};

di.set(Logger, fakeLogger);

di.get(Logger) === fakeLogger; // true

Use set when:

  • You want to override a dependency in a test.
  • You already created an instance yourself.
  • You want to provide an adapter from outside the container.

Call set before the dependency is resolved if you want consumers to receive the override.

di.wire(...fnOrClass)

Eagerly instantiates dependencies and returns nothing.

di.wire(HealthController, AuthController, ProductsController);

Use wire when:

  • A dependency exists for side effects.
  • Controllers register routes during construction.
  • Startup should fail immediately if a dependency cannot be created.

wire uses the same cache as get and deps. If something was already created, it will not be created again.

dimplCircularDepError

dimplCircularDepError is exported for code that wants to recognize container-level circular dependency failures.

import { dimplCircularDepError } from "dimpl";

Most application code does not need to catch it. Prefer structuring circular references so they are accessed lazily through methods or getters.

Circular Dependencies

If two services depend on each other, di.get inside those services will not work. It tries to resolve the dependency immediately, so the container detects that the first service is still being created and throws a dimplCircularDepError.

const A = (b = di.get(B)) => ({
  name: "a",
  b,
});

const B = (a = di.get(A)) => ({
  name: "b",
  a,
});

di.get(A); // throws dimplCircularDepError

Use di.deps for circular dependencies, and keep access lazy. A service cannot read a dependency that is still being constructed. That includes destructuring it or reading it in the factory body.

const A = (deps = di.deps({ b: B })) => {
  return {
    name: "a",
    getB() {
      return deps.b;
    },
  };
};

const B = (deps = di.deps({ a: A })) => {
  deps.a; // throws: A is not ready yet

  return {
    name: "b",
  };
};

Access the circular dependency later from a method or getter:

const A = (deps = di.deps({ b: B })) => ({
  name: "a",
  getB() {
    return deps.b;
  },
});

const B = (deps = di.deps({ a: A })) => ({
  name: "b",
  getA() {
    return deps.a;
  },
});

const a = di.get(A);
const b = di.get(B);

a.getB() === b; // true
b.getA() === a; // true

For circular dependencies, avoid destructuring the circular dependency in the parameter list:

// Avoid this for circular dependencies.
const B = ({ a } = di.deps({ a: A })) => ({
  useA() {
    return a;
  },
});

Destructuring reads a during construction. Keep the deps object and read deps.a later.

Patterns

Export One App Container

// di.ts
import { dimpl } from "dimpl";

export const di = dimpl();

Then import it from factories:

import { di } from "./di";

export const Orders = ({ db, payments } = di.deps({ db: Db, payments: Payments })) => {
  return {
    create() {
      // ...
    },
  };
};

Prefer Default Parameters

Default parameters make production code automatic and tests explicit.

export const Orders = (
  { db, payments } = di.deps({
    db: Db,
    payments: Payments,
  }),
) => {
  // ...
};

In production:

const orders = di.get(Orders);

In tests:

const orders = Orders({ db: fakeDb, payments: fakePayments });

Keep Constructors Simple

Classes are supported, but dimpl constructs them without arguments.

class Ids {
  create() {
    return crypto.randomUUID();
  }
}

di.get(Ids);

If a dependency needs other dependencies, a factory function with default parameters is usually clearer.

const Orders = ({ db, ids } = di.deps({ db: Db, ids: Ids })) => {
  // ...
};

License

ISC