@nextrush/core 3.0.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 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,626 @@
1
+ # @nextrush/core
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**:
18
+
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
23
+
24
+ Everything else (routing, body parsing, authentication) lives in separate packages. You install what you use.
25
+
26
+ ## Mental Model
27
+
28
+ Think of the core as a **middleware pipeline manager**:
29
+
30
+ ```
31
+ Request → [Middleware 1] → [Middleware 2] → [Handler] → [Middleware 2] → [Middleware 1] → Response
32
+ ↓ ↓ ↓ ↑ ↑
33
+ Before Before Execute After After
34
+ ```
35
+
36
+ Each middleware can:
37
+
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
41
+
42
+ This is the "onion model" - requests flow inward, responses flow outward.
43
+
44
+ ## Installation
45
+
46
+ ```bash
47
+ pnpm add @nextrush/core
48
+ ```
49
+
50
+ ## Quick Start
51
+
52
+ ```typescript
53
+ import { createApp } from '@nextrush/core';
54
+ import { listen } from '@nextrush/adapter-node';
55
+
56
+ const app = createApp();
57
+
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
+ });
64
+
65
+ // Handler
66
+ app.use(async (ctx) => {
67
+ ctx.json({ message: 'Hello World' });
68
+ });
69
+
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();
82
+
83
+ // With options
84
+ const app = createApp({
85
+ env: 'production', // 'development' | 'production' | 'test'
86
+ proxy: true, // Trust proxy headers (X-Forwarded-*)
87
+ });
88
+ ```
89
+
90
+ ### Application Options
91
+
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. |
97
+
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
+ ```
107
+
108
+ ## Middleware
109
+
110
+ ### Registration
111
+
112
+ ```typescript
113
+ // Single middleware
114
+ app.use(async (ctx, next) => {
115
+ await next();
116
+ });
117
+
118
+ // Multiple middleware
119
+ app.use(middleware1, middleware2, middleware3);
120
+
121
+ // Method chaining
122
+ app.use(cors()).use(helmet()).use(json());
123
+ ```
124
+
125
+ ### Two Syntax Styles
126
+
127
+ NextRush supports both modern and traditional Koa-style middleware:
128
+
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
+ });
143
+ ```
144
+
145
+ Both styles work identically. Use whichever you prefer.
146
+
147
+ ### Middleware Order
148
+
149
+ Middleware executes in registration order (onion model):
150
+
151
+ ```typescript
152
+ app.use(async (ctx, next) => {
153
+ console.log('1: Start');
154
+ await next();
155
+ console.log('1: End');
156
+ });
157
+
158
+ app.use(async (ctx, next) => {
159
+ console.log('2: Start');
160
+ await next();
161
+ console.log('2: End');
162
+ });
163
+
164
+ 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`);
190
+ });
191
+ ```
192
+
193
+ ### Early Termination
194
+
195
+ Skip remaining middleware by not calling `next()`:
196
+
197
+ ```typescript
198
+ 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
+ await next();
205
+ });
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
+
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
+ });
248
+ ```
249
+
250
+ ### Response
251
+
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 |
261
+
262
+ ```typescript
263
+ app.use(async (ctx) => {
264
+ // Set status
265
+ ctx.status = 201;
266
+
267
+ // Set headers
268
+ ctx.set('X-Request-Id', '12345');
269
+ ctx.set('Cache-Control', 'no-cache');
270
+
271
+ // Send JSON
272
+ ctx.json({ created: true });
273
+ });
274
+ ```
275
+
276
+ ### Error Helpers
277
+
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
283
+
284
+ // Assert condition
285
+ ctx.assert(user, 404, 'User not found');
286
+ ctx.assert(user.isAdmin, 403, 'Admin required');
287
+ });
288
+ ```
289
+
290
+ ### State
291
+
292
+ Share data between middleware:
293
+
294
+ ```typescript
295
+ // Auth middleware
296
+ app.use(async (ctx, next) => {
297
+ ctx.state.user = await validateToken(ctx.get('Authorization'));
298
+ await next();
299
+ });
300
+
301
+ // Handler
302
+ app.get('/profile', (ctx) => {
303
+ const user = ctx.state.user;
304
+ ctx.json({ user });
305
+ });
306
+ ```
307
+
308
+ ### Raw Access
309
+
310
+ Access platform-specific objects:
311
+
312
+ ```typescript
313
+ // Node.js adapter
314
+ ctx.raw.req; // IncomingMessage
315
+ ctx.raw.res; // ServerResponse
316
+
317
+ // Bun/Deno/Edge adapters
318
+ ctx.raw.req; // Request (Web API)
319
+ ```
320
+
321
+ ## Error Handling
322
+
323
+ ### Custom Error Handler
324
+
325
+ ```typescript
326
+ app.setErrorHandler((error, ctx) => {
327
+ console.error('Request failed:', error);
328
+
329
+ if ('status' in error && typeof error.status === 'number') {
330
+ ctx.status = error.status;
331
+ } else {
332
+ ctx.status = 500;
333
+ }
334
+
335
+ ctx.json({
336
+ error: error.message,
337
+ code: error.code || 'UNKNOWN',
338
+ });
339
+ });
340
+ ```
341
+
342
+ > **Note:** `onError()` is deprecated. Use `setErrorHandler()` instead.
343
+
344
+ ### Default Behavior
345
+
346
+ Without a custom handler:
347
+
348
+ - **Development**: Error message exposed, stack logged
349
+ - **Production**: Generic "Internal Server Error" message
350
+
351
+ ```typescript
352
+ // Production mode hides sensitive details
353
+ const app = createApp({ env: 'production' });
354
+
355
+ app.use(async () => {
356
+ throw new Error('Database connection failed'); // User sees "Internal Server Error"
357
+ });
358
+ ```
359
+
360
+ ### Error Classes
361
+
362
+ ```typescript
363
+ import {
364
+ HttpError,
365
+ NotFoundError,
366
+ BadRequestError,
367
+ UnauthorizedError,
368
+ ForbiddenError,
369
+ InternalServerError,
370
+ } from '@nextrush/core';
371
+
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
+ ```
378
+
379
+ ## Plugins
380
+
381
+ ### Using Plugins
382
+
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' }));
393
+
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.
399
+
400
+ ### Plugin API
401
+
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
+ ```
407
+
408
+ ### Creating Plugins
409
+
410
+ ```typescript
411
+ import type { Plugin } from '@nextrush/types';
412
+
413
+ interface MyPluginOptions {
414
+ debug: boolean;
415
+ }
416
+
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 }));
439
+ ```
440
+
441
+ ## Middleware Composition
442
+
443
+ ### compose()
444
+
445
+ Combine multiple middleware into one:
446
+
447
+ ```typescript
448
+ import { compose } from '@nextrush/core';
449
+
450
+ const security = compose([cors(), helmet(), rateLimit()]);
451
+
452
+ app.use(security);
453
+ ```
454
+
455
+ ### Utilities
456
+
457
+ ```typescript
458
+ import { isMiddleware, flattenMiddleware } from '@nextrush/core';
459
+
460
+ // Check if value is middleware
461
+ isMiddleware(fn); // true/false
462
+
463
+ // Flatten nested arrays
464
+ flattenMiddleware([mw1, [mw2, mw3]]); // [mw1, mw2, mw3]
465
+ ```
466
+
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 }));
481
+
482
+ const posts = createRouter();
483
+ posts.get('/', (ctx) => ctx.json([]));
484
+
485
+ // Mount directly — clean like Hono!
486
+ app.route('/api/users', users);
487
+ app.route('/api/posts', posts);
488
+ ```
489
+
490
+ ### Benefits over Classic Pattern
491
+
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 |
497
+
498
+ ### Classic Pattern Still Works
499
+
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
511
+
512
+ ```typescript
513
+ // Adapters call app.start() internally
514
+ app.start();
515
+ console.log(app.isRunning); // true
516
+ ```
517
+
518
+ ### Shutdown
519
+
520
+ ```typescript
521
+ // Graceful shutdown — returns errors from plugins that failed to destroy
522
+ const errors = await app.close();
523
+
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
+ ```
531
+
532
+ ## Request Handler
533
+
534
+ Get the callback for HTTP server integration:
535
+
536
+ ```typescript
537
+ const callback = app.callback();
538
+
539
+ // Use with Node.js http
540
+ import http from 'http';
541
+ http.createServer(callback).listen(3000);
542
+
543
+ // Or use an adapter (recommended)
544
+ import { listen } from '@nextrush/adapter-node';
545
+ listen(app, { port: 3000 });
546
+ ```
547
+
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
+ ```
578
+
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';
604
+ ```
605
+
606
+ ## Runtime Compatibility
607
+
608
+ | Runtime | Supported |
609
+ | ------------------- | --------- |
610
+ | Node.js 22+ | ✅ |
611
+ | Bun 1.0+ | ✅ |
612
+ | Deno 2.0+ | ✅ |
613
+ | Cloudflare Workers | ✅ |
614
+ | Vercel Edge Runtime | ✅ |
615
+
616
+ The core package uses only standard JavaScript APIs. Runtime-specific code lives in adapters.
617
+
618
+ ## Package Size
619
+
620
+ - **Bundle**: ~10 KB
621
+ - **Types**: ~8 KB
622
+ - **Dependencies**: `@nextrush/types`, `@nextrush/errors`
623
+
624
+ ## License
625
+
626
+ MIT