@nextrush/di 3.0.6 → 4.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,481 +1,397 @@
1
1
  # @nextrush/di
2
2
 
3
- Lightweight dependency injection container for NextRush v3.
3
+ > A lightweight dependency-injection container for NextRush — decorator-driven constructor injection over `tsyringe`, with singleton / transient / request scopes and errors that tell you how to fix them.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@nextrush/di.svg)](https://www.npmjs.com/package/@nextrush/di)
6
+ [![downloads](https://img.shields.io/npm/dm/@nextrush/di.svg)](https://www.npmjs.com/package/@nextrush/di)
7
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/@nextrush/di.svg)](https://bundlephobia.com/package/@nextrush/di)
8
+ [![types](https://img.shields.io/npm/types/@nextrush/di.svg)](https://www.npmjs.com/package/@nextrush/di)
9
+ [![ESM only](https://img.shields.io/badge/module-ESM--only-blue.svg)](https://nodejs.org/api/esm.html)
10
+ [![license](https://img.shields.io/npm/l/@nextrush/di.svg)](https://github.com/0xTanzim/nextRush/blob/main/LICENSE)
11
+
12
+ | | |
13
+ | --- | --- |
14
+ | **Purpose** | Decorator-driven dependency injection for NextRush — mark a class `@Service()`, declare dependencies in its constructor, and `container.resolve()` wires the graph |
15
+ | **Package type** | Core |
16
+ | **Status** | Stable ✅ |
17
+ | **Included in `nextrush`?** | Partially — `Service`, `Repository`, `container`, `createContainer`, `inject`, and the `Container` type are re-exported through [`nextrush/class`](../class). Install `@nextrush/di` directly to use `@Config`, `@Injectable`, `@Optional`, `delay`, the DI error classes, or the metadata readers. |
18
+ | **Support tier** | Public — core (stable, semver-guarded) — see [ADR-0005](https://github.com/0xTanzim/nextRush/blob/main/docs/adr/ADR-0005-package-tiers-sealed-surface-deprecation.md) |
19
+ | **Maintenance** | Active |
20
+ | **Runtime** | Universal — Node · Bun · Deno · Edge |
21
+ | **Requires** | Node `>=22` · ESM-only · TypeScript `>=5.x` |
22
+ | **Introduced** | `v3.0.0` |
23
+
24
+ ## Highlights
25
+
26
+ - 🧩 **Decorator-driven** — `@Service()` / `@Repository()` + constructor parameters; no manual wiring for the common case
27
+ - 🔁 **Three scopes** — `singleton` (default), `transient`, and `request` (per-request child container)
28
+ - 🧭 **Actionable errors** — circular dependencies and unregistered tokens throw messages that name the cycle and list fixes
29
+ - 📦 **Peer-depends on `tsyringe` + `reflect-metadata`** — this is the one core package with runtime dependencies (a sanctioned exception; the container wraps `tsyringe`)
30
+ - ✅ **ESM-only**, side-effect-free, **fully typed** — strict TypeScript, zero `any`
31
+
32
+ <details>
33
+ <summary><strong>Table of contents</strong></summary>
34
+
35
+ [The problem](#the-problem) · [When to use](#when-to-use) · [Installation](#installation) · [Quick start](#quick-start) · [Capabilities](#capabilities) · [Mental model](#mental-model) · [Common tasks](#common-tasks) · [API overview](#api-overview) · [Options](#options) · [Compatibility](#compatibility) · [Troubleshooting](#troubleshooting) · [FAQ](#faq) · [Package relationships](#package-relationships) · [Architecture](#architecture) · [Resources](#resources)
36
+
37
+ </details>
38
+
39
+ ---
40
+
41
+ ## The problem
42
+
43
+ Wiring dependencies by hand starts simple and rots fast. A service needs a repository, which needs a database client, which needs config — and every construction site has to know the whole chain and build it in the right order. Swapping an implementation (a real repository for a fake in a test) means threading the change through every caller.
44
+
45
+ ```ts
46
+ // TODAY, without a container — every call site rebuilds the whole graph by hand,
47
+ // in the right order, and a swapped implementation ripples everywhere:
48
+ const config = new DatabaseConfig();
49
+ const db = new DatabaseClient(config);
50
+ const userRepo = new UserRepository(db);
51
+ const userService = new UserService(userRepo);
52
+ // …and the next module that needs UserService does all of this again.
53
+ ```
4
54
 
5
- ## Features
55
+ As the graph grows, ordering bugs and duplicated construction multiply, and a single shared instance (a connection pool) can be built twice by accident. `@nextrush/di` inverts this: a class declares *what it needs* in its constructor and *how it should be shared* with a decorator, and the container resolves the graph once, in order, memoizing singletons.
6
56
 
7
- - **Constructor Injection** — Automatic dependency resolution via TypeScript metadata
8
- - **Singleton & Transient Scopes** — Control instance lifecycle per service
9
- - **Circular Dependency Detection** — O(1) Set-based detection with cycle visualization, catches tsyringe-internal chains
10
- - **Production-Grade Errors** — Actionable messages with fix suggestions (`@Service()`, `@Repository()`, `@Config()` hints)
11
- - **Optional Dependencies** — `@Optional()` decorator for graceful handling of missing services
12
- - **Test-Friendly** — Isolated containers and instance clearing for test setup
57
+ ## When to use
13
58
 
14
- ## Development
59
+ `@nextrush/di` is the DI container behind NextRush's class-based runtime. If you use `@Controller` / `@Service` through [`nextrush/class`](../class), you are already using it. Reach for it **directly** when you want the container on its own, or the parts `nextrush/class` does not re-export.
15
60
 
16
- For the best development experience with full decorator and DI support, use **`@nextrush/dev`**.
61
+ **Use `@nextrush/di` if:**
17
62
 
18
- ```bash
19
- pnpm add -D @nextrush/dev
20
- ```
63
+ - ✓ You want constructor injection with decorators (`@Service`, `@Repository`) and automatic dependency resolution
64
+ - You need lifecycle control — one shared instance (`singleton`), a fresh one each resolve (`transient`), or one per request (`request`)
65
+ - ✓ You want circular-dependency and missing-dependency errors that explain how to fix them
66
+ - ✓ You need the pieces beyond the `nextrush/class` re-export: `@Config`, `@Injectable`, `@Optional`, `delay()`, or the DI error classes
21
67
 
22
- Then in your `package.json`:
68
+ **Reach for something else if:**
23
69
 
24
- ```json
25
- {
26
- "scripts": {
27
- "dev": "nextrush dev"
28
- }
29
- }
30
- ```
31
-
32
- ### Why?
33
-
34
- TypeScript's `emitDecoratorMetadata` option emits runtime type information that allows the DI container to automatically resolve constructor dependencies. Most modern fast runners (`tsx`, `esbuild`, `node --experimental-strip-types`) strip types but **do not** emit this metadata, causing errors like:
35
-
36
- ```
37
- TypeInfo not known for "UserService"
38
- ```
70
+ - ✗ You only need controllers, guards, and request scope wired for you → use [`nextrush/class`](../class), which re-exports the common surface and drives the container for you
71
+ - ✗ You have a two-object graph you construct once — a container is more machinery than a `new` call needs
39
72
 
40
- | Runtime | Decorator Metadata | Recommended |
41
- | ----------------- | ------------------ | ---------------- |
42
- | **nextrush dev** | Full Support | Yes |
43
- | **tsc + node** | Full Support | Yes (Production) |
44
- | **tsx / esbuild** | Not Supported | No |
45
- | **ts-node --esm** | Issues | No |
73
+ ---
46
74
 
47
75
  ## Installation
48
76
 
49
77
  ```bash
50
78
  pnpm add @nextrush/di
79
+ # npm i @nextrush/di · yarn add @nextrush/di · bun add @nextrush/di
51
80
  ```
52
81
 
53
- > If you use the `nextrush` meta-package, `reflect-metadata` is auto-imported. Otherwise, install it separately: `pnpm add reflect-metadata` and add `import 'reflect-metadata'` at your entry point.
82
+ > [!NOTE]
83
+ > Already using `nextrush`? `Service`, `Repository`, `container`, `createContainer`, and `inject`
84
+ > are re-exported from [`nextrush/class`](../class) — `import { Service, container } from 'nextrush/class'`
85
+ > works without installing this directly. Install `@nextrush/di` when you need `@Config`,
86
+ > `@Injectable`, `@Optional`, `delay`, the DI error classes, or the metadata readers.
54
87
 
55
- ## TypeScript Configuration
88
+ > [!IMPORTANT]
89
+ > Decorators need `reflect-metadata` loaded once before any decorated class is defined, and your
90
+ > `tsconfig.json` needs `"experimentalDecorators": true` and `"emitDecoratorMetadata": true`.
91
+ > Importing `@nextrush/di` (or `nextrush/class`) loads `reflect-metadata` for you.
56
92
 
57
- **Required** `tsconfig.json` settings:
93
+ ## Quick start
58
94
 
59
- ```json
60
- {
61
- "compilerOptions": {
62
- "experimentalDecorators": true,
63
- "emitDecoratorMetadata": true
64
- }
65
- }
66
- ```
67
-
68
- ## Quick Start
69
-
70
- ```typescript
71
- import 'reflect-metadata'; // Required when NOT using the nextrush meta-package
95
+ ```ts
72
96
  import { Service, Repository, container } from '@nextrush/di';
73
97
 
74
98
  @Repository()
75
99
  class UserRepository {
76
100
  findAll() {
77
- return [{ id: 1, name: 'Alice' }];
101
+ return [{ id: 1, name: 'Ada' }];
78
102
  }
79
103
  }
80
104
 
81
105
  @Service()
82
106
  class UserService {
83
- constructor(private repo: UserRepository) {}
107
+ constructor(private readonly repo: UserRepository) {}
84
108
 
85
109
  getUsers() {
86
110
  return this.repo.findAll();
87
111
  }
88
112
  }
89
113
 
90
- const userService = container.resolve(UserService);
91
- console.log(userService.getUsers()); // [{ id: 1, name: 'Alice' }]
114
+ // Resolve the whole graph — UserRepository is constructed and injected automatically.
115
+ const users = container.resolve(UserService);
116
+ console.log(users.getUsers()); // [{ id: 1, name: 'Ada' }]
92
117
  ```
93
118
 
94
- ## API Reference
119
+ You never construct `UserRepository` yourself. `@Service()` marks `UserService` as resolvable, the constructor declares what it needs, and `container.resolve()` builds the graph in dependency order — memoizing each singleton so the second resolve returns the same instances.
95
120
 
96
- ### Decorators
121
+ ## Capabilities
97
122
 
98
- #### `@Service(options?)`
123
+ **Injection**
124
+ - **Constructor injection** — declare dependencies as constructor parameters; the container resolves them by type
125
+ - **`@inject(token)`** — inject by string/symbol token or interface when the type can't be inferred at runtime
126
+ - **`@Optional()`** — an unresolved optional dependency is injected as `undefined` instead of throwing
127
+ - **`delay(() => Class)`** — lazily resolve a dependency to break a circular reference
99
128
 
100
- Mark a class as an injectable service. Singletons by default.
129
+ **Scopes** (the canonical reference see [Mental model](#mental-model))
130
+ - **`singleton`** (default) — one shared instance for the process lifetime
131
+ - **`transient`** — a fresh instance on every resolve
132
+ - **`request`** — one instance per request, shared within it, via a per-request child container
101
133
 
102
- ```typescript
103
- @Service()
104
- class MyService {}
134
+ **Providers**
135
+ - **`useClass`** — construct a class (honoring its declared scope)
136
+ - **`useValue`** — register a constant (config object, pre-built client)
137
+ - **`useFactory`** — build lazily, optionally injecting other tokens; async factories are awaited by `bootstrap()`
105
138
 
106
- // Transient (new instance each time)
107
- @Service({ scope: 'transient' })
108
- class RequestLogger {}
109
- ```
110
-
111
- **`ServiceOptions`:**
112
-
113
- | Property | Type | Default | Description |
114
- | -------- | ---------------------------- | ------------- | ------------------ |
115
- | `scope` | `'singleton' \| 'transient'` | `'singleton'` | Instance lifecycle |
139
+ **Safety & developer experience**
140
+ - **Circular-dependency detection** — an O(1) resolution-stack guard throws a `CircularDependencyError` that names the cycle *before* the call stack overflows
141
+ - **Actionable errors** — `DependencyResolutionError` lists concrete fixes; `InvalidProviderError` shows the valid provider shapes
142
+ - **Fully typed** — strict TypeScript, zero `any`; the container contract is shared through `@nextrush/types`
116
143
 
117
- #### `@Repository(options?)`
144
+ ## Mental model
118
145
 
119
- Semantic alias for `@Service()`. Sets metadata type to `'repository'` instead of `'service'`. Use for data access layers.
146
+ A class declares **what it needs** (constructor parameters) and **how it should be shared** (its scope). The container owns the *when* and *how many* — it resolves the graph in order and memoizes according to scope.
120
147
 
121
- ```typescript
122
- @Repository()
123
- class UserRepository {
124
- findById(id: string) {
125
- return db.users.find(id);
126
- }
127
- }
148
+ ```text
149
+ @Service() / @Repository() ---> container.register(token, { useClass }, { scope })
150
+ class + constructor deps | singleton | transient | request
151
+ v
152
+ container.resolve(token) ---> tsyringe constructs, injecting resolved deps
153
+ |
154
+ +-- cycle detected? ---> CircularDependencyError (names the cycle, before the stack blows)
155
+ +-- token missing? ---> DependencyResolutionError (lists the fixes)
128
156
  ```
129
157
 
130
- #### `@inject(token)`
158
+ **Rule:** the class's declared scope — not the call site — decides singleton vs transient; an explicit `register(..., { scope })` only overrides it deliberately.
131
159
 
132
- Explicitly inject a dependency by token. Use for interfaces, string tokens, or symbol tokens.
160
+ > [!TIP]
161
+ > The resolution sequence, the request-scoped child container, and the instance lifecycle per
162
+ > scope (with Mermaid diagrams) are in [`ARCHITECTURE.md`](./ARCHITECTURE.md).
133
163
 
134
- ```typescript
135
- const DATABASE_TOKEN = Symbol('Database');
164
+ ---
136
165
 
137
- @Service()
138
- class UserService {
139
- constructor(
140
- @inject(DATABASE_TOKEN) private db: IDatabase,
141
- @inject('API_KEY') private apiKey: string
142
- ) {}
143
- }
144
- ```
166
+ ## Common tasks
145
167
 
146
- #### `@AutoInjectable()`
168
+ ### Mark a service and inject it
147
169
 
148
- Mark a class as injectable in the container. Sets service type metadata to `'service'`.
170
+ ```ts
171
+ import { Service } from '@nextrush/di';
149
172
 
150
- ```typescript
151
- @AutoInjectable()
152
- class FeatureService {
153
- constructor(private logger: Logger) {}
173
+ @Service() // singleton by default — one shared instance
174
+ class Logger {
175
+ info(msg: string) { console.log(msg); }
154
176
  }
155
177
 
156
- // Resolve through the container
157
- const service = container.resolve(FeatureService);
178
+ @Service()
179
+ class OrderService {
180
+ constructor(private readonly logger: Logger) {} // injected automatically
181
+ }
158
182
  ```
159
183
 
160
- > **Note:** Despite the name, `@AutoInjectable()` does not enable dependency injection via `new`. The current implementation uses tsyringe's `injectable()` decorator internally. Dependencies are resolved only through `container.resolve()`.
184
+ ### Choose a scope
161
185
 
162
- #### `delay(tokenFactory)`
186
+ ```ts
187
+ import { Service } from '@nextrush/di';
163
188
 
164
- Defer resolution to break circular dependencies. Returns a lazy token for use with `@inject()`.
189
+ @Service() // singleton one shared instance
190
+ class ConfigService {}
165
191
 
166
- ```typescript
167
- import { delay, inject, Service } from '@nextrush/di';
192
+ @Service({ scope: 'transient' }) // a fresh instance on every resolve
193
+ class Formatter {}
168
194
 
169
- @Service()
170
- class ServiceA {
171
- constructor(@inject(delay(() => ServiceB)) private b: ServiceB) {}
172
- }
173
-
174
- @Service()
175
- class ServiceB {
176
- constructor(@inject(delay(() => ServiceA)) private a: ServiceA) {}
195
+ @Service({ scope: 'request' }) // one instance per request, shared within it
196
+ class RequestId {
197
+ readonly id = crypto.randomUUID();
177
198
  }
178
199
  ```
179
200
 
180
- #### `@Optional()`
201
+ `request` scope only takes effect when the service is resolved from a per-request child container — the class-based runtime does this for you (see [`nextrush/class`](../class)); to do it manually, `container.createChild()` per request and resolve from the child.
181
202
 
182
- Mark a constructor parameter as optional. When the dependency is not registered, the container injects `undefined` instead of throwing.
203
+ ### Inject by token, or make a dependency optional
183
204
 
184
- ```typescript
185
- import { Service, Optional, inject } from '@nextrush/di';
205
+ ```ts
206
+ import { Service, inject, Optional } from '@nextrush/di';
186
207
 
187
208
  @Service()
188
209
  class NotificationService {
189
210
  constructor(
190
- @Optional() private emailService?: EmailService,
191
- @inject('SLACK_TOKEN') @Optional() private slackToken?: string
211
+ @inject('MAILER') private readonly mailer: Mailer, // by string token
212
+ @Optional() @inject('SMS') private readonly sms?: SmsClient, // undefined if unregistered
192
213
  ) {}
193
-
194
- notify(message: string) {
195
- if (this.emailService) {
196
- this.emailService.send(message);
197
- }
198
- if (this.slackToken) {
199
- // send to Slack
200
- }
201
- }
202
214
  }
203
215
  ```
204
216
 
205
- #### `isParameterOptional(target, parameterIndex)`
206
-
207
- Check if a specific constructor parameter is marked as optional.
217
+ ### Register a value or a factory manually
208
218
 
209
- ```typescript
210
- import { isParameterOptional, Optional, Service } from '@nextrush/di';
219
+ ```ts
220
+ import { container } from '@nextrush/di';
211
221
 
212
- @Service()
213
- class MyService {
214
- constructor(@Optional() private dep?: SomeDep) {}
215
- }
222
+ container.register('CONFIG', { useValue: { port: 8080 } });
223
+ container.register('CLOCK', { useFactory: () => new Date() });
216
224
 
217
- isParameterOptional(MyService, 0); // true
218
- isParameterOptional(MyService, 1); // false
225
+ // Factory with injected dependencies + an async factory awaited by bootstrap()
226
+ container.register('DB', {
227
+ useFactory: (url: string) => connect(url),
228
+ inject: ['DATABASE_URL'],
229
+ });
230
+ await container.bootstrap(); // resolves and caches async factory results
219
231
  ```
220
232
 
221
- #### `getOptionalParams(target)`
222
-
223
- Get all optional parameter indices for a class. Returns a `ReadonlySet<number>`.
233
+ ### Isolate a container for tests
224
234
 
225
- ```typescript
226
- import { getOptionalParams } from '@nextrush/di';
235
+ ```ts
236
+ import { createContainer } from '@nextrush/di';
227
237
 
228
- const optionals = getOptionalParams(MyService);
229
- // Set { 0 }
238
+ const testContainer = createContainer(); // fresh, isolated child container
239
+ testContainer.register(UserRepository, { useClass: FakeUserRepository });
240
+ const service = testContainer.resolve(UserService); // uses the fake
230
241
  ```
231
242
 
232
- ### Utility Functions
243
+ ## API overview
233
244
 
234
- #### `hasServiceMetadata(target)`
245
+ The sealed public surface (ADR-0005), grouped by role.
235
246
 
236
- Check if a class has DI metadata (decorated with `@Service()` or `@Repository()`).
247
+ | Export | Signature | Since | Stability | Description |
248
+ | ------ | --------- | ----- | --------- | ----------- |
249
+ | `container` | `Container` | `3.0.0` | Stable ✅ | The default DI container instance. |
250
+ | `createContainer` | `() => Container` | `3.0.0` | Stable ✅ | Create a fresh, isolated container (testing / scoping). |
251
+ | `Service` | `(options?: ServiceOptions) => ClassDecorator` | `3.0.0` | Stable ✅ | Mark a class injectable (singleton by default). |
252
+ | `Repository` | `(options?: ServiceOptions) => ClassDecorator` | `3.0.0` | Stable ✅ | Same as `Service`, semantically a data-access class. |
253
+ | `Config` | `(options?: ConfigOptions) => ClassDecorator` | `3.0.0` | Stable ✅ | Mark a configuration holder — always a singleton. |
254
+ | `Injectable` | `() => ClassDecorator` | `3.0.0` | Stable ✅ | Make a class resolvable without service metadata (transient). |
255
+ | `inject` | `(token: unknown) => ParameterDecorator` | `3.0.0` | Stable ✅ | Inject a dependency by class/string/symbol token. |
256
+ | `Optional` | `() => ParameterDecorator` | `3.0.0` | Stable ✅ | Inject `undefined` for an unresolved dependency instead of throwing. |
257
+ | `delay` | `(factory: () => Constructor) => unknown` | `3.0.0` | Stable ✅ | Lazily resolve a token to break a circular dependency. |
258
+ | `markInjectable` | `(target: Constructor) => void` | `3.0.0` | Stable ✅ | Make a class resolvable without service metadata (used by `@Controller`). |
259
+ | `hasServiceMetadata` · `getServiceType` · `getServiceScope` · `getConfigPrefix` | `(target) => …` | `3.0.0` | Stable ✅ | Metadata readers for discovery / diagnostics. |
260
+ | `getOptionalParams` · `isParameterOptional` | `(target[, index]) => …` | `3.0.0` | Stable ✅ | Read `@Optional()` parameter markers. |
261
+ | `DIError` · `DependencyResolutionError` · `CircularDependencyError` · `InvalidProviderError` | `class` | `3.0.0` | Stable ✅ | The DI error hierarchy. |
262
+ | `METADATA_KEYS` | `Readonly<Record<string, string>>` | `3.0.0` | Stable ✅ | The metadata key constants decorators write under. |
263
+ | `type Container` · `Scope` · `Token` · `Provider` · `ClassProvider` · `FactoryProvider` · `ValueProvider` · `RegisterOptions` · `ServiceOptions` · `ConfigOptions` · `Constructor` | — | `3.0.0` | Stable ✅ | Public contracts (re-exported from `@nextrush/types`). |
237
264
 
238
- ```typescript
239
- import { hasServiceMetadata, Service } from '@nextrush/di';
265
+ The `Container` interface exposes: `register`, `resolve`, `resolveAsync`, `bootstrap`, `resolveAll`, `isRegistered`, `clearInstances`, `reset`, and `createChild`.
240
266
 
241
- @Service()
242
- class MyService {}
267
+ ## Options
243
268
 
244
- hasServiceMetadata(MyService); // true
245
- ```
269
+ `@nextrush/di` is decorator- and API-driven; the configurable inputs are the decorator/registration option bags.
246
270
 
247
- #### `getServiceType(target)`
271
+ | Option | Type | Required | Default | Security-sensitive | Description |
272
+ | ------ | ---- | -------- | ------- | ------------------ | ----------- |
273
+ | `ServiceOptions.scope` | `'singleton' \| 'transient' \| 'request'` | No | `'singleton'` | — | Lifecycle for `@Service()` / `@Repository()`. |
274
+ | `RegisterOptions.scope` | `'singleton' \| 'transient' \| 'request'` | No | class's declared scope (else `'singleton'` for a decorated class) | — | Per-`register()` override; an explicit scope wins over the class's declared scope. |
275
+ | `ConfigOptions.prefix` | `string` | No | `undefined` | — | Environment-variable prefix recorded on a `@Config()` class (documents the `PREFIX_*` vars it reads). |
248
276
 
249
- Get the service type from a decorated class. Returns `'service'`, `'repository'`, or `undefined`.
277
+ ## Compatibility
250
278
 
251
- #### `getServiceScope(target)`
279
+ **Requirements**
252
280
 
253
- Get the scope from a decorated class. Returns `'singleton'`, `'transient'`, or `undefined`.
281
+ | Requirement | Version |
282
+ | ----------- | ------- |
283
+ | NextRush | `3.x` |
284
+ | Node.js | `>=22` |
285
+ | TypeScript | `>=5.x` (with `experimentalDecorators` + `emitDecoratorMetadata`) |
254
286
 
255
- ### Container
287
+ **Runtimes**
256
288
 
257
- #### `container.register(token, provider)`
289
+ | Runtime | Supported | Notes |
290
+ | ------- | --------- | ----- |
291
+ | Node.js `>=22` | ✅ | ESM-only |
292
+ | Bun / Deno / Edge | ✅ / ✅ / ✅ | Uses only `tsyringe` + `reflect-metadata` — no `node:*` APIs — so behavior is identical across runtimes |
258
293
 
259
- Register a dependency with the container.
294
+ **Integration**
295
+ - **Peer dependencies:** `tsyringe@^4.10.0` and `reflect-metadata@^0.2.2` (runtime), plus `@nextrush/types` (contract, types erased at build).
296
+ - **Works with:** [`nextrush/class`](../class) (re-exports the common surface and drives the container per request), [`@nextrush/core`](../core) (each `Application` may own a container).
297
+ - **Incompatible with:** none.
260
298
 
261
- ```typescript
262
- // Class provider
263
- container.register(UserService, { useClass: UserService });
299
+ > [!IMPORTANT]
300
+ > NextRush is **ESM-only, permanently** — no CommonJS build. On Node `>=22`, CommonJS consumers can
301
+ > `require()` this ESM package natively. See the
302
+ > [Module Format Policy](https://github.com/0xTanzim/nextRush#module-format-policy).
264
303
 
265
- // Value provider
266
- container.register('CONFIG', { useValue: { port: 3000 } });
304
+ ---
267
305
 
268
- // Factory provider — receives the container for nested resolution
269
- container.register(Logger, {
270
- useFactory: (c) => new Logger(c.resolve('CONFIG')),
271
- });
272
- ```
306
+ ## Troubleshooting
273
307
 
274
- #### `container.resolve(token)`
308
+ <details>
309
+ <summary><strong><code>DependencyResolutionError</code>: a token is "not registered in the container"</strong></summary>
275
310
 
276
- Resolve a dependency from the container.
311
+ **Cause:** the class you're resolving depends on a token nothing registered — usually a missing `@Service()` / `@Repository()` decorator, an interface injected without `@inject('TOKEN')`, or a module imported after `resolve()` ran. **Fix:** decorate the class, inject interface/string tokens explicitly, or register it manually.
277
312
 
278
- ```typescript
279
- const service = container.resolve(UserService);
280
- const config = container.resolve<Config>('CONFIG');
313
+ ```ts
314
+ container.register(UserRepository, { useClass: UserRepository });
315
+ // or add @Repository() to the class, or @inject('IUserRepository') on the parameter
281
316
  ```
282
317
 
283
- #### `container.resolveAll(token)`
284
-
285
- Resolve all dependencies registered under a token. Returns an empty array if none are registered.
318
+ </details>
286
319
 
287
- ```typescript
288
- container.register('Plugin', { useValue: pluginA });
289
- container.register('Plugin', { useValue: pluginB });
320
+ <details>
321
+ <summary><strong><code>CircularDependencyError</code>: "circular dependency detected"</strong></summary>
290
322
 
291
- const plugins = container.resolveAll<Plugin>('Plugin');
292
- ```
323
+ **Cause:** two (or more) classes depend on each other, so neither can be constructed first. The container detects the cycle and names it before the call stack overflows. **Fix:** break the cycle — extract shared logic into a third service, or lazily resolve one side with `delay()`.
293
324
 
294
- #### `container.isRegistered(token)`
325
+ ```ts
326
+ import { inject, delay, Service } from '@nextrush/di';
295
327
 
296
- Check if a token is registered.
297
-
298
- ```typescript
299
- if (container.isRegistered(UserService)) {
300
- // ...
328
+ @Service()
329
+ class A {
330
+ constructor(@inject(delay(() => B)) private readonly b: B) {}
301
331
  }
302
332
  ```
303
333
 
304
- #### `container.clearInstances()`
334
+ </details>
305
335
 
306
- Clear cached singleton instances. Registrations remain — the next `resolve()` creates fresh instances.
336
+ <details>
337
+ <summary><strong>Decorators throw <code>Reflect.getMetadata is not a function</code> or types don't inject</strong></summary>
307
338
 
308
- ```typescript
309
- beforeEach(() => {
310
- container.clearInstances();
311
- });
312
- ```
339
+ **Cause:** `reflect-metadata` wasn't loaded, or `emitDecoratorMetadata` is off, so no constructor type information exists. **Fix:** ensure `reflect-metadata` is imported once at your entry point (importing `@nextrush/di` / `nextrush/class` does this), and enable `experimentalDecorators` + `emitDecoratorMetadata` in `tsconfig.json`.
313
340
 
314
- #### `container.reset()`
341
+ </details>
315
342
 
316
- Reset the container completely, removing all registrations and instances.
343
+ <details>
344
+ <summary><strong>A <code>request</code>-scoped service behaves like a singleton</strong></summary>
317
345
 
318
- #### `container.createChild()`
346
+ **Cause:** `request` scope only yields a per-request instance when resolved from a per-request **child** container. Resolving from the root container shares one instance. **Fix:** resolve from `container.createChild()` per request — the [`nextrush/class`](../class) runtime does this automatically for request-scoped controllers and their dependencies.
319
347
 
320
- Create a child container. The child inherits parent registrations but can override them independently.
348
+ </details>
321
349
 
322
- #### `createContainer()`
350
+ ## FAQ
323
351
 
324
- Create a new isolated container with no inherited registrations.
352
+ **Can I use `@nextrush/di` without the rest of NextRush?**
353
+ Yes. The container, decorators, and errors are standalone — they depend on `tsyringe`, `reflect-metadata`, and the types-only `@nextrush/types`. Nothing ties them to the HTTP layer.
325
354
 
326
- ```typescript
327
- const testContainer = createContainer();
328
- testContainer.register(UserService, { useClass: MockUserService });
329
- ```
355
+ **Why ESM-only?**
356
+ See the [Module Format Policy](https://github.com/0xTanzim/nextRush#module-format-policy).
330
357
 
331
- ## Error Handling
332
-
333
- All errors extend `DIError` and include actionable messages:
334
-
335
- ```typescript
336
- import {
337
- DIError,
338
- DependencyResolutionError,
339
- CircularDependencyError,
340
- TypeInferenceError,
341
- MissingDependencyError,
342
- InvalidProviderError,
343
- ContainerDisposedError,
344
- } from '@nextrush/di';
345
-
346
- try {
347
- container.resolve(UnregisteredService);
348
- } catch (error) {
349
- if (error instanceof DependencyResolutionError) {
350
- console.log(error.missingDependency); // token name
351
- console.log(error.chain); // resolution path
352
- }
353
- if (error instanceof CircularDependencyError) {
354
- console.log(error.cycle); // ['ServiceA', 'ServiceB', ...]
355
- }
356
- }
357
- ```
358
+ **Does it work on Bun, Deno, and Edge?**
359
+ Yes. The package uses only `tsyringe` and `reflect-metadata` (no `node:*` APIs), so resolution behaves identically across every supported runtime.
358
360
 
359
- | Error | Cause |
360
- | --------------------------- | --------------------------------------------------------------------------------------------- |
361
- | `DependencyResolutionError` | Token not registered — includes fix suggestions (`@Service()`, import order, manual register) |
362
- | `CircularDependencyError` | Circular dependency detected (wrapper-level + tsyringe-internal chains) |
363
- | `MissingDependencyError` | _(Deprecated)_ Use `DependencyResolutionError` instead |
364
- | `InvalidProviderError` | Provider missing `useClass`, `useValue`, or `useFactory` |
365
- | `TypeInferenceError` | Constructor parameter type not available at runtime |
366
- | `ContainerDisposedError` | Container has been reset or disposed |
367
-
368
- ## TypeScript Exports
369
-
370
- ```typescript
371
- // Container
372
- import { container, createContainer } from '@nextrush/di';
373
-
374
- // Decorators
375
- import { Service, Repository, AutoInjectable, Optional, inject, delay } from '@nextrush/di';
376
-
377
- // Utility functions
378
- import {
379
- hasServiceMetadata,
380
- getServiceType,
381
- getServiceScope,
382
- isParameterOptional,
383
- getOptionalParams,
384
- } from '@nextrush/di';
385
-
386
- // Metadata keys
387
- import { METADATA_KEYS } from '@nextrush/di';
388
-
389
- // Error classes
390
- import {
391
- DIError,
392
- DependencyResolutionError,
393
- CircularDependencyError,
394
- MissingDependencyError,
395
- InvalidProviderError,
396
- TypeInferenceError,
397
- ContainerDisposedError,
398
- } from '@nextrush/di';
399
-
400
- // Types
401
- import type {
402
- ContainerInterface,
403
- Provider,
404
- ClassProvider,
405
- ValueProvider,
406
- FactoryProvider,
407
- Token,
408
- Constructor,
409
- Scope,
410
- ServiceOptions,
411
- } from '@nextrush/di';
412
- ```
361
+ **How is `request` scope different from `transient`?**
362
+ `transient` builds a fresh instance on *every* resolve; `request` builds one instance per request and shares it across every collaborator resolved within that request's child container. Two services that both depend on a `request`-scoped value receive the *same* value within one request.
413
363
 
414
- ## Integration with Guards
364
+ ---
415
365
 
416
- Class-based guards implementing `CanActivate` are resolved from the DI container:
366
+ ## Package relationships
417
367
 
418
- ```typescript
419
- import { Service } from '@nextrush/di';
420
- import type { CanActivate, GuardContext } from '@nextrush/decorators';
421
-
422
- @Service()
423
- class AuthGuard implements CanActivate {
424
- constructor(private authService: AuthService) {}
425
-
426
- async canActivate(ctx: GuardContext): Promise<boolean> {
427
- const token = ctx.get('authorization');
428
- if (!token) return false;
429
-
430
- const user = await this.authService.verify(token);
431
- ctx.state.user = user;
432
- return Boolean(user);
433
- }
434
- }
368
+ ```text
369
+ depends on @nextrush/types (Container / Scope / Provider contract, types only)
370
+ @nextrush/di ----------------> tsyringe · reflect-metadata (runtime the container wraps tsyringe)
371
+ re-exported by @nextrush/class (Service, Repository, container, createContainer, inject)
372
+ used by @nextrush/class (resolves controllers, guards, interceptors, filters)
435
373
  ```
436
374
 
437
- The `@nextrush/controllers` plugin automatically detects class guards and resolves them from the container.
438
-
439
- ## Troubleshooting
375
+ - **Depends on:** [`@nextrush/types`](../types) the shared `Container` / `Scope` / `Provider` / `Token` contracts (types, erased at build) · `tsyringe` + `reflect-metadata` (runtime).
376
+ - **Re-exported by:** [`nextrush/class`](../class) — `Service`, `Repository`, `container`, `createContainer`, `inject`, and the `Container` type reach users through the class runtime.
377
+ - **Used by:** [`@nextrush/class`](../class) — resolves controllers, guards, interceptors, and exception filters from the container, and drives request scope.
378
+ - **Alternative:** none within NextRush — this is the framework's DI container.
440
379
 
441
- ### Error: "TypeInfo not known for X"
380
+ ## Architecture
442
381
 
443
- **Cause**: `emitDecoratorMetadata` is not being emitted at runtime.
444
-
445
- **Fix**: Use `@nextrush/dev` for development. It automatically handles metadata emission.
446
-
447
- ```bash
448
- # ❌ Doesn't work (no decorator metadata)
449
- npx tsx src/index.ts
450
-
451
- # ✅ Works (full decorator support)
452
- nextrush dev
453
- ```
382
+ Maintaining or contributing to this package? The internal design — how the container wraps `tsyringe`,
383
+ the scope-to-lifecycle mapping, the per-request child container behind `request` scope, circular- and
384
+ missing-dependency detection, the architectural invariants, and the decisions and trade-offs behind
385
+ them (with diagrams) — is in **[`ARCHITECTURE.md`](./ARCHITECTURE.md)**. Design history:
386
+ [ADR-0005 (package tiers & sealed surface)](https://github.com/0xTanzim/nextRush/blob/main/docs/adr/ADR-0005-package-tiers-sealed-surface-deprecation.md).
454
387
 
455
- ### Error: "reflect-metadata not found"
388
+ ## Resources
456
389
 
457
- **Cause**: `reflect-metadata` must be imported before decorators.
458
-
459
- **Fix**: Import it first in your entry point:
460
-
461
- ```typescript
462
- import 'reflect-metadata'; // MUST be first!
463
- import { Service } from '@nextrush/di';
464
- ```
465
-
466
- ### Constructor parameters not injected
467
-
468
- **Cause**: Class is missing `@Service()` decorator.
469
-
470
- **Fix**: Add the decorator:
471
-
472
- ```typescript
473
- @Service() // Required for DI!
474
- class MyService {
475
- constructor(private dep: SomeDependency) {}
476
- }
477
- ```
390
+ - 📖 **Learn** [Documentation](https://0xtanzim.github.io/nextRush/docs) · [Architecture](./ARCHITECTURE.md) · [RFCs](https://github.com/0xTanzim/nextRush/tree/main/docs/RFC)
391
+ - 📝 **Changelog** — [CHANGELOG.md](./CHANGELOG.md)
392
+ - 🐛 **Report an issue** [GitHub Issues](https://github.com/0xTanzim/nextRush/issues)
393
+ - 🤝 **Contribute** — [CONTRIBUTING.md](https://github.com/0xTanzim/nextRush/blob/main/CONTRIBUTING.md)
478
394
 
479
- ## License
395
+ ---
480
396
 
481
- MIT
397
+ MIT © [Tanzim Hossain](https://github.com/0xTanzim)