A powerful and flexible dependency injection container for TypeScript/JavaScript applications. Monopole provides a modern, module-based approach to dependency injection with support for async resolution, property injection using TC39 Stage 3 decorators, and comprehensive lifecycle management.
- Module-based architecture - Organize dependencies with modules that support imports/exports
- Multiple provider types - Class, value, factory, and existing providers
- Property injection - Using TC39 Stage 3 decorators (
@inject) - Async resolution - Full support for async providers and initialization
- Circular dependency support - Automatic resolution of circular dependencies
- Lifecycle management - Module boot and dispose hooks
- TypeScript first - Full TypeScript support with type inference
- Framework agnostic - Works with Deno, Node.js, and browsers (Node/Bun/browsers require a build step; see below)
Monopole relies on the TC39 Stage 3 decorators proposal. Today, only Deno (v1.40+)/Deno Deploy ship this syntax natively.
| Runtime | Native Stage 3 decorators | What you need |
|---|---|---|
| Deno | âś… (TS/TSX/JSX) | Works out of the box. Make sure you run deno test/deno run directly against the source files. |
| Node.js / Bun | ❌ (syntax error or legacy decorators) | Compile with TypeScript 5+ or Babel before running. The emitted JS no longer contains raw @decorator syntax. |
| Browsers / Edge Functions / Workers | ❌ | Bundle/transpile with your existing toolchain (Vite, Webpack, Rollup, etc.) so the shipped JS is decorator-free. |
TypeScript 5.0 implements the new decorators proposal and accepts the syntax
without --experimentalDecorators. A minimal tsconfig.json:
Compile your sources (tsc -p tsconfig.json or via ts-node --transpile-only)
and run the generated JS with Node/Bun.
If you stay in JavaScript, enable the official Stage 3 transform:
{
"plugins": [
["@babel/plugin-proposal-decorators", { "version": "2023-11" }],
"@babel/plugin-transform-class-properties"
]
}Ensure your bundler (Vite, Next.js, etc.) runs Babel on Monopole-using files so the output no longer contains raw decorators before it reaches browsers/Node runtimes.
deno add @denostack/monopoleimport { createContainer } from "@denostack/monopole";npm install monopoleimport { createContainer } from "monopole";ℹ️ Remember: runtimes other than Deno must load the transpiled output from the "Runtime compatibility" section above. Install the package, run it through TypeScript/Babel in your build, and execute/bundle the generated JavaScript.
import { createContainer, inject, type Module } from "monopole";
// Define services
class Logger {
log(message: string) {
console.log(`[LOG] ${message}`);
}
}
class UserService {
@inject(Logger)
logger!: Logger;
getUser(id: string) {
this.logger.log(`Fetching user ${id}`);
return { id, name: "John Doe" };
}
}
// Create a module
const appModule: Module = {
providers: [
Logger,
UserService,
],
exports: [UserService],
};
// Create and use container
const container = await createContainer(appModule);
const userService = container.get(UserService);
userService.getUser("123");Modules are the building blocks of your application. They encapsulate providers and can import other modules to compose your dependency graph.
import type { Container, Module } from "monopole";
const databaseModule: Module = {
providers: [
{ id: "dbConfig", useValue: { host: "localhost", port: 5432 } },
{
id: DatabaseConnection,
useFactory: async (config) => {
const conn = new DatabaseConnection(config);
await conn.connect();
return conn;
},
inject: ["dbConfig"],
},
],
exports: [DatabaseConnection],
async dispose(container: Container) {
const conn = container.get(DatabaseConnection);
await conn.disconnect();
},
};
const appModule: Module = {
imports: [databaseModule],
providers: [UserRepository],
exports: [UserRepository],
};Monopole supports four types of providers:
// Direct class registration
providers: [MyService];
// With explicit ID
providers: [{
id: "myService",
useClass: MyService,
}];providers: [
{ id: "apiUrl", useValue: "https://api.example.com" },
{ id: "config", useValue: Promise.resolve({ key: "value" }) },
];providers: [{
id: HttpClient,
useFactory: (apiUrl: string) => new HttpClient(apiUrl),
inject: ["apiUrl"],
}];providers: [
{ id: Logger, useClass: ConsoleLogger },
{ id: "logger", useExisting: Logger },
];Use the @inject decorator with TC39 Stage 3 decorator syntax:
import { inject } from "monopole";
class OrderService {
@inject(Logger)
logger!: Logger;
@inject(DatabaseConnection)
db!: DatabaseConnection;
@inject("config")
config!: Config;
// With transformation
@inject(UserService, (service) => service.getUser.bind(service))
getUser!: (id: string) => User;
}Factory providers can specify optional dependencies:
providers: [{
id: Service,
useFactory: (required, optional) => {
return new Service(required, optional ?? defaultValue);
},
inject: [
RequiredDep,
[OptionalDep, true], // true marks it as optional
],
}];Monopole automatically handles circular dependencies:
class Parent {
@inject(Child)
child!: Child;
}
class Child {
@inject(Parent)
parent!: Parent;
}
const module: Module = {
providers: [Parent, Child],
exports: [Parent, Child],
};
const container = await createContainer(module);
const parent = container.get(Parent);
const child = container.get(Child);
console.log(parent.child === child); // true
console.log(child.parent === parent); // trueCompose complex applications from smaller modules:
// Feature modules
const authModule: Module = {
providers: [AuthService, JwtService],
exports: [AuthService],
};
const dataModule: Module = {
providers: [Database, UserRepository],
exports: [UserRepository],
};
// Application module
const appModule: Module = {
imports: [authModule, dataModule],
providers: [
{
id: AppService,
useFactory: (auth, repo) => new AppService(auth, repo),
inject: [AuthService, UserRepository],
},
],
exports: [AppService],
async boot(container) {
// Initialize application
const app = container.get(AppService);
await app.initialize();
},
async dispose(container) {
// Cleanup
const app = container.get(AppService);
await app.shutdown();
},
};
// Create application
const container = await createContainer(appModule);
const app = container.get(AppService);Containers support the async disposal pattern:
// Using async disposal
await using container = await createContainer(appModule);
// Container will be automatically disposed when going out of scope
// Manual disposal
const container = await createContainer(appModule);
try {
// Use container
} finally {
await container.dispose();
}- Deno HTTP Server - Web application with modular architecture
Creates a new container from a module definition.
get<T>(id: ServiceIdentifier<T>): T- Get a resolved instancehas(id: ServiceIdentifier): boolean- Check if a service existsentries(): IterableIterator<[ServiceIdentifier, unknown]>- Get all entriesdispose(): Promise<void>- Dispose the container and all modules
imports?: Module[]- Modules to importproviders?: Provider[]- Service providersexports?: ServiceIdentifier[]- Exported service identifiersboot?(container: Container): MaybePromise<void>- Initialization hookdispose?(container: Container): MaybePromise<void>- Cleanup hook
Property decorator for dependency injection.
{ "compilerOptions": { "target": "ES2020", "module": "ESNext", "experimentalDecorators": false, "emitDecoratorMetadata": false, "moduleResolution": "bundler" } }