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

monolite-di

v0.9.1

Published

tsyringe wiring for the monolite toolkit: framework tokens, engine factories and composition-root helpers.

Readme

monolite-di

The toolkit's dependency-injection wiring, on tsyringe.

Requires Node 20 or newer.

Why the container is a package of its own

A container is a preference, not a requirement. Every other package of the toolkit takes its dependencies as constructor arguments and never mentions a container: the kernel, the data layer, the HTTP layer and the feature packages can all be assembled with plain new calls, or with Awilix, or with InversifyJS. Nothing in them knows this package exists.

Keeping the wiring here is what makes that true and keeps it true. tsyringe and reflect-metadata appear in exactly one package.json in the whole monorepo — this one — so a consumer who prefers a different container simply never installs it, and a consumer who is happy with tsyringe gets the tokens, the factories and the composition-root helpers already written.

The split also draws the line the framework cares about most: the framework scaffolding is registered here; your features are registered by you. The two modules this package ships (registerPlugins and registerPersistence) cover the cross-cutting pieces and the persistence layer, which every application needs and no application wants to write again. Your repositories, BLLs and controllers stay in your own modules, in your own repository, and go on the same container right after.

What it exports

Tokens

| Export | What it is | | --- | --- | | TOKENS | The framework-level identifiers: IEnvs, ILogger, IRequestContext, ITransactionContext, IHealthProbe, ITokenBLL, IFileStorage, IDbPlugin, IUnitOfWork, IAuditTrail, IAuditLogStore. | | Token, TokenName | The value and the key types of the table. | | storeToken | The token an entity's generic repository is registered under (USERSUSERSStore). |

tsyringe resolves by string, so a typo in an @inject("ILoger") compiles and blows up at run time, with the process already up. Going through TOKENS means the compiler sees the typo. Feature tokens are not here on purpose: your BLL, your repository and your controller belong to your application, and hosting them here would make every consumer inherit a vocabulary of entities it does not have. Declare your own table and use both side by side.

Container helpers

| Export | What it does | | --- | --- | | container | tsyringe's root container, re-exported. | | createContainer | A child container, isolated from the global one — what a test wants. | | registerInstance | Binds an already-built object (the logger, a store). | | registerSingleton | Binds a class that must exist exactly once in the process. | | registerClass | Binds a class rebuilt on every resolution — the right default for a feature's three layers. | | registerFactory | Binds a token to whatever a factory returns. | | registerBinding | Binds an instance or a class, whichever was passed. | | Binding, Constructor | The types those helpers speak. | | CompositionRoot, createCompositionRoot | The entry-point shape: the container plus the resources the process owns. | | IManagedConnection | Something with authenticate() and close() — a connection pool. |

Importing anything from this package loads reflect-metadata once, so no consumer has to remember the import and no other package has to carry the dependency.

Factories

| Export | What it does | | --- | --- | | createPersistenceLayer | Builds the generic repository of every entity, the unit of work, the change log and the connection, on the engine the configuration names. | | EntityRegistration, AnyEntityRegistration | What the layer builds one repository from: the name the unit of work indexes by, the mapping, the token, and the seed the in-memory driver starts with. | | validateDataSourceEnv, missingDataSourceEnv | The variables the configured engine cannot connect without, checked at startup so a missing password is a refusal to boot rather than a timeout on the first request. | | resolveDriver, isOracleDriver | Resolve DATA_SOURCE (and its aliases) into a driver, failing loudly on an unknown value. | | buildOracleConfig, buildSequelizeConfig, buildMongoConfig | Read each engine's own variable prefix, with defaults aligned with the reference compose file. | | createLogger | Picks the logging backend named by LOG_DRIVER out of the ones the application registered. |

Modules

| Export | What it registers | | --- | --- | | registerPlugins | IEnvs, ILogger, IRequestContext, ITransactionContext and, if given, ITokenBLL and IFileStorage. | | registerPersistence | One store per entity, IUnitOfWork, IAuditTrail, IAuditLogStore and IHealthProbe. Each store is wrapped so it joins whatever transaction is open, which is what makes @Transactional() true of the repositories a module injects. Returns the whole layer, because the connection is not a dependency: it is a resource of the process. | | registerModules | Every feature module's own bindings, from the one list in composition/modules.ts. Order does not matter: tsyringe resolves when somebody asks, not when something is registered. |

Modules

| Export | What it is | | --- | --- | | MonoliteModule<T>, AnyMonoliteModule | A feature module as one value: its entity registration, its bindings and its controller. One line in one list instead of three edits in three files, which is also what makes it something a generator can insert. | | entitiesOf | The registrations the persistence layer wants, in the order they were listed. |

registration is optional. A module that owns no table — a report reading entities that already exist, a search across several, a dashboard, a webhook receiver — leaves it off and joins the same list, instead of needing a second place to be registered.

