pleroma
v1.0.0
Published
A lightweight dependency injection (DI) container for TypeScript and Node.js. Pleroma uses legacy TypeScript decorators to register constructor dependencies and resolves them automatically, including support for circular dependencies via proxies.
Readme
Pleroma
A lightweight dependency injection (DI) container for TypeScript and Node.js. Pleroma uses legacy TypeScript decorators to register constructor dependencies and resolves them automatically, including support for circular dependencies via proxies.
Features
- Constructor injection with
@Inject - Class registration with
@Injectable - Singleton container instance (
containerexport orContainer.getInstance()) - Circular dependency resolution using lazy proxies
- ESM-first (
"type": "module") - Built on
reflect-metadata
Requirements
- Node.js 20.19+, 22.13+, or 24+
- TypeScript 5.x+
- Legacy decorators enabled in the consuming project (
experimentalDecorators: true) reflect-metadata— included as a dependency; loaded automatically when Pleroma modules are imported
Installation
yarn add pleroma
# or
npm install pleroma
# or
pnpm add pleromaTypeScript configuration
Consumers must enable legacy decorators in their tsconfig.json:
{
"compilerOptions": {
"experimentalDecorators": true,
"module": "NodeNext",
"moduleResolution": "NodeNext",
"target": "ESNext",
"strict": true
}
}If you use isolatedModules and verbatimModuleSyntax, import service classes as value imports (not import type) so decorator metadata is emitted correctly.
Entry point
Import Pleroma (or your decorated modules) before calling resolve(), so decorators run and metadata is registered:
import { container } from 'pleroma'
import './services/index.js' // loads all @Injectable classes
const app = container.resolve(AppService)You may also import reflect-metadata explicitly at the top of your application entry file; Pleroma loads it internally via MetadataStore, but an explicit import guarantees early initialization:
import 'reflect-metadata'Quick start
1. Define services
// services/Database.ts
import { Injectable } from 'pleroma'
@Injectable()
export class Database {
connect() {
return 'connected'
}
}// services/UserRepository.ts
import { Inject, Injectable } from 'pleroma'
import { Database } from './Database.js'
@Injectable()
export class UserRepository {
constructor(@Inject(() => Database) private readonly db: Database) {}
findAll() {
return this.db.connect()
}
}// services/AppService.ts
import { Inject, Injectable } from 'pleroma'
import { UserRepository } from './UserRepository.js'
@Injectable()
export class AppService {
constructor(
@Inject(() => UserRepository) private readonly users: UserRepository,
) {}
run() {
return this.users.findAll()
}
}2. Resolve from the container
import { container } from 'pleroma'
import './services/Database.js'
import './services/UserRepository.js'
import './services/AppService.js'
const app = container.resolve(AppService)
app.run() // 'connected'Use arrow functions in @Inject(() => Class) so the class reference is resolved lazily (helps with circular imports and hoisting).
API
container
Pre-initialized singleton instance of Container. Same object as Container.getInstance().
import { container } from 'pleroma'
const instance = container.resolve(MyService)Container
| Method | Description |
| ------------------------- | --------------------------------------------------- |
| Container.getInstance() | Returns the process-wide singleton container |
| resolve<T>(target) | Creates or returns the cached instance for target |
import { Container } from 'pleroma'
const container = Container.getInstance()
const instance = container.resolve(MyService)Resolved instances are cached per class constructor. The container does not support scoped lifetimes (request scope, transient, etc.) in the current version.
@Injectable()
Class decorator that registers the class in the internal metadata store. Apply it to every class you intend to resolve through the container.
@Injectable()
export class MyService {}@Inject(getInjectable)
Parameter decorator for constructor injection. getInjectable is a zero-argument factory that returns the dependency class:
constructor(@Inject(() => OtherService) other: OtherService) {}The container:
- Reads metadata keyed by
injectable:<ClassName> - Sorts dependencies by
parameterIndex - Recursively resolves each dependency
- Instantiates the class with
new Class(...dependencies)
Circular dependencies
When class A depends on B and B depends on A, Pleroma detects the cycle and returns a proxy for the dependency still being constructed. Method access on the proxy forwards to the real instance once initialization completes.
Ensure both classes use @Injectable() and @Inject, and that modules are loaded before resolve() is called.
Using Pleroma as a library
Exports
From the package entry (src/index.ts):
container— singleton container instanceContainer— container class andConstructabletypeInjectable— class decoratorInject— parameter decorator
Singleton behavior
All imports of container from the same installed copy of Pleroma share one registry. Avoid duplicate package versions in monorepos (e.g. hoist or align versions) so metadata and instances stay consistent.
Testing
The container has no public reset() API. For isolated tests, prefer:
- Separate test processes, or
Container.getInstance()with a future API if you add instance creation for tests
Today the constructor is private and only the singleton is available.
Development
Prerequisites
- Yarn 4.x (see
packageManagerinpackage.json)
Setup
yarn installScripts
| Script | Description |
| ----------------- | ------------------------- |
| yarn test | Run unit tests (Vitest) |
| yarn test:watch | Run tests in watch mode |
| yarn lint | Run ESLint on the project |
| yarn lint:fix | Run ESLint with auto-fix |
Type-check
yarn exec tsc --noEmitProject structure
src/
├── index.ts # Public API entry
├── core/
│ └── Container.ts # DI container and resolution
├── decorators/
│ ├── Injectable.ts # Class decorator
│ └── Inject.ts # Constructor parameter decorator
└── tools/
└── MetadataStore.ts # Metadata storage (reflect-metadata)Editor setup (optional)
If you use ESLint and Prettier together on save, avoid running both as formatters. Recommended approach:
- Use the Prettier extension for
editor.formatOnSave - Use
eslint-config-prettierto disable conflicting ESLint style rules - Set
typescript.preferences.quoteStyleto"single"if you usesource.organizeImportson save (matches this repo’s Prettier config)
License
MIT
