@nextrush/class 1.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/LICENSE +21 -0
- package/README.md +456 -0
- package/dist/index.d.ts +2306 -0
- package/dist/index.js +2371 -0
- package/dist/index.js.map +1 -0
- package/package.json +68 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tanzim (NextRush)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
# @nextrush/class
|
|
2
|
+
|
|
3
|
+
> The unified class-based runtime for NextRush — `@Controller` + route/parameter decorators, guards, interceptors, exception filters, lifecycle hooks, modules, request scope, and automatic controller registration, with dependency injection re-exported from `@nextrush/di`.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@nextrush/class)
|
|
6
|
+
[](https://www.npmjs.com/package/@nextrush/class)
|
|
7
|
+
[](https://bundlephobia.com/package/@nextrush/class)
|
|
8
|
+
[](https://www.npmjs.com/package/@nextrush/class)
|
|
9
|
+
[](https://nodejs.org/api/esm.html)
|
|
10
|
+
[](https://github.com/0xTanzim/nextRush/blob/main/LICENSE)
|
|
11
|
+
|
|
12
|
+
| | |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| **Purpose** | Structure a NextRush app as classes — decorate a controller, declare routes and parameters, add guards/interceptors/filters, and let `registerControllers` / `registerModule` wire the whole graph |
|
|
15
|
+
| **Package type** | Core (Registrar + decorator runtime) |
|
|
16
|
+
| **Status** | Stable ✅ |
|
|
17
|
+
| **Included in `nextrush`?** | ✅ Yes — via the `nextrush/class` subpath (`import { Controller, Get } from 'nextrush/class'`) |
|
|
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 for the decorator + request pipeline · filesystem discovery is Node-only (see [Compatibility](#compatibility)) |
|
|
21
|
+
| **Requires** | Node `>=22` · ESM-only · TypeScript `>=5.x` (with `experimentalDecorators` + `emitDecoratorMetadata`) |
|
|
22
|
+
| **Introduced** | `v3.0.0` (consolidated into `@nextrush/class` in `v3.1.0`) |
|
|
23
|
+
|
|
24
|
+
## Highlights
|
|
25
|
+
|
|
26
|
+
- 🧩 **One import for the whole class API** — controllers, routes, params, guards, interceptors, filters, modules, lifecycle, and DI, from `nextrush/class`
|
|
27
|
+
- 🏗️ **Registrar, not a framework rewrite** — `registerControllers` / `registerModule` read `app.router` + `app.container` and build routes; the functional core is untouched
|
|
28
|
+
- 🔁 **Request scope that bubbles** — a request-scoped dependency anywhere in a controller's graph makes that controller resolve fresh per request; pure-singleton graphs keep the memoized fast path
|
|
29
|
+
- ⚡ **Zero reflection on the request path** — decorator metadata is read once at bootstrap and baked into an immutable route graph; requests execute pre-built handlers
|
|
30
|
+
- 🧰 **Fail-at-boot validation + opt-in diagnostics** — unresolvable/circular controllers and guards throw at registration, not as a first-request 500
|
|
31
|
+
- ✅ **ESM-only**, **fully typed** — strict TypeScript, zero `any`
|
|
32
|
+
|
|
33
|
+
<details>
|
|
34
|
+
<summary><strong>Table of contents</strong></summary>
|
|
35
|
+
|
|
36
|
+
[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)
|
|
37
|
+
|
|
38
|
+
</details>
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## The problem
|
|
43
|
+
|
|
44
|
+
The functional API is direct for a handful of routes, but a growing app repeats the same wiring by hand: pull each value off `ctx`, look up dependencies, thread shared services through every handler, and re-implement auth checks and error mapping per route. The controller's actual job — take an id, return a user — gets buried under plumbing.
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
// TODAY, without the class runtime — every handler re-does the same extraction,
|
|
48
|
+
// wiring, guarding, and error mapping by hand:
|
|
49
|
+
router.get('/users/:id', async (ctx) => {
|
|
50
|
+
const token = ctx.get('authorization');
|
|
51
|
+
if (!token || !(await verify(token))) { // auth, inline, copy-pasted per route
|
|
52
|
+
ctx.status = 403;
|
|
53
|
+
return ctx.json({ error: 'forbidden' });
|
|
54
|
+
}
|
|
55
|
+
const db = getDb(); // dependency lookup, by hand
|
|
56
|
+
const repo = new UserRepository(db); // construction, by hand
|
|
57
|
+
const id = ctx.params.id; // extraction, by hand
|
|
58
|
+
ctx.json(await repo.findOne(id));
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
As routes multiply, the extraction, construction, auth, and error handling are duplicated at every call site, and a shared instance (a connection pool) can be built twice by accident. `@nextrush/class` moves each of those into a declaration: `@Param('id')` extracts, the constructor declares dependencies, `@UseGuard` attaches auth once, `@UseFilter` maps errors, and the registrar builds the routes.
|
|
63
|
+
|
|
64
|
+
## When to use
|
|
65
|
+
|
|
66
|
+
`@nextrush/class` is the class-based half of NextRush. It sits on top of the same `Application` and router the functional API uses — you are not choosing a different framework, only a different authoring style for the parts that benefit from structure.
|
|
67
|
+
|
|
68
|
+
**Use `@nextrush/class` if:**
|
|
69
|
+
|
|
70
|
+
- ✓ You want controllers with declarative routes (`@Get`, `@Post`, …) and typed parameter extraction (`@Body`, `@Param`, `@Query`, …)
|
|
71
|
+
- ✓ You want dependency injection for services shared across routes, with singleton / transient / request scopes
|
|
72
|
+
- ✓ You want cross-cutting concerns as reusable units — `@UseGuard` for auth, `@UseInterceptor` for wrapping/timing, `@UseFilter` + `@Catch` for error mapping
|
|
73
|
+
- ✓ You want feature modules (`@Module`) and one-call registration of a whole app graph (`registerModule`)
|
|
74
|
+
|
|
75
|
+
**Reach for something else if:**
|
|
76
|
+
|
|
77
|
+
- ✗ You have a small, flat set of routes and prefer no decorators or reflection → use the functional API from [`nextrush`](../nextrush) (`createApp` / `createRouter`)
|
|
78
|
+
- ✗ You only need the DI container on its own, or the parts this package does not re-export (`@Config`, `@Injectable`, `@Optional`, `delay`, DI error classes) → use [`@nextrush/di`](../di) directly
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Installation
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
pnpm add nextrush
|
|
86
|
+
# npm i nextrush · yarn add nextrush · bun add nextrush
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The class API is exposed through the `nextrush/class` subpath, so installing `nextrush` is enough. Install the package directly (`pnpm add @nextrush/class`) only if you depend on it without the meta package.
|
|
90
|
+
|
|
91
|
+
> [!NOTE]
|
|
92
|
+
> Already using `nextrush`? The whole class surface — decorators, `registerControllers`,
|
|
93
|
+
> `registerModule`, and the re-exported DI (`Service`, `container`, …) — imports from
|
|
94
|
+
> `nextrush/class`. You do not install `@nextrush/class` separately.
|
|
95
|
+
|
|
96
|
+
> [!IMPORTANT]
|
|
97
|
+
> Decorators need `reflect-metadata` loaded once before any decorated class is defined, and your
|
|
98
|
+
> `tsconfig.json` needs `"experimentalDecorators": true` and `"emitDecoratorMetadata": true`.
|
|
99
|
+
> Importing `nextrush/class` loads `reflect-metadata` for you — no manual import required.
|
|
100
|
+
|
|
101
|
+
## Quick start
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
import { createApp, listen } from 'nextrush';
|
|
105
|
+
import { Controller, Get, Post, Param, Body, Service, registerControllers } from 'nextrush/class';
|
|
106
|
+
|
|
107
|
+
@Service() // singleton by default — @Controller injects it automatically
|
|
108
|
+
class UserService {
|
|
109
|
+
private users = [{ id: '1', name: 'Ada' }];
|
|
110
|
+
findAll() { return this.users; }
|
|
111
|
+
findOne(id: string) { return this.users.find((u) => u.id === id) ?? null; }
|
|
112
|
+
create(data: { name: string }) {
|
|
113
|
+
const user = { id: String(this.users.length + 1), ...data };
|
|
114
|
+
this.users.push(user);
|
|
115
|
+
return user;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
@Controller('/users')
|
|
120
|
+
class UserController {
|
|
121
|
+
constructor(private readonly users: UserService) {} // resolved from DI
|
|
122
|
+
|
|
123
|
+
@Get()
|
|
124
|
+
findAll() { return this.users.findAll(); }
|
|
125
|
+
|
|
126
|
+
@Get('/:id')
|
|
127
|
+
findOne(@Param('id') id: string) { return this.users.findOne(id); }
|
|
128
|
+
|
|
129
|
+
@Post()
|
|
130
|
+
create(@Body() data: { name: string }) { return this.users.create(data); }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const app = createApp();
|
|
134
|
+
await registerControllers(app, { controllers: [UserController] });
|
|
135
|
+
listen(app, 8080);
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
You never construct `UserService` or read `ctx` by hand. `@Controller` marks the class resolvable, the constructor declares its dependency, `@Param('id')` extracts the route param, and `registerControllers` builds and mounts the routes on `app.router` — after eagerly resolving every controller once so a broken dependency fails here, not on the first request.
|
|
139
|
+
|
|
140
|
+
## Capabilities
|
|
141
|
+
|
|
142
|
+
**Routing & extraction**
|
|
143
|
+
- **`@Controller(path)`** — mark a class as an HTTP controller (path derived from the class name if omitted)
|
|
144
|
+
- **Route decorators** — `@Get` `@Post` `@Put` `@Patch` `@Delete` `@Head` `@Options` `@All`
|
|
145
|
+
- **Parameter decorators** — `@Body` `@Param` `@Query` `@Header` `@Ctx` `@Req` `@Res`, with `transform` on `@Body`/`@Param`/`@Query` and `defaultValue` on `@Param`/`@Query`/`@Header` (`@Ctx`/`@Req`/`@Res` take no options), plus `createCustomParamDecorator` for your own extractors
|
|
146
|
+
- **Response decorators** — `@HttpCode(status)`, `@Redirect(url, status?)`, `@SetHeader(name, value)`
|
|
147
|
+
|
|
148
|
+
**Cross-cutting concerns**
|
|
149
|
+
- **Guards** — `@UseGuard(...)` accepts a `GuardFn` or a `CanActivate` class (resolved from DI); a `false` return throws a 403, a thrown `HttpError` keeps its own status
|
|
150
|
+
- **Interceptors** — `@UseInterceptor(...)` wraps the handler (onion / around advice); the return value replaces the result
|
|
151
|
+
- **Exception filters** — `@Catch(...ErrorTypes)` + `@UseFilter(...)` map thrown errors to responses; unmatched errors fall through to the global error middleware
|
|
152
|
+
|
|
153
|
+
**Composition & lifecycle**
|
|
154
|
+
- **Dependency injection** — `@Service` / `@Repository` with `singleton` / `transient` / `request` scopes, re-exported from [`@nextrush/di`](../di)
|
|
155
|
+
- **Modules** — `@Module({ imports, controllers, providers, exports })` + `registerModule` wire a feature graph in one call
|
|
156
|
+
- **Lifecycle hooks** — duck-typed `OnInit` / `OnShutdown` (no decorator) run at `app.ready()` / `app.close()` in dependency order
|
|
157
|
+
- **Request scope** — bubbles automatically: a request-scoped dependency makes its whole controller graph resolve per request
|
|
158
|
+
|
|
159
|
+
**Registration & tooling**
|
|
160
|
+
- **`registerControllers`** — filesystem auto-discovery (`root`) or an explicit `controllers` list, with fail-at-boot validation
|
|
161
|
+
- **Discovery sources** — `FilesystemSource` / `MemorySource` behind the `DiscoverySource` interface
|
|
162
|
+
- **Diagnostics** — opt-in (`diagnostics: true`) route/provider/duplicate/cycle/timing report via `getClassDiagnostics(app)`
|
|
163
|
+
|
|
164
|
+
**Performance & DX**
|
|
165
|
+
- **Zero reflection per request** — metadata is read once at bootstrap and baked into an immutable route graph
|
|
166
|
+
- **Memoized singleton controllers** — the default path adds no per-request resolution cost
|
|
167
|
+
- **Fully typed** — strict TypeScript, zero `any`
|
|
168
|
+
|
|
169
|
+
## Mental model
|
|
170
|
+
|
|
171
|
+
Decorators only *record metadata* on your classes. Nothing runs until `registerControllers` (or `registerModule`) reads that metadata at bootstrap, builds one handler per route, and mounts them on the app's router. At request time, the pre-built handler runs the pipeline in a fixed order.
|
|
172
|
+
|
|
173
|
+
```text
|
|
174
|
+
@Controller / @Get / @Body / @UseGuard ... (decorators record metadata)
|
|
175
|
+
|
|
|
176
|
+
v
|
|
177
|
+
registerControllers(app, options) --- reads app.router + app.container,
|
|
178
|
+
| builds routes ONCE at bootstrap
|
|
179
|
+
v
|
|
180
|
+
per request: guards --> resolve controller --> params --> interceptors
|
|
181
|
+
--> handler --> response (all wrapped by exception filters)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**Rule:** decorators declare, the registrar builds at bootstrap, and requests execute a baked handler — so no decorator metadata is read on the hot path.
|
|
185
|
+
|
|
186
|
+
> [!TIP]
|
|
187
|
+
> The full request pipeline, the bootstrap stages, and the request-scope lifecycle (with Mermaid
|
|
188
|
+
> diagrams) are in [`ARCHITECTURE.md`](./ARCHITECTURE.md).
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Common tasks
|
|
193
|
+
|
|
194
|
+
### Define a controller with typed parameters
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
import { Controller, Get, Post, Param, Query, Body } from 'nextrush/class';
|
|
198
|
+
|
|
199
|
+
@Controller('/articles')
|
|
200
|
+
class ArticleController {
|
|
201
|
+
@Get()
|
|
202
|
+
list(@Query('page', { defaultValue: 1, transform: Number }) page: number) {
|
|
203
|
+
return { page };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
@Get('/:id')
|
|
207
|
+
read(@Param('id') id: string) {
|
|
208
|
+
return { id };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
@Post()
|
|
212
|
+
create(@Body() data: { title: string }) {
|
|
213
|
+
return { created: data.title };
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Protect a route with a guard
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
import { Controller, Get, UseGuard, Service } from 'nextrush/class';
|
|
222
|
+
import type { CanActivate, GuardContext } from 'nextrush/class';
|
|
223
|
+
|
|
224
|
+
@Service()
|
|
225
|
+
class AuthGuard implements CanActivate {
|
|
226
|
+
canActivate(ctx: GuardContext): boolean {
|
|
227
|
+
return Boolean(ctx.get('authorization')); // false -> 403; throw an HttpError for another status
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
@Controller('/admin')
|
|
232
|
+
@UseGuard(AuthGuard) // class-level: applies to every route; resolved from DI
|
|
233
|
+
class AdminController {
|
|
234
|
+
@Get()
|
|
235
|
+
dashboard() {
|
|
236
|
+
return { ok: true };
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Map errors with an exception filter
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
import { Controller, Get, UseFilter, Catch, Service } from 'nextrush/class';
|
|
245
|
+
import type { ExceptionFilter } from 'nextrush/class';
|
|
246
|
+
import type { Context } from '@nextrush/types';
|
|
247
|
+
|
|
248
|
+
class EntityNotFoundError extends Error {}
|
|
249
|
+
|
|
250
|
+
@Service()
|
|
251
|
+
@Catch(EntityNotFoundError) // no argument = catch-all
|
|
252
|
+
class NotFoundFilter implements ExceptionFilter {
|
|
253
|
+
catch(_error: unknown, ctx: Context): void {
|
|
254
|
+
ctx.status = 404;
|
|
255
|
+
ctx.json({ error: 'Resource not found' });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
@Controller('/items')
|
|
260
|
+
@UseFilter(NotFoundFilter)
|
|
261
|
+
class ItemController {
|
|
262
|
+
@Get('/:id')
|
|
263
|
+
read() {
|
|
264
|
+
throw new EntityNotFoundError(); // handled by NotFoundFilter -> 404
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Group features with a module
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
import { createApp, listen } from 'nextrush';
|
|
273
|
+
import { Module, registerModule } from 'nextrush/class';
|
|
274
|
+
|
|
275
|
+
@Module({ controllers: [UserController], providers: [UserService] })
|
|
276
|
+
class UserModule {}
|
|
277
|
+
|
|
278
|
+
@Module({ imports: [UserModule] }) // compose feature modules through `imports`
|
|
279
|
+
class AppModule {}
|
|
280
|
+
|
|
281
|
+
const app = createApp();
|
|
282
|
+
await registerModule(app, AppModule, { prefix: '/api' });
|
|
283
|
+
listen(app, 8080);
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Run setup/teardown with lifecycle hooks
|
|
287
|
+
|
|
288
|
+
```ts
|
|
289
|
+
import { Service } from 'nextrush/class';
|
|
290
|
+
import type { OnInit, OnShutdown } from 'nextrush/class';
|
|
291
|
+
|
|
292
|
+
@Service()
|
|
293
|
+
class Database implements OnInit, OnShutdown {
|
|
294
|
+
async onInit() { /* runs at app.ready(), dependencies first */ }
|
|
295
|
+
async onShutdown() { /* runs at app.close(), reverse order */ }
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## API overview
|
|
300
|
+
|
|
301
|
+
The sealed public surface (ADR-0005), grouped by role. DI exports (`Service`, `Repository`, `container`, `createContainer`, `inject`, `Container`) are re-exported from [`@nextrush/di`](../di) — see its docs for the container contract.
|
|
302
|
+
|
|
303
|
+
| Export | Signature | Since | Stability | Description |
|
|
304
|
+
| ------ | --------- | ----- | --------- | ----------- |
|
|
305
|
+
| `Controller` | `(pathOrOptions?: string \| ControllerOptions) => ClassDecorator` | `3.0.0` | Stable ✅ | Mark a class an HTTP controller; makes it DI-resolvable. |
|
|
306
|
+
| `Get` `Post` `Put` `Patch` `Delete` `Head` `Options` `All` | `(pathOrOptions?, options?) => MethodDecorator` | `3.0.0` | Stable ✅ | Bind a method to an HTTP method + path. |
|
|
307
|
+
| `Body` `Param` `Query` `Header` | `(name?/options?, options?) => ParameterDecorator` | `3.0.0` | Stable ✅ | Extract body / route param / query / header (`transform` on `Body`/`Param`/`Query`; `defaultValue` on `Param`/`Query`/`Header`). |
|
|
308
|
+
| `Ctx` `Req` `Res` | `() => ParameterDecorator` | `3.0.0` | Stable ✅ | Inject the `Context`, or the raw request/response (adapter-specific). |
|
|
309
|
+
| `createCustomParamDecorator` | `(extractor, options?) => ParameterDecorator` | `3.0.0` | Stable ✅ | Build a custom parameter extractor. |
|
|
310
|
+
| `HttpCode` | `(statusCode: number) => MethodDecorator` | `3.0.0` | Stable ✅ | Set the success status code (overrides the route `statusCode`). |
|
|
311
|
+
| `Redirect` | `(url: string, statusCode?: number) => MethodDecorator` | `3.0.0` | Stable ✅ | Redirect the response (default `302`). |
|
|
312
|
+
| `SetHeader` | `(name: string, value: string) => MethodDecorator` | `3.0.0` | Stable ✅ | Attach a response header (stackable). |
|
|
313
|
+
| `UseGuard` | `(...guards: Guard[]) => ClassDecorator & MethodDecorator` | `3.0.0` | Stable ✅ | Attach guards (`GuardFn` or `CanActivate` class). |
|
|
314
|
+
| `UseInterceptor` | `(...interceptors: InterceptorClass[]) => ClassDecorator & MethodDecorator` | `3.0.0` | Stable ✅ | Wrap the handler with interceptors. |
|
|
315
|
+
| `UseFilter` · `Catch` | `(...filters/errorTypes) => ClassDecorator & MethodDecorator` | `3.0.0` | Stable ✅ | Attach exception filters and declare which errors they catch. |
|
|
316
|
+
| `Module` | `(options?: ModuleOptions) => ClassDecorator` | `3.1.0` | Stable ✅ | Declare a feature module (`imports`/`controllers`/`providers`/`exports`). |
|
|
317
|
+
| `registerControllers` | `(app, options?: ControllersOptions) => Promise<void>` | `3.0.0` | Stable ✅ | Discover/register controllers and build routes. |
|
|
318
|
+
| `registerModule` | `(app, rootModule, options?: ModuleRegistrationOptions) => Promise<void>` | `3.1.0` | Stable ✅ | Register a whole `@Module` graph in one call. |
|
|
319
|
+
| `FilesystemSource` · `MemorySource` | `class implements DiscoverySource` | `3.1.0` | Stable ✅ | Filesystem-scan / in-memory controller discovery. |
|
|
320
|
+
| `discoverControllers` · `getControllersFromResults` · `getErrorsFromResults` | `(…) => …` | `3.0.0` | Stable ✅ | Lower-level filesystem discovery helpers. |
|
|
321
|
+
| `getClassDiagnostics` | `(app) => DiagnosticsReport \| undefined` | `3.1.0` | Stable ✅ | Read the opt-in diagnostics report (`diagnostics: true`). |
|
|
322
|
+
| `getControllerDefinition` · `getRouteMetadata` · `getParamMetadata` · `isController` · … | `(target[, method]) => …` | `3.0.0` | Stable ✅ | Metadata readers for introspection / renderers (e.g. OpenAPI). |
|
|
323
|
+
| `ControllerRegistry` · `buildRoutes` | `class` / `(…) => BuiltRoute[]` | `3.0.0` | Stable ✅ | Lower-level registry + route builder. |
|
|
324
|
+
| `isOnInit` · `isOnShutdown` · `isGuardClass` · `isModule` · `getModuleMetadata` | `(value) => boolean/meta` | `3.x` | Stable ✅ | Duck-typed lifecycle/guard/module detection. |
|
|
325
|
+
| `ControllerError` · `ControllerResolutionError` · `DiscoveryError` · `GuardRejectionError` · `MissingParameterError` · `ParameterInjectionError` · `NoRoutesError` · `NotAControllerError` · `NotAModuleError` · `RouteRegistrationError` · `HttpError` | `class` | `3.0.0` | Stable ✅ | The class-runtime error hierarchy (4xx extend `@nextrush/errors`, 5xx are config errors). |
|
|
326
|
+
| `Service` · `Repository` · `container` · `createContainer` · `inject` · `type Container` | — | `3.0.0` | Stable ✅ | Re-exported from [`@nextrush/di`](../di) (its docs are the DI reference). |
|
|
327
|
+
| `type OnInit` · `OnShutdown` · `Guard` · `GuardFn` · `CanActivate` · `Interceptor` · `ExceptionFilter` · `ModuleOptions` · `ControllersOptions` · `BuiltRoute` · `DiagnosticsReport` · … | — | `3.x` | Stable ✅ | Public contracts for the surface above. |
|
|
328
|
+
|
|
329
|
+
## Options
|
|
330
|
+
|
|
331
|
+
`registerControllers(app, options)` accepts the following (all optional). `registerModule` accepts the subset `prefix` · `middleware` · `container` · `isolate` · `validate` · `debug`.
|
|
332
|
+
|
|
333
|
+
| Option | Type | Required | Default | Security-sensitive | Description |
|
|
334
|
+
| ------ | ---- | -------- | ------- | ------------------ | ----------- |
|
|
335
|
+
| `controllers` | `Function[]` | No | `[]` | — | Explicit controller classes to register (merged with `root` discovery). |
|
|
336
|
+
| `root` | `string` | No | `undefined` | ⚠️ | Directory to auto-discover; each matched file is dynamically `import()`ed (runs its top-level code). |
|
|
337
|
+
| `include` | `string[]` | No | `['**/*.controller.ts', '**/*.controller.js']` | — | Glob patterns for auto-discovery. |
|
|
338
|
+
| `exclude` | `string[]` | No | test / `node_modules` / `dist` globs | — | Glob patterns excluded from discovery. |
|
|
339
|
+
| `source` | `DiscoverySource` | No | `undefined` | — | Custom discovery source (precedes `root`; incompatible with `controllers`). |
|
|
340
|
+
| `container` | `Container` | No | `app.container` then the global container | — | DI container to resolve from; an explicit one always wins. |
|
|
341
|
+
| `isolate` | `boolean` | No | `false` | — | Give this registration its own fresh container so apps in one process do not share service singletons. |
|
|
342
|
+
| `prefix` | `string` | No | `''` | — | Path prefix prepended to every route (e.g. `/api`). |
|
|
343
|
+
| `middleware` | `Middleware[]` | No | `[]` | — | Global middleware applied to all controllers. |
|
|
344
|
+
| `validate` | `boolean` | No | `true` | — | Eagerly resolve every controller (and class guard) at boot so broken dependencies fail there, not as a first-request 500. |
|
|
345
|
+
| `strict` | `boolean` | No | `false` | — | Throw on discovery errors instead of logging warnings. |
|
|
346
|
+
| `diagnostics` | `boolean` | No | `false` | — | Collect the route/provider/duplicate/cycle/timing report (`getClassDiagnostics`); zero-cost when off. |
|
|
347
|
+
| `debug` | `boolean` | No | `false` | — | Write discovery/registration logs to `stderr`. |
|
|
348
|
+
|
|
349
|
+
## Compatibility
|
|
350
|
+
|
|
351
|
+
**Requirements**
|
|
352
|
+
|
|
353
|
+
| Requirement | Version |
|
|
354
|
+
| ----------- | ------- |
|
|
355
|
+
| NextRush | `3.x` |
|
|
356
|
+
| Node.js | `>=22` |
|
|
357
|
+
| TypeScript | `>=5.x` (with `experimentalDecorators` + `emitDecoratorMetadata`) |
|
|
358
|
+
|
|
359
|
+
**Runtimes**
|
|
360
|
+
|
|
361
|
+
| Runtime | Supported | Notes |
|
|
362
|
+
| ------- | --------- | ----- |
|
|
363
|
+
| Node.js `>=22` | ✅ | ESM-only; the only runtime where filesystem auto-discovery (`root` / `FilesystemSource`) works |
|
|
364
|
+
| Bun / Deno / Edge | ✅ / ✅ / ✅ | The decorator, DI, and request pipeline are runtime-agnostic — register with an explicit `controllers` list or `MemorySource` (no filesystem scan) |
|
|
365
|
+
|
|
366
|
+
**Integration**
|
|
367
|
+
- **Peer dependencies:** [`@nextrush/core`](../core) and [`@nextrush/router`](../router) (the app + router it builds onto); also depends on [`@nextrush/di`](../di), [`@nextrush/errors`](../errors), [`@nextrush/types`](../types), and `reflect-metadata`.
|
|
368
|
+
- **Works with:** [`@nextrush/openapi`](../middleware/openapi) (reads the route metadata it contributes), any `@nextrush/*` middleware via the `middleware` option.
|
|
369
|
+
- **Incompatible with:** the deprecated `@nextrush/decorators` and `@nextrush/controllers` shims — this package supersedes both; do not mix.
|
|
370
|
+
|
|
371
|
+
> [!IMPORTANT]
|
|
372
|
+
> NextRush is **ESM-only, permanently** — no CommonJS build. On Node `>=22`, CommonJS consumers can
|
|
373
|
+
> `require()` this ESM package natively. See the
|
|
374
|
+
> [Module Format Policy](https://github.com/0xTanzim/nextRush#module-format-policy).
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
## Troubleshooting
|
|
379
|
+
|
|
380
|
+
<details>
|
|
381
|
+
<summary><strong><code>ControllerResolutionError</code> / a DI error at startup</strong></summary>
|
|
382
|
+
|
|
383
|
+
**Cause:** a controller (or something in its constructor graph) can't be resolved — a missing `@Service()`, an interface injected without `@inject('TOKEN')`, or a circular dependency. With `validate: true` (the default) this surfaces at boot instead of as a first-request 500. **Fix:** decorate the service, inject tokens explicitly, or break the cycle (see [`@nextrush/di`](../di) troubleshooting). A `@nextrush/di` error is rethrown as-is so its actionable message shows.
|
|
384
|
+
|
|
385
|
+
</details>
|
|
386
|
+
|
|
387
|
+
<details>
|
|
388
|
+
<summary><strong>"No controllers found" warning and no routes registered</strong></summary>
|
|
389
|
+
|
|
390
|
+
**Cause:** auto-discovery scanned `root` but matched nothing — the default `include` only matches `*.controller.ts` / `*.controller.js`. **Fix:** name files with the `.controller.` convention, pass a custom `include`, or register with an explicit `controllers: [...]` list.
|
|
391
|
+
|
|
392
|
+
</details>
|
|
393
|
+
|
|
394
|
+
<details>
|
|
395
|
+
<summary><strong>A <code>request</code>-scoped service behaves like a singleton</strong></summary>
|
|
396
|
+
|
|
397
|
+
**Cause:** `request` scope only takes effect per-request child container. The class runtime creates one automatically **only** when a controller (or its graph) is effectively request-scoped. **Fix:** ensure the request-scoped `@Service({ scope: 'request' })` is actually in the controller's dependency graph — scope bubbles up from the dependency, so the controller resolves fresh per request. DI-scope details live in [`@nextrush/di`](../di).
|
|
398
|
+
|
|
399
|
+
</details>
|
|
400
|
+
|
|
401
|
+
<details>
|
|
402
|
+
<summary><strong>Lifecycle hooks (<code>onInit</code>/<code>onShutdown</code>) never run</strong></summary>
|
|
403
|
+
|
|
404
|
+
**Cause:** `registerControllers` bridges hooks into the app's lifecycle at registration time — if it runs *after* `serve()`/`listen()`/`ready()`, the config is frozen and it throws. Controllers' own hooks also require `validate: true`. **Fix:** call `registerControllers` / `registerModule` **before** `listen()`, and keep `validate` on (the default).
|
|
405
|
+
|
|
406
|
+
</details>
|
|
407
|
+
|
|
408
|
+
## FAQ
|
|
409
|
+
|
|
410
|
+
**Do I need `@Service()` on a `@Controller`?**
|
|
411
|
+
No. `@Controller` already makes the class DI-resolvable. Add `@Service({ scope })` to a controller only to change its scope; add it to the *services* it depends on.
|
|
412
|
+
|
|
413
|
+
**Why ESM-only?**
|
|
414
|
+
See the [Module Format Policy](https://github.com/0xTanzim/nextRush#module-format-policy).
|
|
415
|
+
|
|
416
|
+
**Does it work on Bun, Deno, and Edge?**
|
|
417
|
+
Yes for the decorator, DI, and request pipeline. Filesystem auto-discovery (`root`) is Node-only; on other runtimes register with an explicit `controllers` list or a `MemorySource`.
|
|
418
|
+
|
|
419
|
+
**How is `@nextrush/class` different from `@nextrush/decorators` / `@nextrush/controllers`?**
|
|
420
|
+
It consolidates both (plus modules, request scope, and diagnostics) into one package and re-exports `@nextrush/di`. The two older packages are deprecated shims — migrate to `nextrush/class`.
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
## Package relationships
|
|
425
|
+
|
|
426
|
+
```text
|
|
427
|
+
depends on @nextrush/core · router · di · errors · types (+ reflect-metadata)
|
|
428
|
+
@nextrush/class ----------------->
|
|
429
|
+
re-exports @nextrush/di (Service, Repository, container, createContainer, inject)
|
|
430
|
+
exposed via nextrush (the `nextrush/class` subpath)
|
|
431
|
+
supersedes @nextrush/decorators · @nextrush/controllers (deprecated)
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
- **Depends on:** [`@nextrush/core`](../core) + [`@nextrush/router`](../router) (peer — the app/router it builds on), [`@nextrush/di`](../di) (DI, re-exported), [`@nextrush/errors`](../errors) (error base classes), [`@nextrush/types`](../types) (shared contracts).
|
|
435
|
+
- **Re-exports:** [`@nextrush/di`](../di) — the DI surface reaches users through this package; its docs are the canonical DI reference.
|
|
436
|
+
- **Exposed via:** [`nextrush`](../nextrush) — the meta package's `nextrush/class` subpath is the recommended import.
|
|
437
|
+
- **Alternative:** the functional API in [`nextrush`](../nextrush) for apps that prefer no decorators.
|
|
438
|
+
|
|
439
|
+
## Architecture
|
|
440
|
+
|
|
441
|
+
Maintaining or contributing to this package? The internal design — the bootstrap pipeline, the
|
|
442
|
+
per-request handler pipeline (guards -> resolve -> params -> interceptors -> handler -> filters),
|
|
443
|
+
request-scope bubbling, the immutable route graph, the architectural invariants, and the decisions
|
|
444
|
+
and trade-offs behind them (with diagrams) — is in **[`ARCHITECTURE.md`](./ARCHITECTURE.md)**.
|
|
445
|
+
Design history: [RFC-NEXTRUSH-CLASS-CONSOLIDATION, RFC-NEXTRUSH-MODULES, RFC-NEXTRUSH-REQUEST-SCOPE](https://github.com/0xTanzim/nextRush/tree/main/docs/RFC) · [ADR-0005](https://github.com/0xTanzim/nextRush/blob/main/docs/adr/ADR-0005-package-tiers-sealed-surface-deprecation.md).
|
|
446
|
+
|
|
447
|
+
## Resources
|
|
448
|
+
|
|
449
|
+
- 📖 **Learn** — [Documentation](https://0xtanzim.github.io/nextRush/docs) · [Architecture](./ARCHITECTURE.md) · [RFCs](https://github.com/0xTanzim/nextRush/tree/main/docs/RFC)
|
|
450
|
+
- 📝 **Changelog** — [CHANGELOG.md](./CHANGELOG.md)
|
|
451
|
+
- 🐛 **Report an issue** — [GitHub Issues](https://github.com/0xTanzim/nextRush/issues)
|
|
452
|
+
- 🤝 **Contribute** — [CONTRIBUTING.md](https://github.com/0xTanzim/nextRush/blob/main/CONTRIBUTING.md)
|
|
453
|
+
|
|
454
|
+
---
|
|
455
|
+
|
|
456
|
+
MIT © [Tanzim Hossain](https://github.com/0xTanzim)
|