Building a composition root

The composition root is the one place that knows which implementation covers each interface, and the only place allowed to know it. It fixes the order —the environment before the logger, because the log driver is read from it; the request context before persistence, because the user of the audit columns comes from it— and then hands over.

import type { IHealthProbe } from "monolite-core";
import {
  createCompositionRoot,
  createLogger,
  registerPersistence,
  registerPlugins,
  TOKENS,
} from "monolite-di";
import { USERS_ENTITY, USERS_SEED, AUDIT_LOG_ENTITY } from "./infrastructure/entities.js";
import { DotenvEnvs } from "./infrastructure/dotenv.js";
import { PinoLogger } from "./infrastructure/pino.js";
import { validateCriticalEnvs } from "./config/env.validation.js";
import { registerUsers } from "./modules/users.module.js";

const envs = new DotenvEnvs();

export const root = createCompositionRoot((root) => {
  const plugins = registerPlugins({
    container: root.container,
    envs,
    // The logger arrives built: choosing a backend is a configuration read, not
    // a dependency the container can resolve.
    logger: createLogger({ envs, drivers: { pino: (s) => new PinoLogger(s) } }),
    // Fails loudly when a critical secret is missing, rather than starting with
    // an insecure default nobody notices.
    validate: validateCriticalEnvs,
  });

  const persistence = registerPersistence({
    container: root.container,
    ...plugins,
    entities: [
      { name: "USERS", metadata: USERS_ENTITY, seed: USERS_SEED, token: "UsersStore" },
    ],
    auditLog: AUDIT_LOG_ENTITY,
  });

  // The connection is not registered in the container — nobody injects it — but
  // the process owns it, so the root opens and closes it.
  root.manage(persistence.connection);

  // From here on order stops mattering: a module declares its classes and
  // tsyringe resolves the dependencies when somebody asks for them.
  registerUsers(root.container);
});

// In `main.ts`
await root.warmUp(); // a bad password shows up in the boot log, not in request #1

process.on("SIGTERM", async () => {
  // Readiness turns to false before the process stops accepting traffic, so the
  // load balancer takes it out of rotation first.
  root.container.resolve<IHealthProbe>(TOKENS.IHealthProbe).beginShutdown();
  await root.shutdown();
});

A feature module of your own is three lines, and it never mentions an engine:

import { registerClass } from "monolite-di";
import type { DependencyContainer } from "monolite-di";
import { APP_TOKENS } from "./tokens.js"; // your own table, next to TOKENS

export function registerUsers(container: DependencyContainer): void {
  // `UsersRepository` injects "UsersStore" and never learns which engine is
  // underneath. Transient on purpose: these three keep no state between
  // requests, so a singleton would save nothing and would hide an accidental
  // shared state the day somebody adds a field to the class.
  registerClass(container, APP_TOKENS.IUsersRepository, UsersRepository);
  registerClass(container, APP_TOKENS.IUsersBLL, UsersBLL);
  registerClass(container, APP_TOKENS.IUsersController, UsersController);
}

Swapping the engine

createPersistenceLayer is the piece that makes changing database a change of configuration. It accepts memory, oracle, mssql (sqlserver), postgres (postgresql), mysql (mariadb) and mongodb (mongo), reading DATA_SOURCE unless the caller names one, and it validates the value against that table instead of falling back to memory: a misspelled DATA_SOURCE=postgress would otherwise start in memory and the failure would surface much later, as data that does not persist.

Whichever engine comes out, the BLLs receive exactly the same IGenericRepository<T>. The in-memory driver is the one that makes the test suite run without bringing a container up, and it is the reason connection is optional: there is nothing to open and nothing to close.

Connection settings can also be passed explicitly (oracle, sequelize, mongo), which is what an application configured from something other than environment variables does.

Logging backends

createLogger keeps the selection logic —LOG_DRIVER with pino as the default, and a loud failure on an unknown value— but the backends are handed to it rather than imported:

createLogger({
  envs,
  drivers: {
    pino: (settings) => new PinoLogger(settings),
    winston: (settings) => new WinstonLogger(settings),
  },
});

Importing both here would put both in the dependency tree of everyone who installs this package, to run one of them at most. The part worth sharing is the selection; the implementations stay where they are already paid for.

The rest of the framework works without this

Nothing outside this package imports tsyringe or reflect-metadata. If you would rather not use a container at all, build the same objects by hand — the factories here are plain functions and are perfectly usable on their own:

import { createPersistenceLayer } from "monolite-di";

const persistence = createPersistenceLayer({ envs, logger, entities, auditLog });
const users = new UsersBLL(persistence.store("USERS"), persistence.unitOfWork);

And if you would rather not install this package either, monolite-core and monolite-data know nothing about it: the wiring is yours to write.

License

MIT