@nextrush/di 3.0.7 → 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 +264 -348
- package/dist/index.d.ts +119 -268
- package/dist/index.js +188 -217
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -1,481 +1,397 @@
|
|
|
1
1
|
# @nextrush/di
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+
[](https://www.npmjs.com/package/@nextrush/di)
|
|
6
|
+
[](https://www.npmjs.com/package/@nextrush/di)
|
|
7
|
+
[](https://bundlephobia.com/package/@nextrush/di)
|
|
8
|
+
[](https://www.npmjs.com/package/@nextrush/di)
|
|
9
|
+
[](https://nodejs.org/api/esm.html)
|
|
10
|
+
[](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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
61
|
+
**Use `@nextrush/di` if:**
|
|
17
62
|
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
68
|
+
**Reach for something else if:**
|
|
23
69
|
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
>
|
|
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
|
-
|
|
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
|
-
|
|
93
|
+
## Quick start
|
|
58
94
|
|
|
59
|
-
```
|
|
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: '
|
|
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
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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
|
-
|
|
121
|
+
## Capabilities
|
|
97
122
|
|
|
98
|
-
|
|
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
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
144
|
+
## Mental model
|
|
118
145
|
|
|
119
|
-
|
|
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
|
-
```
|
|
122
|
-
@Repository()
|
|
123
|
-
class
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
135
|
-
const DATABASE_TOKEN = Symbol('Database');
|
|
164
|
+
---
|
|
136
165
|
|
|
137
|
-
|
|
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
|
-
|
|
168
|
+
### Mark a service and inject it
|
|
147
169
|
|
|
148
|
-
|
|
170
|
+
```ts
|
|
171
|
+
import { Service } from '@nextrush/di';
|
|
149
172
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
157
|
-
|
|
178
|
+
@Service()
|
|
179
|
+
class OrderService {
|
|
180
|
+
constructor(private readonly logger: Logger) {} // injected automatically
|
|
181
|
+
}
|
|
158
182
|
```
|
|
159
183
|
|
|
160
|
-
|
|
184
|
+
### Choose a scope
|
|
161
185
|
|
|
162
|
-
|
|
186
|
+
```ts
|
|
187
|
+
import { Service } from '@nextrush/di';
|
|
163
188
|
|
|
164
|
-
|
|
189
|
+
@Service() // singleton — one shared instance
|
|
190
|
+
class ConfigService {}
|
|
165
191
|
|
|
166
|
-
|
|
167
|
-
|
|
192
|
+
@Service({ scope: 'transient' }) // a fresh instance on every resolve
|
|
193
|
+
class Formatter {}
|
|
168
194
|
|
|
169
|
-
@Service()
|
|
170
|
-
class
|
|
171
|
-
|
|
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
|
-
|
|
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
|
-
|
|
203
|
+
### Inject by token, or make a dependency optional
|
|
183
204
|
|
|
184
|
-
```
|
|
185
|
-
import { Service,
|
|
205
|
+
```ts
|
|
206
|
+
import { Service, inject, Optional } from '@nextrush/di';
|
|
186
207
|
|
|
187
208
|
@Service()
|
|
188
209
|
class NotificationService {
|
|
189
210
|
constructor(
|
|
190
|
-
@
|
|
191
|
-
@
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
Check if a specific constructor parameter is marked as optional.
|
|
217
|
+
### Register a value or a factory manually
|
|
208
218
|
|
|
209
|
-
```
|
|
210
|
-
import {
|
|
219
|
+
```ts
|
|
220
|
+
import { container } from '@nextrush/di';
|
|
211
221
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
constructor(@Optional() private dep?: SomeDep) {}
|
|
215
|
-
}
|
|
222
|
+
container.register('CONFIG', { useValue: { port: 8080 } });
|
|
223
|
+
container.register('CLOCK', { useFactory: () => new Date() });
|
|
216
224
|
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
Get all optional parameter indices for a class. Returns a `ReadonlySet<number>`.
|
|
233
|
+
### Isolate a container for tests
|
|
224
234
|
|
|
225
|
-
```
|
|
226
|
-
import {
|
|
235
|
+
```ts
|
|
236
|
+
import { createContainer } from '@nextrush/di';
|
|
227
237
|
|
|
228
|
-
const
|
|
229
|
-
|
|
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
|
-
|
|
243
|
+
## API overview
|
|
233
244
|
|
|
234
|
-
|
|
245
|
+
The sealed public surface (ADR-0005), grouped by role.
|
|
235
246
|
|
|
236
|
-
|
|
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
|
-
|
|
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
|
-
|
|
242
|
-
class MyService {}
|
|
267
|
+
## Options
|
|
243
268
|
|
|
244
|
-
|
|
245
|
-
```
|
|
269
|
+
`@nextrush/di` is decorator- and API-driven; the configurable inputs are the decorator/registration option bags.
|
|
246
270
|
|
|
247
|
-
|
|
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
|
-
|
|
277
|
+
## Compatibility
|
|
250
278
|
|
|
251
|
-
|
|
279
|
+
**Requirements**
|
|
252
280
|
|
|
253
|
-
|
|
281
|
+
| Requirement | Version |
|
|
282
|
+
| ----------- | ------- |
|
|
283
|
+
| NextRush | `3.x` |
|
|
284
|
+
| Node.js | `>=22` |
|
|
285
|
+
| TypeScript | `>=5.x` (with `experimentalDecorators` + `emitDecoratorMetadata`) |
|
|
254
286
|
|
|
255
|
-
|
|
287
|
+
**Runtimes**
|
|
256
288
|
|
|
257
|
-
|
|
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
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
266
|
-
container.register('CONFIG', { useValue: { port: 3000 } });
|
|
304
|
+
---
|
|
267
305
|
|
|
268
|
-
|
|
269
|
-
container.register(Logger, {
|
|
270
|
-
useFactory: (c) => new Logger(c.resolve('CONFIG')),
|
|
271
|
-
});
|
|
272
|
-
```
|
|
306
|
+
## Troubleshooting
|
|
273
307
|
|
|
274
|
-
|
|
308
|
+
<details>
|
|
309
|
+
<summary><strong><code>DependencyResolutionError</code>: a token is "not registered in the container"</strong></summary>
|
|
275
310
|
|
|
276
|
-
|
|
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
|
-
```
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
Resolve all dependencies registered under a token. Returns an empty array if none are registered.
|
|
318
|
+
</details>
|
|
286
319
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
container.register('Plugin', { useValue: pluginB });
|
|
320
|
+
<details>
|
|
321
|
+
<summary><strong><code>CircularDependencyError</code>: "circular dependency detected"</strong></summary>
|
|
290
322
|
|
|
291
|
-
|
|
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
|
-
|
|
325
|
+
```ts
|
|
326
|
+
import { inject, delay, Service } from '@nextrush/di';
|
|
295
327
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
334
|
+
</details>
|
|
305
335
|
|
|
306
|
-
|
|
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
|
-
|
|
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
|
-
|
|
341
|
+
</details>
|
|
315
342
|
|
|
316
|
-
|
|
343
|
+
<details>
|
|
344
|
+
<summary><strong>A <code>request</code>-scoped service behaves like a singleton</strong></summary>
|
|
317
345
|
|
|
318
|
-
|
|
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
|
-
|
|
348
|
+
</details>
|
|
321
349
|
|
|
322
|
-
|
|
350
|
+
## FAQ
|
|
323
351
|
|
|
324
|
-
|
|
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
|
-
|
|
327
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
364
|
+
---
|
|
415
365
|
|
|
416
|
-
|
|
366
|
+
## Package relationships
|
|
417
367
|
|
|
418
|
-
```
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
@
|
|
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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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
|
-
|
|
380
|
+
## Architecture
|
|
442
381
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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
|
-
|
|
388
|
+
## Resources
|
|
456
389
|
|
|
457
|
-
**
|
|
458
|
-
|
|
459
|
-
|
|
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
|
-
|
|
395
|
+
---
|
|
480
396
|
|
|
481
|
-
MIT
|
|
397
|
+
MIT © [Tanzim Hossain](https://github.com/0xTanzim)
|