@nextrush/core 3.0.7 → 4.0.0-beta.1

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,626 +1,455 @@
1
1
  # @nextrush/core
2
2
 
3
- > The minimal core of NextRush: Application, middleware composition, and plugin system.
4
-
5
- ## The Problem
6
-
7
- Backend frameworks often bundle everything together. You pay for features you don't use:
8
-
9
- - Routing logic when you only need middleware composition
10
- - Body parsing when you're building a proxy
11
- - Complex plugin systems when you need simple extensibility
12
-
13
- This creates bloat. Cold starts suffer. Memory usage grows. Debugging becomes harder.
14
-
15
- ## How NextRush Approaches This
16
-
17
- `@nextrush/core` provides **only the essentials**:
3
+ > The engine behind every NextRush app — the `Application` object, the Koa-style middleware pipeline (`compose` + `ctx.next()`), router mounting, the extension lifecycle, and the default error response — for framework and adapter authors.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@nextrush/core.svg)](https://www.npmjs.com/package/@nextrush/core)
6
+ [![downloads](https://img.shields.io/npm/dm/@nextrush/core.svg)](https://www.npmjs.com/package/@nextrush/core)
7
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/@nextrush/core.svg)](https://bundlephobia.com/package/@nextrush/core)
8
+ [![types](https://img.shields.io/npm/types/@nextrush/core.svg)](https://www.npmjs.com/package/@nextrush/core)
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/core.svg)](https://github.com/0xTanzim/nextRush/blob/main/LICENSE)
11
+
12
+ | | |
13
+ | --- | --- |
14
+ | **Purpose** | The `Application` object, middleware composition, router mounting, extension lifecycle, and default error handling for NextRush |
15
+ | **Package type** | Core |
16
+ | **Status** | Stable ✅ |
17
+ | **Included in `nextrush`?** | ✅ Yes — `createApp`, `compose`, and the common error classes are re-exported. The `nextrush` meta-package's `createApp` wraps this one and auto-injects a router. |
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
+ - ✅ **No third-party dependencies** — depends only on `@nextrush/errors` and `@nextrush/types`
27
+ - ✅ **ESM-only**, tree-shakable, side-effect-free (`"sideEffects": false`)
28
+ - ✅ **Fully typed** — strict TypeScript, zero `any`; extension shapes inferred through `extend()`
29
+ - ✅ **Runtime-independent** — no `node:*` / `process` / runtime globals; the same code runs on every adapter
30
+
31
+ <details>
32
+ <summary><strong>Table of contents</strong></summary>
33
+
34
+ [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) · [Performance](#performance) · [Compatibility](#compatibility) · [Troubleshooting](#troubleshooting) · [FAQ](#faq) · [Package relationships](#package-relationships) · [Architecture](#architecture) · [Resources](#resources)
35
+
36
+ </details>
37
+
38
+ ---
39
+
40
+ ## The problem
41
+
42
+ Every HTTP framework needs the same plumbing before it can serve a single request: a place to register middleware, a way to run that middleware in order and let each step wrap the next, somewhere to mount routers, a boot step for long-lived services, and a single error path so a thrown error never crashes the process or leaks a stack trace. Hand-rolled, that plumbing is where subtle bugs live.
43
+
44
+ ```ts
45
+ // TODAY, without a composition engine — chaining middleware by hand:
46
+ async function handle(ctx) {
47
+ try {
48
+ await mwA(ctx, () => // each layer must remember to await the next…
49
+ mwB(ctx, () => // …and pass a `next` that's only safe to call once…
50
+ handler(ctx))); // …and one forgotten try/catch leaks the raw error.
51
+ } catch (err) {
52
+ ctx.status = 500;
53
+ ctx.json({ error: String(err) }); // shape drifts; internals leak
54
+ }
55
+ }
56
+ ```
18
57
 
19
- - **Application**: Middleware registration and plugin management
20
- - **Middleware Composition**: Koa-style `compose()` for async middleware chains
21
- - **Plugin System**: Simple, typed plugin interface
22
- - **Error Handling**: Configurable error handlers with production/development modes
58
+ `@nextrush/core` owns exactly this plumbing so applications never write it. `compose()` runs the chain with correct ordering and single-call `next()` semantics, `Application` manages registration and a frozen-after-boot lifecycle, and one default error path serializes any thrown error through the same contract as `@nextrush/errors`.
23
59
 
24
- Everything else (routing, body parsing, authentication) lives in separate packages. You install what you use.
60
+ ## When to use
25
61
 
26
- ## Mental Model
62
+ `@nextrush/core` is the engine the `nextrush` meta-package is built on. Most applications import `createApp` and `listen` from [`nextrush`](../nextrush) and never depend on `@nextrush/core` directly.
27
63
 
28
- Think of the core as a **middleware pipeline manager**:
64
+ **Use `@nextrush/core` if:**
29
65
 
30
- ```
31
- Request [Middleware 1] [Middleware 2] [Handler] [Middleware 2] [Middleware 1] → Response
32
- ↓ ↓ ↓ ↑ ↑
33
- Before Before Execute After After
34
- ```
66
+ - ✓ You're building your own meta-package, runtime, or adapter on top of NextRush and need the raw `Application`
67
+ - You want `compose()` the Koa-style middleware composer as a standalone building block
68
+ - ✓ You're writing an extension or registrar and need the `Extension` / `ExtensionContext` contracts and the boot lifecycle
35
69
 
36
- Each middleware can:
70
+ **Reach for something else if:**
37
71
 
38
- 1. Do something before calling `ctx.next()` or `next()`
39
- 2. Call `await ctx.next()` to pass control downstream
40
- 3. Do something after `ctx.next()` returns
72
+ - You're building an application — import `createApp`, `createRouter`, and `listen` from [`nextrush`](../nextrush); its `createApp` wires a router for you so `app.get(...)` works out of the box
73
+ - You need route matching — that's [`@nextrush/router`](../router); core only *mounts* a router, it doesn't match paths
74
+ - You need the concrete request/response object — the `Context` implementation is built by [`@nextrush/adapter-*`](../adapters); core defines the pipeline that operates on it
41
75
 
42
- This is the "onion model" - requests flow inward, responses flow outward.
76
+ ---
43
77
 
44
78
  ## Installation
45
79
 
46
80
  ```bash
47
81
  pnpm add @nextrush/core
82
+ # npm i @nextrush/core · yarn add @nextrush/core · bun add @nextrush/core
48
83
  ```
49
84
 
50
- ## Quick Start
85
+ > [!NOTE]
86
+ > Already using `nextrush`? `createApp`, `compose`, `isMiddleware`, and the common error classes are
87
+ > re-exported from the meta package — `import { createApp } from 'nextrush'` works without installing
88
+ > this directly, and that `createApp` auto-injects a router. Install `@nextrush/core` only to build
89
+ > your own meta-package/adapter or to use `compose()` on its own.
51
90
 
52
- ```typescript
53
- import { createApp } from '@nextrush/core';
54
- import { listen } from '@nextrush/adapter-node';
91
+ ## Quick start
55
92
 
56
- const app = createApp();
93
+ ```ts
94
+ import { createApp, createRouter, listen } from 'nextrush';
57
95
 
58
- // Logging middleware
59
- app.use(async (ctx, next) => {
60
- console.log(`→ ${ctx.method} ${ctx.path}`);
61
- await next();
62
- console.log(`← ${ctx.status}`);
63
- });
96
+ const app = createApp();
64
97
 
65
- // Handler
98
+ // Middleware runs in registration order; each may await ctx.next().
66
99
  app.use(async (ctx) => {
67
- ctx.json({ message: 'Hello World' });
100
+ const start = Date.now();
101
+ await ctx.next();
102
+ ctx.set('x-response-time', `${Date.now() - start}ms`);
68
103
  });
69
104
 
70
- listen(app, { port: 3000 });
71
- ```
72
-
73
- ## Application
74
-
75
- ### Creating an Application
76
-
77
- ```typescript
78
- import { createApp, Application } from '@nextrush/core';
79
-
80
- // Factory function (recommended)
81
- const app = createApp();
105
+ // Feature router, mounted under a prefix (Hono-style composition).
106
+ const users = createRouter();
107
+ users.get('/:id', (ctx) => ctx.json({ id: ctx.params.id }));
108
+ app.route('/users', users);
82
109
 
83
- // With options
84
- const app = createApp({
85
- env: 'production', // 'development' | 'production' | 'test'
86
- proxy: true, // Trust proxy headers (X-Forwarded-*)
87
- });
110
+ listen(app, 8080);
88
111
  ```
89
112
 
90
- ### Application Options
113
+ `createApp()` (from `nextrush`) gives you an `Application` with a router already wired in. You register middleware and routers against it; `listen()` calls `app.ready()` to boot, then starts the adapter. That boot step is why extensions and mounted routers are all in place before the first request.
91
114
 
92
- | Option | Type | Default | Description |
93
- | -------- | ----------------------------------------- | --------------- | ------------------------------------------------------- |
94
- | `env` | `'development' \| 'production' \| 'test'` | `'development'` | Environment mode |
95
- | `proxy` | `boolean` | `false` | Trust proxy headers |
96
- | `logger` | `Logger` | No-op (silent) | Pluggable logger. Pass `console` for quick dev logging. |
115
+ ## Capabilities
97
116
 
98
- ### Application Properties
99
-
100
- ```typescript
101
- app.isProduction; // boolean - true if env === 'production'
102
- app.isRunning; // boolean - true after app.start() called
103
- app.middlewareCount; // number - count of registered middleware
104
- app.options; // ApplicationOptions - readonly config
105
- app.logger; // Logger - readonly configured logger instance
106
- ```
117
+ **Application**
118
+ - **Middleware registration** — `app.use(...mw)`, chainable, validated (a non-function throws)
119
+ - **Router mounting** — `app.route(prefix, router)` mounts a feature router, rewriting `ctx.path` under the prefix
120
+ - **Route shortcuts** `app.get/post/put/patch/delete/head/all(...)` delegate to the app-owned router
121
+ - **Frozen-after-boot lifecycle** `ready()` -> `start()` -> `close()`, with config frozen once booted
107
122
 
108
- ## Middleware
123
+ **Middleware pipeline**
124
+ - **`compose()`** — Koa-style composition; each middleware may `await ctx.next()` to wrap downstream
125
+ - **Single-call `next()`** — a second `next()` rejects with `next() called multiple times`
126
+ - **Double-response warning** — opt-in dev warning when a middleware responds *and* calls `next()`
109
127
 
110
- ### Registration
128
+ **Extensions**
129
+ - **`app.extend(ext)`** — register a long-lived service; `setup()` runs at `ready()`, `destroy()` at `close()`
130
+ - **Typed decorations** — `extend()` returns `this & TDecorated`, so `app.events` is statically inferred
131
+ - **Dependency assertion** — an extension's `needs` are checked at boot (registration order, no auto-sort)
111
132
 
112
- ```typescript
113
- // Single middleware
114
- app.use(async (ctx, next) => {
115
- await next();
116
- });
133
+ **Error handling**
134
+ - **One default error path** — any thrown error is serialized through the `@nextrush/errors` contract
135
+ - **Custom handler** — `app.setErrorHandler((err, ctx) => ...)` overrides the default
117
136
 
118
- // Multiple middleware
119
- app.use(middleware1, middleware2, middleware3);
137
+ **Performance**
138
+ - **Compose fast paths** — dedicated zero- and single-middleware paths avoid the recursive dispatch closure
139
+ - **Snapshot at compose** — the middleware array is snapshotted once, not walked per request
120
140
 
121
- // Method chaining
122
- app.use(cors()).use(helmet()).use(json());
123
- ```
141
+ **Developer experience**
142
+ - **Fully typed, zero `any`** — contracts shared via `@nextrush/types`
143
+ - **Actionable errors** — misconfiguration (no router, config frozen, duplicate extension) throws with a fix
124
144
 
125
- ### Two Syntax Styles
145
+ ## Mental model
126
146
 
127
- NextRush supports both modern and traditional Koa-style middleware:
147
+ An `Application` is a **list of middleware plus a lifecycle**. `compose()` turns that list into one function where each middleware can wrap the next by `await`-ing `ctx.next()`; the app-owned router is mounted **last**, so it runs after all middleware.
128
148
 
129
- ```typescript
130
- // Modern syntax (ctx.next)
131
- app.use(async (ctx) => {
132
- console.log('Before');
133
- await ctx.next();
134
- console.log('After');
135
- });
136
-
137
- // Traditional Koa syntax (next parameter)
138
- app.use(async (ctx, next) => {
139
- console.log('Before');
140
- await next();
141
- console.log('After');
142
- });
149
+ ```text
150
+ createApp() --> app.use(mw) --> app.route(prefix, router) --> await app.ready()
151
+ |
152
+ compose([...mw, router.routes()])
153
+ |
154
+ request --> ctx --> mw1 -> mw2 -> ... -> router --> handler --> ctx.json(...)
155
+ |______ await next() wraps downstream, unwinds back up ______|
143
156
  ```
144
157
 
145
- Both styles work identically. Use whichever you prefer.
158
+ **Rule:** `use`, `route`, and `extend` configure the app; `ready()` freezes that config and boots extensions. Configure before `ready()`, serve after — calling a configuration method post-boot throws.
146
159
 
147
- ### Middleware Order
160
+ > [!TIP]
161
+ > The full request lifecycle, the app state machine, and the `Application`/`Context` structure (with
162
+ > Mermaid diagrams) are in [`ARCHITECTURE.md`](./ARCHITECTURE.md).
148
163
 
149
- Middleware executes in registration order (onion model):
164
+ ---
150
165
 
151
- ```typescript
152
- app.use(async (ctx, next) => {
153
- console.log('1: Start');
154
- await next();
155
- console.log('1: End');
156
- });
166
+ ## Common tasks
157
167
 
158
- app.use(async (ctx, next) => {
159
- console.log('2: Start');
160
- await next();
161
- console.log('2: End');
162
- });
168
+ ### Register middleware
163
169
 
170
+ ```ts
171
+ // Modern form — ctx.next()
164
172
  app.use(async (ctx) => {
165
- console.log('3: Handler');
166
- ctx.json({ ok: true });
167
- });
168
-
169
- // Output:
170
- // 1: Start
171
- // 2: Start
172
- // 3: Handler
173
- // 2: End
174
- // 1: End
175
- ```
176
-
177
- ### Conditional Middleware
178
-
179
- ```typescript
180
- app.use(async (ctx, next) => {
181
- // Skip middleware for health checks
182
- if (ctx.path === '/health') {
183
- return next();
184
- }
185
-
186
- // Apply logic to other routes
187
- const start = Date.now();
188
- await next();
189
- console.log(`${ctx.path} took ${Date.now() - start}ms`);
173
+ await ctx.next();
174
+ ctx.set('x-powered-by', 'nextrush');
190
175
  });
191
- ```
192
176
 
193
- ### Early Termination
194
-
195
- Skip remaining middleware by not calling `next()`:
196
-
197
- ```typescript
177
+ // Traditional form — (ctx, next)
198
178
  app.use(async (ctx, next) => {
199
- if (!ctx.get('Authorization')) {
200
- ctx.status = 401;
201
- ctx.json({ error: 'Unauthorized' });
202
- return; // Don't call next()
203
- }
204
179
  await next();
205
180
  });
206
- ```
207
-
208
- ## Context
209
-
210
- The Context (`ctx`) object provides access to request data and response methods.
211
-
212
- ### Request Properties (Read-only)
213
-
214
- | Property | Type | Description |
215
- | ---------------- | ----------------- | ----------------------------- |
216
- | `ctx.method` | `HttpMethod` | HTTP method (GET, POST, etc.) |
217
- | `ctx.url` | `string` | Full URL with query string |
218
- | `ctx.path` | `string` | Path without query string |
219
- | `ctx.query` | `QueryParams` | Parsed query parameters |
220
- | `ctx.headers` | `IncomingHeaders` | Request headers |
221
- | `ctx.ip` | `string` | Client IP address |
222
- | `ctx.runtime` | `Runtime` | Current JS runtime |
223
- | `ctx.raw` | `RawHttp` | Raw platform objects |
224
- | `ctx.bodySource` | `BodySource` | Body stream for parsers |
225
-
226
- ### Request Body
227
-
228
- ```typescript
229
- // ctx.body is set by body parser middleware
230
- import { json } from '@nextrush/body-parser';
231
-
232
- app.use(json());
233
-
234
- app.post('/users', async (ctx) => {
235
- const { name, email } = ctx.body as CreateUserDto;
236
- ctx.json({ name, email });
237
- });
238
- ```
239
-
240
- ### Route Parameters
241
181
 
242
- ```typescript
243
- // Set by router when route matches
244
- app.get('/users/:id', (ctx) => {
245
- const { id } = ctx.params;
246
- ctx.json({ id });
247
- });
182
+ app.use(mwA, mwB, mwC); // register several at once; runs in this order
248
183
  ```
249
184
 
250
- ### Response
185
+ Both signatures are supported. Middleware runs in registration order, and each layer wraps the ones after it.
251
186
 
252
- | Property/Method | Description |
253
- | ---------------------------- | ----------------------------------- |
254
- | `ctx.status` | Set HTTP status code (default: 200) |
255
- | `ctx.json(data)` | Send JSON response |
256
- | `ctx.send(data)` | Send text, buffer, or stream |
257
- | `ctx.html(content)` | Send HTML response |
258
- | `ctx.redirect(url, status?)` | Redirect to URL |
259
- | `ctx.set(field, value)` | Set response header |
260
- | `ctx.get(field)` | Get request header |
187
+ ### Mount feature routers
261
188
 
262
- ```typescript
263
- app.use(async (ctx) => {
264
- // Set status
265
- ctx.status = 201;
189
+ ```ts
190
+ import { createApp, createRouter } from 'nextrush';
266
191
 
267
- // Set headers
268
- ctx.set('X-Request-Id', '12345');
269
- ctx.set('Cache-Control', 'no-cache');
192
+ const app = createApp();
193
+ const api = createRouter();
194
+ api.get('/health', (ctx) => ctx.json({ ok: true }));
270
195
 
271
- // Send JSON
272
- ctx.json({ created: true });
273
- });
196
+ app.route('/api', api); // mounted under /api; ctx.path is rewritten for the sub-router
197
+ app.route('/', api); // root mount — no prefix processing
274
198
  ```
275
199
 
276
- ### Error Helpers
200
+ `app.route()` strips the prefix from `ctx.path` while the sub-router runs and restores it afterwards, so middleware registered after the mount still sees the original path.
277
201
 
278
- ```typescript
279
- app.use(async (ctx) => {
280
- // Throw HTTP error
281
- ctx.throw(404, 'User not found');
282
- ctx.throw(401); // Uses default message
202
+ ### Set a custom error handler
283
203
 
284
- // Assert condition
285
- ctx.assert(user, 404, 'User not found');
286
- ctx.assert(user.isAdmin, 403, 'Admin required');
204
+ ```ts
205
+ app.setErrorHandler((err, ctx) => {
206
+ ctx.status = 500;
207
+ ctx.json({ error: 'Something went wrong' });
287
208
  });
288
209
  ```
289
210
 
290
- ### State
211
+ If you set no handler, core writes the same JSON shape as `@nextrush/errors`' `errorHandler()` — a `NextRushError` serializes via its own `toJSON()`; any other error becomes a safe, coded `500` (its internal message is never sent to the client).
291
212
 
292
- Share data between middleware:
213
+ ### Register an extension (long-lived service)
293
214
 
294
- ```typescript
295
- // Auth middleware
296
- app.use(async (ctx, next) => {
297
- ctx.state.user = await validateToken(ctx.get('Authorization'));
298
- await next();
299
- });
215
+ ```ts
216
+ import { createApp } from 'nextrush';
217
+ import { events } from '@nextrush/events';
300
218
 
301
- // Handler
302
- app.get('/profile', (ctx) => {
303
- const user = ctx.state.user;
304
- ctx.json({ user });
305
- });
219
+ const app = createApp().extend(events()); // chain in one expression
220
+ await app.ready(); // adapters call this automatically
221
+ app.events.emit('user:created', { id: '1' }); // available only AFTER ready()
306
222
  ```
307
223
 
308
- ### Raw Access
224
+ `extend()` queues the extension and returns `this & TDecorated`; `setup()` runs once at `ready()` in registration order, and `destroy()` runs at `close()` in reverse. Anything an extension decorates (like `app.events`) exists only after `ready()`.
309
225
 
310
- Access platform-specific objects:
226
+ ### Compose middleware standalone
311
227
 
312
- ```typescript
313
- // Node.js adapter
314
- ctx.raw.req; // IncomingMessage
315
- ctx.raw.res; // ServerResponse
228
+ ```ts
229
+ import { compose } from '@nextrush/core';
316
230
 
317
- // Bun/Deno/Edge adapters
318
- ctx.raw.req; // Request (Web API)
231
+ const chain = compose([mwA, mwB, handler], { warnDoubleResponse: true });
232
+ await chain(ctx); // run the composed chain against a Context
319
233
  ```
320
234
 
321
- ## Error Handling
235
+ `compose()` is the pipeline on its own — useful for building sub-pipelines or your own runtime. It validates its input, snapshots the array, and enforces single-call `next()` semantics.
322
236
 
323
- ### Custom Error Handler
237
+ ## API overview
324
238
 
325
- ```typescript
326
- app.setErrorHandler((error, ctx) => {
327
- console.error('Request failed:', error);
239
+ The sealed public surface (ADR-0005).
328
240
 
329
- if ('status' in error && typeof error.status === 'number') {
330
- ctx.status = error.status;
331
- } else {
332
- ctx.status = 500;
333
- }
241
+ | Export | Signature | Since | Stability | Description |
242
+ | ------ | --------- | ----- | --------- | ----------- |
243
+ | `createApp` | `(options?: ApplicationOptions) => Application` | `3.0.0` | Stable ✅ | Create an `Application`. (The `nextrush` meta-package wraps this to inject a router.) |
244
+ | `Application` | `class` | `3.0.0` | Stable ✅ | The application object (methods below). |
245
+ | `compose` | `(middleware: Middleware[], options?: ComposeOptions) => ComposedMiddleware` | `3.0.0` | Stable ✅ | Koa-style middleware composer. |
246
+ | `isMiddleware` | `(fn: unknown) => fn is Middleware` | `3.0.0` | Stable ✅ | Type guard — a middleware is any function. |
247
+ | `flattenMiddleware` | `(arr: (Middleware \| Middleware[])[]) => Middleware[]` | `3.0.0` | Stable ✅ | Flatten nested middleware arrays (bounded depth), validating each entry. |
248
+ | `type ApplicationOptions` · `ErrorHandler` · `ListenCallback` · `Routable` | — | `3.0.0` | Stable ✅ | Application-level contracts. |
249
+ | `type ComposeOptions` · `ComposedMiddleware` | — | `3.0.0` | Stable ✅ | Composition contracts. |
250
+ | Error classes | `NextRushError` · `HttpError` · `BadRequestError` · `UnauthorizedError` · `ForbiddenError` · `NotFoundError` · `InternalServerError` | `3.0.0` | Stable ✅ | Re-exported from [`@nextrush/errors`](../errors) for convenience. |
251
+ | Type re-exports | `Context` · `ContextState` · `Extension` · `ExtensionContext` · `ExtensionHost` · `Middleware` · `Next` · `Router` · `RouteEntry` · `RouteHandler` · `RouteParams` · `QueryParams` · `HttpMethod` · `HttpStatusCode` · `Logger` | — | `3.0.0` | Stable ✅ | Re-exported from [`@nextrush/types`](../types). |
252
+ | Constant re-exports | `ContentType` · `HttpStatus` | — | `3.0.0` | Stable ✅ | Re-exported from `@nextrush/types`. |
334
253
 
335
- ctx.json({
336
- error: error.message,
337
- code: error.code || 'UNKNOWN',
338
- });
339
- });
340
- ```
254
+ ### `Application` methods
341
255
 
342
- > **Note:** `onError()` is deprecated. Use `setErrorHandler()` instead.
256
+ | Member | Signature | Description |
257
+ | ------ | --------- | ----------- |
258
+ | `use` | `(...middleware: Middleware[]) => this` | Register middleware. Throws after boot; a non-function throws `TypeError`. |
259
+ | `route` | `(path: string, router: Routable) => this` | Mount anything with a `routes()` method at a prefix (`/` = root mount). |
260
+ | `get` / `post` / `put` / `patch` / `delete` / `head` / `all` | `(path, ...entries: RouteEntry[]) => this` | Delegate to the app-owned router. Requires a router (see FAQ). No `options()` verb — use `all()`. |
261
+ | `setErrorHandler` | `(handler: ErrorHandler) => this` | Replace the default error handler. |
262
+ | `extend` | `<TDecorated>(extension: Extension<TDecorated>) => this & TDecorated` | Queue an extension; `setup()` runs at `ready()`. |
263
+ | `ready` | `() => Promise<this>` | Boot: run each extension's `setup()`, mount the router last, freeze config. Idempotent. |
264
+ | `callback` | `() => (ctx: Context) => Promise<void>` | The request handler — snapshots the middleware stack and wraps it in the error path. |
265
+ | `start` | `() => void` | Mark the app running (adapters call this after `ready()`). |
266
+ | `close` | `() => Promise<Error[]>` | Graceful shutdown: `destroy()` extensions in reverse order; returns any failures. |
267
+ | `hasDecorator` | `(name: string) => boolean` | Whether a decoration already occupies `name`. |
268
+ | `isReady` / `isRunning` / `isProduction` | `boolean` (getters) | Lifecycle/env state. |
269
+ | `middlewareCount` / `extensionCount` | `number` (getters) | Registration counts. |
270
+ | `logger` / `options` / `router` / `container` | readonly | The configured logger, options, app-owned router, and DI container (last two optional). |
343
271
 
344
- ### Default Behavior
272
+ ## Options
345
273
 
346
- Without a custom handler:
274
+ `createApp(options?)` takes `ApplicationOptions`; `compose(_, options?)` takes `ComposeOptions`.
347
275
 
348
- - **Development**: Error message exposed, stack logged
349
- - **Production**: Generic "Internal Server Error" message
276
+ | Option | Type | Required | Default | Security-sensitive | Description |
277
+ | ------ | ---- | -------- | ------- | ------------------ | ----------- |
278
+ | `env` | `'development' \| 'production' \| 'test'` | No | `'development'` | — | Environment mode. `production` hides dev warnings and error internals. |
279
+ | `proxy` | `boolean` | No | `false` | ⚠️ | Whether to trust proxy headers (`X-Forwarded-For`, etc.). Enable only behind a trusted proxy. |
280
+ | `logger` | `Logger` | No | no-op (silent) | — | Structured logger. Pass `console` for dev; a real logger (`@nextrush/logger`, pino) for prod. |
281
+ | `router` | `Router` | No | `undefined` | — | The app-owned router `app.get(...)` delegates to. The `nextrush` `createApp` injects one. |
282
+ | `container` | `Container` | No | `undefined` | — | Per-app DI container, exposed to extensions/registrars. Injected by `nextrush/class`. |
283
+ | `warnDoubleResponse` | `boolean` (on `ComposeOptions`) | No | `false` | — | Warn when a middleware responds *and* calls `next()`. `Application` enables it in non-production. |
350
284
 
351
- ```typescript
352
- // Production mode hides sensitive details
353
- const app = createApp({ env: 'production' });
285
+ ## Performance
354
286
 
355
- app.use(async () => {
356
- throw new Error('Database connection failed'); // User sees "Internal Server Error"
357
- });
358
- ```
287
+ Core is on the request hot path — `callback()` wraps every request and `compose()` runs it — so the pipeline is built to allocate as little as possible per request:
359
288
 
360
- ### Error Classes
289
+ - **The middleware array is snapshotted once** at `compose()` time, not re-read per request.
290
+ - **Two fast paths bypass the general dispatcher.** An empty chain returns a trivial pass-through; the **single-middleware** case (the overwhelmingly common shape — one mounted router) uses a flat closure with a per-invocation guard instead of the recursive `dispatch` used for longer chains.
291
+ - **`next()` guarding is per-invocation**, declared inside the returned function, so concurrent requests never corrupt each other's call state.
361
292
 
362
- ```typescript
363
- import {
364
- HttpError,
365
- NotFoundError,
366
- BadRequestError,
367
- UnauthorizedError,
368
- ForbiddenError,
369
- InternalServerError,
370
- } from '@nextrush/core';
293
+ > [!NOTE]
294
+ > For reproducible throughput numbers on your own hardware, run the suite in
295
+ > [`apps/benchmark`](https://github.com/0xTanzim/nextRush/tree/main/apps/benchmark). Published
296
+ > figures are being re-measured on a hardened harness — see the
297
+ > [root README's Performance note](https://github.com/0xTanzim/nextRush#performance) — so this
298
+ > package documents the pipeline's allocation characteristics, not point numbers.
371
299
 
372
- app.use(async (ctx) => {
373
- throw new NotFoundError('User not found');
374
- throw new BadRequestError('Invalid email');
375
- throw new UnauthorizedError('Token expired');
376
- });
377
- ```
300
+ ## Compatibility
378
301
 
379
- ## Plugins
302
+ **Requirements**
380
303
 
381
- ### Using Plugins
304
+ | Requirement | Version |
305
+ | ----------- | ------- |
306
+ | NextRush | `3.x` |
307
+ | Node.js | `>=22` |
308
+ | TypeScript | `>=5.x` |
382
309
 
383
- ```typescript
384
- import { createApp } from '@nextrush/core';
385
- import { eventsPlugin } from '@nextrush/events';
386
- import { loggerPlugin } from '@nextrush/logger';
387
-
388
- const app = createApp();
389
-
390
- // Sync plugins
391
- app.plugin(eventsPlugin());
392
- app.plugin(loggerPlugin({ level: 'info' }));
310
+ **Runtimes**
393
311
 
394
- // Async plugin plugin() handles both automatically
395
- await app.plugin(databasePlugin({ uri: '...' }));
396
- ```
397
-
398
- > **Note:** `pluginAsync()` is deprecated. Use `plugin()` instead — it detects async `install()` methods and returns a `Promise` when needed.
312
+ | Runtime | Supported | Notes |
313
+ | ------- | --------- | ----- |
314
+ | Node.js `>=22` | ✅ | ESM-only |
315
+ | Bun / Deno / Edge | ✅ / ✅ / ✅ | Uses only Web-standard JavaScript; no `node:*` API. Parity held by `@nextrush/adapter-*` |
399
316
 
400
- ### Plugin API
317
+ **Integration**
318
+ - **Peer dependencies:** none — depends on `@nextrush/errors` (default error shape) and `@nextrush/types` (contracts; types erased at build).
319
+ - **Works with:** [`@nextrush/router`](../router) (mounted via `app.route()`), [`@nextrush/runtime`](../runtime) (`listen`), `@nextrush/adapter-*` (build the `Context`), any `@nextrush/*` middleware/extension.
320
+ - **Incompatible with:** none.
401
321
 
402
- ```typescript
403
- app.plugin(plugin); // Install plugin (handles sync and async)
404
- app.hasPlugin('plugin-name'); // Check if installed
405
- app.getPlugin('plugin-name'); // Get plugin instance
406
- ```
322
+ > [!IMPORTANT]
323
+ > NextRush is **ESM-only, permanently** no CommonJS build. On Node `>=22`, CommonJS consumers
324
+ > can `require()` this ESM package natively. See the
325
+ > [Module Format Policy](https://github.com/0xTanzim/nextRush#module-format-policy).
407
326
 
408
- ### Creating Plugins
327
+ ---
409
328
 
410
- ```typescript
411
- import type { Plugin } from '@nextrush/types';
329
+ ## Troubleshooting
412
330
 
413
- interface MyPluginOptions {
414
- debug: boolean;
415
- }
331
+ <details>
332
+ <summary><strong><code>Error: No router configured</code> when calling <code>app.get(...)</code></strong></summary>
416
333
 
417
- function myPlugin(options: MyPluginOptions): Plugin {
418
- return {
419
- name: 'my-plugin',
420
-
421
- install(app) {
422
- // Add middleware
423
- app.use(async (ctx, next) => {
424
- if (options.debug) {
425
- console.log(`${ctx.method} ${ctx.path}`);
426
- }
427
- await next();
428
- });
429
- },
430
-
431
- destroy() {
432
- // Cleanup on app.close()
433
- },
434
- };
435
- }
436
-
437
- // Usage
438
- app.plugin(myPlugin({ debug: true }));
334
+ **Cause:** you created the app with `@nextrush/core`'s own `createApp()`, which does **not** inject a router. **Fix:** import `createApp` from `nextrush` (it injects one), or pass one explicitly.
335
+
336
+ ```ts
337
+ import { createApp } from 'nextrush'; // ← injects a router; app.get works
338
+ // or, using @nextrush/core directly:
339
+ import { createApp } from '@nextrush/core';
340
+ import { createRouter } from '@nextrush/router';
341
+ const app = createApp({ router: createRouter() });
439
342
  ```
440
343
 
441
- ## Middleware Composition
442
-
443
- ### compose()
344
+ </details>
444
345
 
445
- Combine multiple middleware into one:
346
+ <details>
347
+ <summary><strong><code>Cannot call use() after the app has booted ... configuration is frozen</code></strong></summary>
446
348
 
447
- ```typescript
448
- import { compose } from '@nextrush/core';
449
-
450
- const security = compose([cors(), helmet(), rateLimit()]);
451
-
452
- app.use(security);
453
- ```
349
+ **Cause:** `use` / `route` / `extend` / `get` were called after `ready()` or `start()` — configuration is frozen once the app boots. **Fix:** register everything before `await app.ready()` (and adapters call `ready()` for you inside `listen`).
454
350
 
455
- ### Utilities
351
+ </details>
456
352
 
457
- ```typescript
458
- import { isMiddleware, flattenMiddleware } from '@nextrush/core';
353
+ <details>
354
+ <summary><strong><code>Error: next() called multiple times</code></strong></summary>
459
355
 
460
- // Check if value is middleware
461
- isMiddleware(fn); // true/false
356
+ **Cause:** a middleware called `next()` (or `ctx.next()`) more than once in the same invocation. Each dispatch allows exactly one advance. **Fix:** call `next()` once; to run work after downstream, `await next()` and continue below it.
462
357
 
463
- // Flatten nested arrays
464
- flattenMiddleware([mw1, [mw2, mw3]]); // [mw1, mw2, mw3]
358
+ ```ts
359
+ app.use(async (ctx) => {
360
+ await ctx.next(); // once
361
+ ctx.set('x-done', '1'); // work after downstream — no second next()
362
+ });
465
363
  ```
466
364
 
467
- ## Router Composition
468
-
469
- Mount routers directly on the application using `app.route()` — Hono-style composition:
470
-
471
- ```typescript
472
- import { createApp } from '@nextrush/core';
473
- import { createRouter } from '@nextrush/router';
474
-
475
- const app = createApp();
476
-
477
- // Create feature routers
478
- const users = createRouter();
479
- users.get('/', (ctx) => ctx.json([]));
480
- users.get('/:id', (ctx) => ctx.json({ id: ctx.params.id }));
365
+ </details>
481
366
 
482
- const posts = createRouter();
483
- posts.get('/', (ctx) => ctx.json([]));
367
+ <details>
368
+ <summary><strong>Console warning: middleware called next() after the response was already committed</strong></summary>
484
369
 
485
- // Mount directlyclean like Hono!
486
- app.route('/api/users', users);
487
- app.route('/api/posts', posts);
488
- ```
370
+ **Cause:** a middleware sent a response (e.g. `ctx.json(...)`) *and* also called `next()`, so downstream may write to a finished response. This warning is on in non-production. **Fix:** either `await next()` to delegate, or respond without calling `next()` not both.
489
371
 
490
- ### Benefits over Classic Pattern
372
+ </details>
491
373
 
492
- | Classic Pattern | Hono-Style Composition |
493
- | ------------------------------------------------------------------- | ---------------------------- |
494
- | `router.use('/users', usersRouter)` then `app.use(router.routes())` | `app.route('/users', users)` |
495
- | Requires main router | Direct mounting |
496
- | Extra `.routes()` call | No extra calls |
374
+ <details>
375
+ <summary><strong>An un-awaited async call crashed the process</strong></summary>
497
376
 
498
- ### Classic Pattern Still Works
377
+ **Cause:** NextRush installs **no** global `unhandledRejection` or `uncaughtException` handler by
378
+ default — the framework's core is deliberately silent on process-level policy, so an application
379
+ owns that decision rather than inheriting a hidden one. Fire-and-forget work (an un-awaited
380
+ `app.events.emit(...)`, a detached WebSocket message handler, a background task started from a
381
+ middleware) that later rejects surfaces as an **unhandled rejection**, and depending on your
382
+ process manager/runtime that can terminate the process. **Fix:** guard every detached call —
383
+ `void somePromise().catch((err) => logger.error(err))` — or explicitly install your own
384
+ `process.on('unhandledRejection', ...)` policy if your deployment needs one. This applies
385
+ everywhere NextRush hands you a promise you don't have to await synchronously: extension
386
+ `destroy()`/`onClose` hooks are awaited and isolated by the framework (see `app.onClose`), but a
387
+ promise your own handler code starts and does not return or await is your responsibility.
499
388
 
500
- ```typescript
501
- // The traditional approach still works
502
- const router = createRouter();
503
- router.use('/users', usersRouter);
504
- router.use('/posts', postsRouter);
505
- app.route('/', router);
506
- ```
507
-
508
- ## Lifecycle
509
-
510
- ### Starting
389
+ ```ts
390
+ // Risky a rejection here is unhandled, not caught by anything in the pipeline.
391
+ app.use(async (ctx) => {
392
+ app.events.emit('audit:logged', { path: ctx.path }); // not awaited
393
+ ctx.json({ ok: true });
394
+ });
511
395
 
512
- ```typescript
513
- // Adapters call app.start() internally
514
- app.start();
515
- console.log(app.isRunning); // true
396
+ // Safe — the detached call is explicitly guarded.
397
+ app.use(async (ctx) => {
398
+ void app.events.emit('audit:logged', { path: ctx.path }).catch((err: unknown) => {
399
+ app.logger.error('audit emit failed:', err);
400
+ });
401
+ ctx.json({ ok: true });
402
+ });
516
403
  ```
517
404
 
518
- ### Shutdown
405
+ </details>
519
406
 
520
- ```typescript
521
- // Graceful shutdown — returns errors from plugins that failed to destroy
522
- const errors = await app.close();
407
+ ## FAQ
523
408
 
524
- // What happens:
525
- // 1. Sets isRunning = false
526
- // 2. Calls destroy() on all plugins (reverse registration order)
527
- // 3. Collects errors via Promise.allSettled
528
- // 4. Clears plugin registry
529
- // 5. Returns Error[] (empty on success)
530
- ```
409
+ **Why does `app.get(...)` work with `nextrush`'s `createApp` but throw with `@nextrush/core`'s?**
410
+ The `nextrush` meta-package's `createApp` injects an app-owned router, so the route shortcuts have something to delegate to. `@nextrush/core`'s `createApp` is router-agnostic by design — pass `createApp({ router: createRouter() })` if you use it directly.
531
411
 
532
- ## Request Handler
412
+ **Why does configuration freeze after `ready()`?**
413
+ Boot runs every extension's `setup()`, mounts the app-owned router last, and snapshots state. Allowing `use`/`route`/`extend` after that would mean middleware or extensions that never boot, or a router mounted in the wrong order — so those methods throw once the app is ready.
533
414
 
534
- Get the callback for HTTP server integration:
415
+ **Why ESM-only?**
416
+ See the [Module Format Policy](https://github.com/0xTanzim/nextRush#module-format-policy).
535
417
 
536
- ```typescript
537
- const callback = app.callback();
418
+ **Does it work on Bun, Deno, and Edge?**
419
+ Yes. Core uses only Web-standard JavaScript — no `node:*` APIs, no `process` — so it behaves identically on every supported runtime; platform specifics live in `@nextrush/adapter-*`, and parity is enforced by the conformance suite.
538
420
 
539
- // Use with Node.js http
540
- import http from 'http';
541
- http.createServer(callback).listen(3000);
421
+ ---
542
422
 
543
- // Or use an adapter (recommended)
544
- import { listen } from '@nextrush/adapter-node';
545
- listen(app, { port: 3000 });
546
- ```
423
+ ## Package relationships
547
424
 
548
- ## API Reference
549
-
550
- ### Exports
551
-
552
- ```typescript
553
- import {
554
- // Application
555
- createApp,
556
- Application,
557
-
558
- // Middleware
559
- compose,
560
- isMiddleware,
561
- flattenMiddleware,
562
-
563
- // Errors
564
- HttpError,
565
- NextRushError,
566
- NotFoundError,
567
- BadRequestError,
568
- UnauthorizedError,
569
- ForbiddenError,
570
- InternalServerError,
571
- createHttpError,
572
-
573
- // Re-exports from @nextrush/types
574
- HttpStatus,
575
- ContentType,
576
- } from '@nextrush/core';
577
- ```
425
+ ```text
426
+ depends on @nextrush/errors (default error response shape)
427
+ @nextrush/core ─────────────▶ @nextrush/types (Context / Middleware / Extension contracts)
578
428
 
579
- ### Types
580
-
581
- ```typescript
582
- import type {
583
- // Application
584
- ApplicationOptions,
585
- ErrorHandler,
586
- ListenCallback,
587
- Logger,
588
- Routable,
589
- ComposedMiddleware,
590
-
591
- // Context & Middleware (from @nextrush/types)
592
- Context,
593
- ContextState,
594
- Middleware,
595
- Next,
596
- Plugin,
597
- PluginWithHooks,
598
- RouteHandler,
599
- RouteParams,
600
- QueryParams,
601
- HttpMethod,
602
- HttpStatusCode,
603
- } from '@nextrush/core';
429
+ often used with @nextrush/router (mounted via app.route())
430
+ usually used next @nextrush/runtime (listen) · @nextrush/adapter-* (build Context)
604
431
  ```
605
432
 
606
- ## Runtime Compatibility
433
+ - **Depends on:** [`@nextrush/errors`](../errors) — the default error path reuses its serializer · [`@nextrush/types`](../types) — the shared `Context` / `Middleware` / `Extension` contracts (types erased at build).
434
+ - **Often used with:** [`@nextrush/router`](../router) — the router `app.route()` mounts and `app.get(...)` delegates to.
435
+ - **Usually used next:** [`@nextrush/runtime`](../runtime) — `listen(app, port)` boots and starts the app · `@nextrush/adapter-*` — build the concrete `Context` from a platform request.
436
+ - **Alternative:** none — `Application` and `compose` are the framework's composition core.
607
437
 
608
- | Runtime | Supported |
609
- | ------------------- | --------- |
610
- | Node.js 22+ | ✅ |
611
- | Bun 1.0+ | ✅ |
612
- | Deno 2.0+ | ✅ |
613
- | Cloudflare Workers | ✅ |
614
- | Vercel Edge Runtime | ✅ |
438
+ ## Architecture
615
439
 
616
- The core package uses only standard JavaScript APIs. Runtime-specific code lives in adapters.
440
+ Maintaining or contributing to this package? The internal design the `Application` lifecycle, the
441
+ `compose()` dispatcher and its fast paths, prefix mounting, the extension boot/teardown model, the
442
+ default error path, the architectural invariants, and the decisions and trade-offs behind them
443
+ (with diagrams) — is in **[`ARCHITECTURE.md`](./ARCHITECTURE.md)**. Design history:
444
+ [extension model RFC](https://github.com/0xTanzim/nextRush/tree/main/docs/RFC/class-runtime), [ADR-0005 (package tiers & sealed surface)](https://github.com/0xTanzim/nextRush/blob/main/docs/adr/ADR-0005-package-tiers-sealed-surface-deprecation.md).
617
445
 
618
- ## Package Size
446
+ ## Resources
619
447
 
620
- - **Bundle**: ~10 KB
621
- - **Types**: ~8 KB
622
- - **Dependencies**: `@nextrush/types`, `@nextrush/errors`
448
+ - 📖 **Learn** [Documentation](https://0xtanzim.github.io/nextRush/docs) · [Middleware concept](https://0xtanzim.github.io/nextRush/docs/concepts) · [Architecture](./ARCHITECTURE.md) · [RFCs](https://github.com/0xTanzim/nextRush/tree/main/docs/RFC)
449
+ - 📝 **Changelog** [CHANGELOG.md](./CHANGELOG.md)
450
+ - 🐛 **Report an issue** — [GitHub Issues](https://github.com/0xTanzim/nextRush/issues)
451
+ - 🤝 **Contribute** — [CONTRIBUTING.md](https://github.com/0xTanzim/nextRush/blob/main/CONTRIBUTING.md)
623
452
 
624
- ## License
453
+ ---
625
454
 
626
- MIT
455
+ MIT © [Tanzim Hossain](https://github.com/0xTanzim)