exisjs 0.7.5 → 0.7.6
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/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +156 -85
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/start.d.ts.map +1 -1
- package/dist/cli/commands/start.js +54 -16
- package/dist/cli/commands/start.js.map +1 -1
- package/dist/cli/index.js +10 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/manifest.d.ts.map +1 -1
- package/dist/cli/manifest.js +106 -13
- package/dist/cli/manifest.js.map +1 -1
- package/dist/cli/resolve-aliases.d.ts +1 -1
- package/dist/cli/resolve-aliases.d.ts.map +1 -1
- package/dist/cli/resolve-aliases.js +30 -13
- package/dist/cli/resolve-aliases.js.map +1 -1
- package/dist/config/config.d.ts +3 -1
- package/dist/config/config.d.ts.map +1 -1
- package/dist/config/config.js.map +1 -1
- package/dist/config/types.d.ts +2 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/decorators/middleware.d.ts +65 -2
- package/dist/decorators/middleware.d.ts.map +1 -1
- package/dist/decorators/middleware.js +148 -62
- package/dist/decorators/middleware.js.map +1 -1
- package/dist/decorators/params.d.ts +5 -0
- package/dist/decorators/params.d.ts.map +1 -1
- package/dist/decorators/params.js +11 -1
- package/dist/decorators/params.js.map +1 -1
- package/dist/decorators/response.d.ts.map +1 -1
- package/dist/decorators/response.js +32 -22
- package/dist/decorators/response.js.map +1 -1
- package/dist/di/container.d.ts.map +1 -1
- package/dist/di/container.js +6 -1
- package/dist/di/container.js.map +1 -1
- package/dist/docs/content/cli/commands.mdx +1 -1
- package/dist/docs/content/controllers.mdx +127 -10
- package/dist/docs/content/decorators.mdx +22 -4
- package/dist/docs/content/introduction.mdx +7 -7
- package/dist/docs/content/middleware.mdx +269 -6
- package/dist/docs/content/requests.mdx +180 -6
- package/dist/docs/content/responses.mdx +283 -32
- package/dist/docs/content/routing.mdx +46 -0
- package/dist/docs/content/validation.mdx +190 -11
- package/dist/docs/reference/context.mdx +11 -0
- package/dist/docs/reference/decorators.mdx +14 -4
- package/dist/docs/reference/error.mdx +44 -0
- package/dist/docs/reference/logger.mdx +175 -43
- package/dist/docs/reference/middleware.mdx +31 -0
- package/dist/docs/reference/router.mdx +22 -0
- package/dist/docs/reference/validator.mdx +83 -23
- package/dist/error/errors.d.ts.map +1 -1
- package/dist/error/errors.js +95 -43
- package/dist/error/errors.js.map +1 -1
- package/dist/error/index.d.ts +1 -0
- package/dist/error/index.d.ts.map +1 -1
- package/dist/error/index.js +11 -1
- package/dist/error/index.js.map +1 -1
- package/dist/error/overlay.d.ts +42 -0
- package/dist/error/overlay.d.ts.map +1 -0
- package/dist/error/overlay.js +306 -0
- package/dist/error/overlay.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/start-server.js +33 -7
- package/dist/lib/start-server.js.map +1 -1
- package/dist/logger/index.d.ts +1 -1
- package/dist/logger/index.d.ts.map +1 -1
- package/dist/logger/index.js +9 -1
- package/dist/logger/index.js.map +1 -1
- package/dist/middleware/guard.d.ts +2 -2
- package/dist/middleware/guard.d.ts.map +1 -1
- package/dist/middleware/guard.js.map +1 -1
- package/dist/middleware/ip-filter.d.ts +1 -0
- package/dist/middleware/ip-filter.d.ts.map +1 -1
- package/dist/middleware/ip-filter.js +2 -0
- package/dist/middleware/ip-filter.js.map +1 -1
- package/dist/middleware/middleware.d.ts +2 -2
- package/dist/middleware/middleware.d.ts.map +1 -1
- package/dist/middleware/middleware.js +20 -2
- package/dist/middleware/middleware.js.map +1 -1
- package/dist/middleware/security.d.ts +43 -1
- package/dist/middleware/security.d.ts.map +1 -1
- package/dist/middleware/security.js +101 -0
- package/dist/middleware/security.js.map +1 -1
- package/dist/response/index.d.ts +229 -6
- package/dist/response/index.d.ts.map +1 -1
- package/dist/response/index.js +272 -2
- package/dist/response/index.js.map +1 -1
- package/dist/router/boundary.d.ts +17 -9
- package/dist/router/boundary.d.ts.map +1 -1
- package/dist/router/boundary.js.map +1 -1
- package/dist/router/controller-registrar.d.ts.map +1 -1
- package/dist/router/controller-registrar.js +56 -1
- package/dist/router/controller-registrar.js.map +1 -1
- package/dist/router/index.d.ts +2 -2
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +2 -1
- package/dist/router/index.js.map +1 -1
- package/dist/router/route-builder.d.ts +70 -36
- package/dist/router/route-builder.d.ts.map +1 -1
- package/dist/router/route-builder.js +13 -15
- package/dist/router/route-builder.js.map +1 -1
- package/dist/router/route-scanner.d.ts.map +1 -1
- package/dist/router/route-scanner.js +24 -2
- package/dist/router/route-scanner.js.map +1 -1
- package/dist/router/router.d.ts.map +1 -1
- package/dist/router/router.js +35 -17
- package/dist/router/router.js.map +1 -1
- package/dist/router/types.d.ts +8 -0
- package/dist/router/types.d.ts.map +1 -1
- package/dist/server/app.d.ts +1 -0
- package/dist/server/app.d.ts.map +1 -1
- package/dist/server/app.js +10 -0
- package/dist/server/app.js.map +1 -1
- package/dist/server/bootstrapper.d.ts.map +1 -1
- package/dist/server/bootstrapper.js +91 -30
- package/dist/server/bootstrapper.js.map +1 -1
- package/dist/server/cluster.d.ts.map +1 -1
- package/dist/server/cluster.js +13 -2
- package/dist/server/cluster.js.map +1 -1
- package/dist/server/context.d.ts +8 -0
- package/dist/server/context.d.ts.map +1 -1
- package/dist/server/context.js +50 -6
- package/dist/server/context.js.map +1 -1
- package/dist/server/lifecycle.d.ts +22 -0
- package/dist/server/lifecycle.d.ts.map +1 -0
- package/dist/server/lifecycle.js +106 -0
- package/dist/server/lifecycle.js.map +1 -0
- package/dist/server/request-handler.d.ts +1 -0
- package/dist/server/request-handler.d.ts.map +1 -1
- package/dist/server/request-handler.js +27 -17
- package/dist/server/request-handler.js.map +1 -1
- package/dist/server/request.d.ts.map +1 -1
- package/dist/server/request.js +18 -0
- package/dist/server/request.js.map +1 -1
- package/dist/server/response.d.ts +10 -1
- package/dist/server/response.d.ts.map +1 -1
- package/dist/server/response.js +154 -2
- package/dist/server/response.js.map +1 -1
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +6 -1
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/mocks.d.ts.map +1 -1
- package/dist/testing/mocks.js +15 -3
- package/dist/testing/mocks.js.map +1 -1
- package/dist/utils/logger.d.ts +8 -0
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +184 -1
- package/dist/utils/logger.js.map +1 -1
- package/dist/validator/tex-types.d.ts +2 -1
- package/dist/validator/tex-types.d.ts.map +1 -1
- package/dist/validator/tex-types.js +2 -1
- package/dist/validator/tex-types.js.map +1 -1
- package/dist/validator/tex.d.ts +31 -15
- package/dist/validator/tex.d.ts.map +1 -1
- package/dist/validator/tex.js +280 -42
- package/dist/validator/tex.js.map +1 -1
- package/package.json +4 -4
- package/dist/dev/error-overlay.d.ts +0 -3
- package/dist/dev/error-overlay.d.ts.map +0 -1
- package/dist/dev/error-overlay.js +0 -118
- package/dist/dev/error-overlay.js.map +0 -1
|
@@ -97,7 +97,9 @@ Handling `multipart/form-data` in Node.js can be difficult. ExisJS provides mult
|
|
|
97
97
|
|
|
98
98
|
### A. Declarative Route Uploads (Recommended)
|
|
99
99
|
|
|
100
|
-
You can declare file upload requirements directly inside your route schema. ExisJS automatically executes the multipart parser, validates MIME types, enforces byte caps, and populates `req.file` or `req.files` along with non-file fields in `req.body`:
|
|
100
|
+
You can declare file upload requirements directly inside your route schema or decorators. ExisJS automatically executes the multipart parser, validates MIME types, enforces byte caps, and populates `req.file` or `req.files` along with non-file fields in `req.body`:
|
|
101
|
+
|
|
102
|
+
#### Functional Paradigm
|
|
101
103
|
|
|
102
104
|
```typescript filename="src/http/upload/route.ts"
|
|
103
105
|
import { controller, route } from 'exisjs/router'
|
|
@@ -128,10 +130,47 @@ export default controller({
|
|
|
128
130
|
})
|
|
129
131
|
```
|
|
130
132
|
|
|
133
|
+
#### Class-Based (OOP) Paradigm
|
|
134
|
+
|
|
135
|
+
```typescript filename="src/http/upload/route.ts"
|
|
136
|
+
import { Controller, Post, UseUpload, UploadedFile, Body } from 'exisjs/decorators'
|
|
137
|
+
import { tex } from 'exisjs/validator'
|
|
138
|
+
import type { UploadedFile as IUploadedFile } from 'exisjs/router'
|
|
139
|
+
|
|
140
|
+
const TitleSchema = tex.object({
|
|
141
|
+
title: tex.string({ min: 1 }),
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
@Controller('/upload')
|
|
145
|
+
export default class UploadController {
|
|
146
|
+
@Post('/')
|
|
147
|
+
@UseUpload({
|
|
148
|
+
field: 'photo',
|
|
149
|
+
maxBytes: 10 * 1024 * 1024,
|
|
150
|
+
mimeTypes: ['image/jpeg', 'image/png', 'image/webp'],
|
|
151
|
+
})
|
|
152
|
+
async uploadPhoto(
|
|
153
|
+
@UploadedFile() file: IUploadedFile,
|
|
154
|
+
@Body(TitleSchema) body: { title: string }
|
|
155
|
+
) {
|
|
156
|
+
console.log(`Uploaded ${file.originalname} (${file.size} bytes)`)
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
success: true,
|
|
160
|
+
filename: file.filename,
|
|
161
|
+
mimetype: file.mimetype,
|
|
162
|
+
title: body.title,
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
131
168
|
### B. First-Party `fileUpload` Middleware
|
|
132
169
|
|
|
133
170
|
ExisJS exports a dedicated `fileUpload` middleware from `exisjs/middleware` supporting single, multiple, and multi-field uploads:
|
|
134
171
|
|
|
172
|
+
#### Functional Paradigm
|
|
173
|
+
|
|
135
174
|
```typescript filename="src/http/documents/route.ts"
|
|
136
175
|
import { controller, route } from 'exisjs/router'
|
|
137
176
|
import { fileUpload } from 'exisjs/middleware'
|
|
@@ -173,10 +212,44 @@ export default controller({
|
|
|
173
212
|
})
|
|
174
213
|
```
|
|
175
214
|
|
|
215
|
+
#### Class-Based (OOP) Paradigm
|
|
216
|
+
|
|
217
|
+
```typescript filename="src/http/documents/route.ts"
|
|
218
|
+
import { Controller, Post, Use, UploadedFile, UploadedFiles } from 'exisjs/decorators'
|
|
219
|
+
import { fileUpload } from 'exisjs/middleware'
|
|
220
|
+
import type { UploadedFile as IUploadedFile } from 'exisjs/router'
|
|
221
|
+
|
|
222
|
+
@Controller('/documents')
|
|
223
|
+
export default class DocumentsController {
|
|
224
|
+
// Single file upload
|
|
225
|
+
@Post('/single')
|
|
226
|
+
@Use(fileUpload.single('document', { maxBytes: 15 * 1024 * 1024, dest: './uploads/docs' }))
|
|
227
|
+
async uploadSingle(@UploadedFile() file: IUploadedFile) {
|
|
228
|
+
return { file }
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Multiple files with the same field name
|
|
232
|
+
@Post('/gallery')
|
|
233
|
+
@Use(fileUpload.array('photos', 5, { mimeTypes: ['image/*'] }))
|
|
234
|
+
async uploadGallery(@UploadedFiles() files: IUploadedFile[]) {
|
|
235
|
+
return { count: files.length, files }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Multiple distinct fields
|
|
239
|
+
@Post('/profile')
|
|
240
|
+
@Use(fileUpload.fields([{ name: 'avatar', maxCount: 1 }, { name: 'cover', maxCount: 1 }]))
|
|
241
|
+
async uploadProfile(@UploadedFiles() files: Record<string, IUploadedFile[]>) {
|
|
242
|
+
return { files }
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
176
247
|
### C. Integrating Express/Connect Middleware (e.g. Multer)
|
|
177
248
|
|
|
178
249
|
If your enterprise project relies on existing Express middleware like **Multer** (for custom storage engines, S3 direct-to-cloud streams, or specialized filters), ExisJS provides the `fromExpress()` bridge:
|
|
179
250
|
|
|
251
|
+
#### Functional Paradigm
|
|
252
|
+
|
|
180
253
|
```typescript filename="src/http/legacy/route.ts"
|
|
181
254
|
import { controller, route } from 'exisjs/router'
|
|
182
255
|
import { fromExpress } from 'exisjs/middleware'
|
|
@@ -199,10 +272,36 @@ export default controller({
|
|
|
199
272
|
})
|
|
200
273
|
```
|
|
201
274
|
|
|
275
|
+
#### Class-Based (OOP) Paradigm
|
|
276
|
+
|
|
277
|
+
```typescript filename="src/http/legacy/route.ts"
|
|
278
|
+
import { Controller, Post, Use, Req } from 'exisjs/decorators'
|
|
279
|
+
import { fromExpress } from 'exisjs/middleware'
|
|
280
|
+
import type { Request } from 'exisjs/router'
|
|
281
|
+
import multer from 'multer'
|
|
282
|
+
|
|
283
|
+
const upload = multer({
|
|
284
|
+
storage: multer.memoryStorage(),
|
|
285
|
+
limits: { fileSize: 10 * 1024 * 1024 },
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
@Controller('/legacy')
|
|
289
|
+
export default class LegacyUploadController {
|
|
290
|
+
@Post('/upload')
|
|
291
|
+
@Use(fromExpress(upload.single('file')))
|
|
292
|
+
async upload(@Req() req: Request) {
|
|
293
|
+
const file = (req.raw as any).file || req.file
|
|
294
|
+
return { success: true, originalname: file?.originalname }
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
202
299
|
### D. Manual `req.formData()` & Streaming Uploads
|
|
203
300
|
|
|
204
301
|
For manual control over incoming multipart streams without route middleware:
|
|
205
302
|
|
|
303
|
+
#### Functional Paradigm
|
|
304
|
+
|
|
206
305
|
```typescript filename="src/http/upload/route.ts"
|
|
207
306
|
import { controller, route } from 'exisjs/router'
|
|
208
307
|
|
|
@@ -225,6 +324,29 @@ export default controller({
|
|
|
225
324
|
})
|
|
226
325
|
```
|
|
227
326
|
|
|
327
|
+
#### Class-Based (OOP) Paradigm
|
|
328
|
+
|
|
329
|
+
```typescript filename="src/http/upload/route.ts"
|
|
330
|
+
import { Controller, Post, Req } from 'exisjs/decorators'
|
|
331
|
+
import type { Request } from 'exisjs/router'
|
|
332
|
+
|
|
333
|
+
@Controller('/upload')
|
|
334
|
+
export default class ManualUploadController {
|
|
335
|
+
@Post('/avatar')
|
|
336
|
+
async uploadAvatar(@Req() req: Request) {
|
|
337
|
+
const { fields, files } = await req.formData()
|
|
338
|
+
const uploadedFile = files['profile_pic']
|
|
339
|
+
|
|
340
|
+
if (uploadedFile) {
|
|
341
|
+
const savedPath = await uploadedFile.saveToDisk('./uploads/avatars')
|
|
342
|
+
return { success: true, path: savedPath }
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return { success: false, error: 'No file uploaded' }
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
228
350
|
---
|
|
229
351
|
|
|
230
352
|
## 4. Body Size Limits & HTTP 413
|
|
@@ -260,22 +382,24 @@ ExisJS ships with a **60-second default request timeout** in its `timeout()` mid
|
|
|
260
382
|
|
|
261
383
|
### Dynamic In-Handler Control
|
|
262
384
|
|
|
263
|
-
Handlers can dynamically extend or cancel the active timeout on the fly using `req.setTimeout()` and `req.clearTimeout()`:
|
|
385
|
+
Handlers can dynamically extend or cancel the active timeout on the fly using `req.setTimeout()` and `req.clearTimeout()`, or declaratively via `@Timeout(ms)`:
|
|
386
|
+
|
|
387
|
+
#### Functional Paradigm
|
|
264
388
|
|
|
265
389
|
```typescript filename="src/http/ai/route.ts"
|
|
266
390
|
import { controller, route } from 'exisjs/router'
|
|
267
391
|
|
|
268
392
|
export default controller({
|
|
269
393
|
generateVideo: route.post('/', {
|
|
394
|
+
timeoutMs: 180000, // 1. Declarative 3-minute timeout for this endpoint
|
|
270
395
|
async handle({ req }) {
|
|
271
|
-
//
|
|
272
|
-
req.setTimeout(
|
|
396
|
+
// 2. Or dynamically adjust timeout during processing
|
|
397
|
+
req.setTimeout(240000)
|
|
273
398
|
|
|
274
399
|
try {
|
|
275
400
|
const result = await longRunningAiTask()
|
|
276
401
|
return { success: true, result }
|
|
277
402
|
} finally {
|
|
278
|
-
// 2. Clear timeout once work completes
|
|
279
403
|
req.clearTimeout()
|
|
280
404
|
}
|
|
281
405
|
}
|
|
@@ -283,6 +407,30 @@ export default controller({
|
|
|
283
407
|
})
|
|
284
408
|
```
|
|
285
409
|
|
|
410
|
+
#### Class-Based (OOP) Paradigm
|
|
411
|
+
|
|
412
|
+
```typescript filename="src/http/ai/route.ts"
|
|
413
|
+
import { Controller, Post, Timeout, Req } from 'exisjs/decorators'
|
|
414
|
+
import type { Request } from 'exisjs/router'
|
|
415
|
+
|
|
416
|
+
@Controller('/ai')
|
|
417
|
+
export default class AiTaskController {
|
|
418
|
+
@Post('/generate-video')
|
|
419
|
+
@Timeout(180000) // 1. Declarative 3-minute timeout
|
|
420
|
+
async generateVideo(@Req() req: Request) {
|
|
421
|
+
// 2. Or dynamically adjust on the fly
|
|
422
|
+
req.setTimeout(240000)
|
|
423
|
+
|
|
424
|
+
try {
|
|
425
|
+
const result = await longRunningAiTask()
|
|
426
|
+
return { success: true, result }
|
|
427
|
+
} finally {
|
|
428
|
+
req.clearTimeout()
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
```
|
|
433
|
+
|
|
286
434
|
---
|
|
287
435
|
|
|
288
436
|
## 6. HTTP Headers & Content Negotiation
|
|
@@ -321,7 +469,7 @@ ExisJS provides a first-class standard Web API `req.signal: AbortSignal` tied di
|
|
|
321
469
|
|
|
322
470
|
When a client closes their browser tab, cancels a download, or aborts an HTTP request, `req.signal` immediately triggers an `abort` event. You can pass `req.signal` directly to ORMs, database drivers, and HTTP clients to cancel expensive in-flight operations and eliminate wasted server resources.
|
|
323
471
|
|
|
324
|
-
####
|
|
472
|
+
#### Functional Paradigm
|
|
325
473
|
|
|
326
474
|
```typescript filename="src/http/reports/route.ts"
|
|
327
475
|
import { controller, route } from 'exisjs/router'
|
|
@@ -346,6 +494,32 @@ export default controller({
|
|
|
346
494
|
})
|
|
347
495
|
```
|
|
348
496
|
|
|
497
|
+
#### Class-Based (OOP) Paradigm
|
|
498
|
+
|
|
499
|
+
```typescript filename="src/http/reports/route.ts"
|
|
500
|
+
import { Controller, Get, Req } from 'exisjs/decorators'
|
|
501
|
+
import type { Request } from 'exisjs/router'
|
|
502
|
+
|
|
503
|
+
@Controller('/reports')
|
|
504
|
+
export default class ReportsController {
|
|
505
|
+
@Get('/')
|
|
506
|
+
async heavyReport(@Req() req: Request) {
|
|
507
|
+
// 1. Pass req.signal to fetch()
|
|
508
|
+
const upstream = await fetch('https://api.external.com/data', {
|
|
509
|
+
signal: req.signal,
|
|
510
|
+
})
|
|
511
|
+
const payload = await upstream.json()
|
|
512
|
+
|
|
513
|
+
// 2. Pass req.signal to your database queries
|
|
514
|
+
const results = await db.query(sql`SELECT * FROM heavy_analytics`, {
|
|
515
|
+
signal: req.signal,
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
return { payload, results }
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
```
|
|
522
|
+
|
|
349
523
|
---
|
|
350
524
|
|
|
351
525
|
## 9. Context & Metadata
|
|
@@ -79,14 +79,30 @@ export default class UsersController {
|
|
|
79
79
|
|
|
80
80
|
## 2. Standardized JSON Responses (`exisjs/response`)
|
|
81
81
|
|
|
82
|
-
To keep your APIs consistent, ExisJS
|
|
83
|
-
Using these ensures your entire application adheres to a unified `{ success: boolean, data?: any, error?: any }` structure, which frontend teams love.
|
|
82
|
+
To keep your APIs consistent and predictable across teams, ExisJS provides standard response helpers, envelope constructors, status code constants, and RFC 7807 problem details in `exisjs/response` (also re-exported from the root `exisjs` package).
|
|
84
83
|
|
|
85
|
-
|
|
84
|
+
Using these ensures your entire backend adheres to a clean, strongly-typed `{ success: boolean, data?: any, error?: any, meta?: any }` structure that frontend clients and SDK generators can reliably consume.
|
|
85
|
+
|
|
86
|
+
### Available Response Builders
|
|
87
|
+
|
|
88
|
+
| Builder | HTTP Status | Envelope / Output | Purpose |
|
|
89
|
+
| :--- | :--- | :--- | :--- |
|
|
90
|
+
| `success(data, message?, meta?)` | 200 OK | `{ success: true, data, message?, meta? }` | Standard successful payload |
|
|
91
|
+
| `created(data, message?, meta?)` | 201 Created | `{ success: true, data, message: 'Created', meta? }` | Created resource |
|
|
92
|
+
| `accepted(data?, message?, meta?)` | 202 Accepted | `{ success: true, data, message: 'Accepted', meta? }` | Queued async/background job |
|
|
93
|
+
| `noContent()` | 204 No Content | `{ success: true, data: null }` | Empty successful action |
|
|
94
|
+
| `error(msg, code?, details?, status?)` | 4xx / 5xx | `{ success: false, error: { message, code?, details?, status? } }` | Structured error envelope |
|
|
95
|
+
| `problem(title, status, detail?, opts?)` | Custom | RFC 7807 / RFC 9457 Problem Details object | Standard API problem specification |
|
|
96
|
+
| `paginate(items, total, options)` | 200 OK | `{ success: true, data: items, pagination: { ... } }` | Standard pagination envelope |
|
|
97
|
+
| `getPaginationSkip(query)` | N/A | `{ skip, limit, page }` | Database offset/limit calculator |
|
|
98
|
+
| `json(data, status?, headers?)` | Custom | `{ data, status, headers }` | Typed JSON payload descriptor |
|
|
99
|
+
| `redirect(url, status?)` | 3xx | `{ redirect, status }` | Redirection descriptor |
|
|
100
|
+
|
|
101
|
+
#### Functional Paradigm Example
|
|
86
102
|
|
|
87
103
|
```typescript filename="src/http/route.ts"
|
|
88
104
|
import { controller, route } from 'exisjs/router'
|
|
89
|
-
import { success, error } from 'exisjs/response'
|
|
105
|
+
import { success, created, error, HttpStatus } from 'exisjs/response'
|
|
90
106
|
|
|
91
107
|
export default controller({
|
|
92
108
|
getUser: route.get('/:id', {
|
|
@@ -94,7 +110,7 @@ export default controller({
|
|
|
94
110
|
const user = db.users.find(req.params.id)
|
|
95
111
|
|
|
96
112
|
if (!user) {
|
|
97
|
-
res.status(
|
|
113
|
+
res.status(HttpStatus.NOT_FOUND)
|
|
98
114
|
// Generates: { success: false, error: { message: "User not found", code: "NOT_FOUND" } }
|
|
99
115
|
return error("User not found", "NOT_FOUND")
|
|
100
116
|
}
|
|
@@ -102,15 +118,24 @@ export default controller({
|
|
|
102
118
|
// Generates: { success: true, data: { id: 1, name: "Alice" }, message: "Found user" }
|
|
103
119
|
return success(user, "Found user")
|
|
104
120
|
}
|
|
121
|
+
}),
|
|
122
|
+
|
|
123
|
+
createUser: route.post('/', {
|
|
124
|
+
handle(req, res) {
|
|
125
|
+
const user = db.users.create(req.body)
|
|
126
|
+
res.status(HttpStatus.CREATED)
|
|
127
|
+
// Generates: { success: true, data: user, message: "User registered" }
|
|
128
|
+
return created(user, "User registered")
|
|
129
|
+
}
|
|
105
130
|
})
|
|
106
131
|
})
|
|
107
132
|
```
|
|
108
133
|
|
|
109
|
-
#### Class-Based (OOP) Paradigm
|
|
134
|
+
#### Class-Based (OOP) Paradigm Example
|
|
110
135
|
|
|
111
136
|
```typescript filename="src/http/route.ts"
|
|
112
|
-
import { Controller, Get, Param, Res } from 'exisjs/decorators'
|
|
113
|
-
import { success, error } from 'exisjs/response'
|
|
137
|
+
import { Controller, Get, Post, Param, Body, Res } from 'exisjs/decorators'
|
|
138
|
+
import { success, created, error, HttpStatus } from 'exisjs/response'
|
|
114
139
|
|
|
115
140
|
@Controller()
|
|
116
141
|
export default class UsersController {
|
|
@@ -119,23 +144,49 @@ export default class UsersController {
|
|
|
119
144
|
const user = db.users.find(id)
|
|
120
145
|
|
|
121
146
|
if (!user) {
|
|
122
|
-
res.status(
|
|
123
|
-
// Generates: { success: false, error: { message: "User not found", code: "NOT_FOUND" } }
|
|
147
|
+
res.status(HttpStatus.NOT_FOUND)
|
|
124
148
|
return error("User not found", "NOT_FOUND")
|
|
125
149
|
}
|
|
126
150
|
|
|
127
|
-
// Generates: { success: true, data: { id: 1, name: "Alice" }, message: "Found user" }
|
|
128
151
|
return success(user, "Found user")
|
|
129
152
|
}
|
|
153
|
+
|
|
154
|
+
@Post('/')
|
|
155
|
+
async createUser(@Body() body: any, @Res() res: any) {
|
|
156
|
+
const user = db.users.create(body)
|
|
157
|
+
res.status(HttpStatus.CREATED)
|
|
158
|
+
return created(user, "User created successfully")
|
|
159
|
+
}
|
|
130
160
|
}
|
|
131
161
|
```
|
|
132
162
|
|
|
163
|
+
### RFC 7807 / RFC 9457 Problem Details (`problem`)
|
|
164
|
+
|
|
165
|
+
For enterprise and public REST APIs requiring standardized error models, `problem()` produces RFC-compliant Problem Details:
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
import { problem, HttpStatus } from 'exisjs/response'
|
|
169
|
+
|
|
170
|
+
res.status(HttpStatus.UNPROCESSABLE_ENTITY)
|
|
171
|
+
return problem('Invalid Request Parameters', 422, 'One or more fields failed schema validation', {
|
|
172
|
+
type: 'https://api.example.com/errors/validation',
|
|
173
|
+
instance: req.url,
|
|
174
|
+
invalidParams: [
|
|
175
|
+
{ name: 'email', reason: 'Must be a valid email address' },
|
|
176
|
+
{ name: 'age', reason: 'Must be greater than 18' }
|
|
177
|
+
]
|
|
178
|
+
})
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
|
|
133
182
|
---
|
|
134
183
|
|
|
135
184
|
## 3. Standardized Pagination Envelopes (`paginate`)
|
|
136
185
|
|
|
137
186
|
To eliminate pagination inconsistency across different endpoints, ExisJS exports standard `paginate()` and `getPaginationSkip()` utilities directly from `exisjs/validator` (as well as `exisjs/response`).
|
|
138
187
|
|
|
188
|
+
#### Functional Paradigm
|
|
189
|
+
|
|
139
190
|
```typescript filename="src/http/products/route.ts"
|
|
140
191
|
import { controller, route } from 'exisjs/router'
|
|
141
192
|
import { tex, paginate, getPaginationSkip } from 'exisjs/validator'
|
|
@@ -160,6 +211,30 @@ export default controller({
|
|
|
160
211
|
})
|
|
161
212
|
```
|
|
162
213
|
|
|
214
|
+
#### Class-Based (OOP) Paradigm
|
|
215
|
+
|
|
216
|
+
```typescript filename="src/http/products/route.ts"
|
|
217
|
+
import { Controller, Get, Query } from 'exisjs/decorators'
|
|
218
|
+
import { tex, paginate, getPaginationSkip } from 'exisjs/validator'
|
|
219
|
+
|
|
220
|
+
const ProductQuerySchema = tex.pagination({ defaultLimit: 15, maxLimit: 50 })
|
|
221
|
+
|
|
222
|
+
@Controller('/products')
|
|
223
|
+
export default class ProductsController {
|
|
224
|
+
@Get('/', { query: ProductQuerySchema })
|
|
225
|
+
async listProducts(@Query() query: any) {
|
|
226
|
+
const { skip, limit, page } = getPaginationSkip(query)
|
|
227
|
+
|
|
228
|
+
const [items, total] = await Promise.all([
|
|
229
|
+
db.product.findMany({ skip, take: limit, orderBy: { createdAt: 'desc' } }),
|
|
230
|
+
db.product.count(),
|
|
231
|
+
])
|
|
232
|
+
|
|
233
|
+
return paginate(items, total, { page, limit })
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
163
238
|
### The Standardized Pagination Envelope
|
|
164
239
|
|
|
165
240
|
Calling `paginate(items, total, { page, limit })` produces a uniform envelope:
|
|
@@ -298,22 +373,28 @@ export default class ReportController {
|
|
|
298
373
|
}
|
|
299
374
|
```
|
|
300
375
|
|
|
301
|
-
### `res.sendStream()`
|
|
302
|
-
|
|
303
|
-
- **
|
|
304
|
-
- **
|
|
376
|
+
### `res.sendStream()` & `res.stream()`
|
|
377
|
+
ExisJS provides first-class streaming support across `res.sendStream()` (aliased as `res.stream()`), accepting:
|
|
378
|
+
- **Node.js `Readable` streams** (e.g. `fs.createReadStream`, database cursors, zlib streams)
|
|
379
|
+
- **Web Standard `ReadableStream`** (e.g. from `fetch()`, OpenAI SDK, Anthropic SDK, and AI frameworks)
|
|
380
|
+
- **`AsyncIterable` & Async Generators** (e.g. `async function* generate() { yield ... }`)
|
|
305
381
|
|
|
306
|
-
####
|
|
382
|
+
#### Core Reliability Guarantees:
|
|
383
|
+
- **Socket Backpressure Control**: If the underlying network socket buffer fills up (`res.raw.write()` returns `false`), ExisJS automatically pauses stream consumption until the `'drain'` event fires, preventing fast producers from exhausting server RAM.
|
|
384
|
+
- **Client Disconnect Teardown**: If the client aborts or closes the connection prematurely (`req.raw.on('close')`), ExisJS cancels Web stream readers (`reader.cancel()`), destroys Node streams (`stream.destroy()`), or terminates async generators (`generator.return()`).
|
|
385
|
+
- **Resilient Error Trapping**: Stream transmission failures are trapped without crashing the Node.js process. If headers have not yet been sent, a structured 500 error envelope is returned.
|
|
307
386
|
|
|
308
|
-
|
|
387
|
+
#### 1. Streaming Node.js Readable Streams
|
|
388
|
+
|
|
389
|
+
##### Functional Paradigm
|
|
390
|
+
```typescript filename="src/http/video/route.ts"
|
|
309
391
|
import { controller, route } from 'exisjs/router'
|
|
310
392
|
import fs from 'node:fs'
|
|
311
393
|
|
|
312
394
|
export default controller({
|
|
313
|
-
streamVideo: route.get('/
|
|
395
|
+
streamVideo: route.get('/', {
|
|
314
396
|
handle(req, res) {
|
|
315
397
|
const videoStream = fs.createReadStream('/path/to/video.mp4')
|
|
316
|
-
|
|
317
398
|
res.type('video/mp4')
|
|
318
399
|
res.sendStream(videoStream)
|
|
319
400
|
}
|
|
@@ -321,36 +402,175 @@ export default controller({
|
|
|
321
402
|
})
|
|
322
403
|
```
|
|
323
404
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
```typescript
|
|
405
|
+
##### Class-Based (OOP) Paradigm
|
|
406
|
+
```typescript filename="src/http/video/route.ts"
|
|
327
407
|
import { Controller, Get, Res } from 'exisjs/decorators'
|
|
408
|
+
import type { Response } from 'exisjs/router'
|
|
328
409
|
import fs from 'node:fs'
|
|
329
410
|
|
|
330
|
-
@Controller()
|
|
411
|
+
@Controller('/video')
|
|
331
412
|
export default class VideoController {
|
|
332
|
-
@Get('/
|
|
333
|
-
|
|
413
|
+
@Get('/')
|
|
414
|
+
streamVideo(@Res() res: Response) {
|
|
334
415
|
const videoStream = fs.createReadStream('/path/to/video.mp4')
|
|
335
|
-
|
|
336
416
|
res.type('video/mp4')
|
|
337
417
|
res.sendStream(videoStream)
|
|
338
418
|
}
|
|
339
419
|
}
|
|
340
420
|
```
|
|
341
421
|
|
|
422
|
+
#### 2. Streaming Web Standard `ReadableStream` (AI SDKs & Fetch)
|
|
423
|
+
|
|
424
|
+
##### Functional Paradigm
|
|
425
|
+
```typescript filename="src/http/ai/route.ts"
|
|
426
|
+
import { controller, route } from 'exisjs/router'
|
|
427
|
+
|
|
428
|
+
export default controller({
|
|
429
|
+
chat: route.post('/', {
|
|
430
|
+
async handle(req, res) {
|
|
431
|
+
const response = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
432
|
+
method: 'POST',
|
|
433
|
+
headers: { 'Authorization': `Bearer ${process.env.OPENAI_API_KEY}` },
|
|
434
|
+
body: JSON.stringify({ model: 'gpt-4o', stream: true, messages: req.body.messages }),
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
res.type('text/event-stream')
|
|
438
|
+
res.stream(response.body!) // Web ReadableStream piped with backpressure
|
|
439
|
+
}
|
|
440
|
+
})
|
|
441
|
+
})
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
##### Class-Based (OOP) Paradigm
|
|
445
|
+
```typescript filename="src/http/ai/route.ts"
|
|
446
|
+
import { Controller, Post, Body, Res } from 'exisjs/decorators'
|
|
447
|
+
import type { Response } from 'exisjs/router'
|
|
448
|
+
|
|
449
|
+
@Controller('/ai')
|
|
450
|
+
export default class AiController {
|
|
451
|
+
@Post('/chat')
|
|
452
|
+
async chat(@Body() body: any, @Res() res: Response) {
|
|
453
|
+
const response = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
454
|
+
method: 'POST',
|
|
455
|
+
headers: { 'Authorization': `Bearer ${process.env.OPENAI_API_KEY}` },
|
|
456
|
+
body: JSON.stringify({ model: 'gpt-4o', stream: true, messages: body.messages }),
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
res.type('text/event-stream')
|
|
460
|
+
res.stream(response.body!)
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
#### 3. Streaming Async Generators
|
|
466
|
+
|
|
467
|
+
##### Functional Paradigm
|
|
468
|
+
```typescript filename="src/http/feed/route.ts"
|
|
469
|
+
import { controller, route } from 'exisjs/router'
|
|
470
|
+
|
|
471
|
+
export default controller({
|
|
472
|
+
feed: route.get('/', {
|
|
473
|
+
handle(req, res) {
|
|
474
|
+
async function* generateData() {
|
|
475
|
+
for (let i = 0; i < 100; i++) {
|
|
476
|
+
yield `data: item ${i}\n\n`
|
|
477
|
+
await new Promise((r) => setTimeout(r, 50))
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
res.type('text/event-stream')
|
|
482
|
+
res.stream(generateData())
|
|
483
|
+
}
|
|
484
|
+
})
|
|
485
|
+
})
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
##### Class-Based (OOP) Paradigm
|
|
489
|
+
```typescript filename="src/http/feed/route.ts"
|
|
490
|
+
import { Controller, Get, Res } from 'exisjs/decorators'
|
|
491
|
+
import type { Response } from 'exisjs/router'
|
|
492
|
+
|
|
493
|
+
@Controller('/feed')
|
|
494
|
+
export default class FeedController {
|
|
495
|
+
@Get('/')
|
|
496
|
+
streamFeed(@Res() res: Response) {
|
|
497
|
+
async function* generateData() {
|
|
498
|
+
for (let i = 0; i < 100; i++) {
|
|
499
|
+
yield `data: item ${i}\n\n`
|
|
500
|
+
await new Promise((r) => setTimeout(r, 50))
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
res.type('text/event-stream')
|
|
505
|
+
res.stream(generateData())
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
#### 4. Automatic Stream Piping from Handler Return Values
|
|
511
|
+
Route handlers returning a stream or async generator directly are automatically detected by the ExisJS pipeline and piped via `res.sendStream()`:
|
|
512
|
+
|
|
513
|
+
##### Functional Paradigm
|
|
514
|
+
```typescript filename="src/http/export/route.ts"
|
|
515
|
+
import { controller, route } from 'exisjs/router'
|
|
516
|
+
import fs from 'node:fs'
|
|
517
|
+
|
|
518
|
+
export default controller({
|
|
519
|
+
exportCsv: route.get('/', {
|
|
520
|
+
handle() {
|
|
521
|
+
// Simply return the readable stream; ExisJS auto-pipes it to res!
|
|
522
|
+
return fs.createReadStream('./data/export.csv')
|
|
523
|
+
}
|
|
524
|
+
})
|
|
525
|
+
})
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
##### Class-Based (OOP) Paradigm
|
|
529
|
+
```typescript filename="src/http/export/route.ts"
|
|
530
|
+
import { Controller, Get } from 'exisjs/decorators'
|
|
531
|
+
import fs from 'node:fs'
|
|
532
|
+
|
|
533
|
+
@Controller('/export')
|
|
534
|
+
export default class ExportController {
|
|
535
|
+
@Get('/')
|
|
536
|
+
exportCsv() {
|
|
537
|
+
// Simply return the readable stream; ExisJS auto-pipes it to res!
|
|
538
|
+
return fs.createReadStream('./data/export.csv')
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
```
|
|
542
|
+
|
|
342
543
|
---
|
|
343
544
|
|
|
344
545
|
## 5. Redirects
|
|
345
546
|
|
|
346
547
|
You can instantly redirect users to another URL using `res.redirect()`. It defaults to a `302 Found` temporary redirect, but you can override the status code.
|
|
347
548
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
549
|
+
#### Functional Paradigm
|
|
550
|
+
```typescript filename="src/http/auth/route.ts"
|
|
551
|
+
import { controller, route } from 'exisjs/router'
|
|
552
|
+
|
|
553
|
+
export default controller({
|
|
554
|
+
login: route.get('/login', {
|
|
555
|
+
handle(req, res) {
|
|
556
|
+
res.redirect('https://auth.example.com/login', 302)
|
|
557
|
+
}
|
|
558
|
+
})
|
|
559
|
+
})
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
#### Class-Based (OOP) Paradigm
|
|
563
|
+
```typescript filename="src/http/auth/route.ts"
|
|
564
|
+
import { Controller, Get, Res } from 'exisjs/decorators'
|
|
565
|
+
import type { Response } from 'exisjs/router'
|
|
351
566
|
|
|
352
|
-
|
|
353
|
-
|
|
567
|
+
@Controller('/auth')
|
|
568
|
+
export default class AuthController {
|
|
569
|
+
@Get('/login')
|
|
570
|
+
login(@Res() res: Response) {
|
|
571
|
+
res.redirect('https://auth.example.com/login', 302)
|
|
572
|
+
}
|
|
573
|
+
}
|
|
354
574
|
```
|
|
355
575
|
|
|
356
576
|
---
|
|
@@ -428,7 +648,9 @@ export default class EventsController {
|
|
|
428
648
|
|
|
429
649
|
### AI / LLM Streaming with `sse.pipeFrom()`
|
|
430
650
|
|
|
431
|
-
You can directly stream chunks from any `AsyncIterable` (such as the OpenAI SDK or custom generators) using `sse.pipeFrom()`:
|
|
651
|
+
You can directly stream chunks from any `AsyncIterable` (such as the OpenAI SDK, Anthropic SDK, or custom async generators) using `sse.pipeFrom()`:
|
|
652
|
+
|
|
653
|
+
#### Functional Paradigm
|
|
432
654
|
|
|
433
655
|
```typescript filename="src/http/ai/route.ts"
|
|
434
656
|
import { controller, route } from 'exisjs/router'
|
|
@@ -457,6 +679,35 @@ export default controller({
|
|
|
457
679
|
})
|
|
458
680
|
```
|
|
459
681
|
|
|
682
|
+
#### Class-Based (OOP) Paradigm
|
|
683
|
+
|
|
684
|
+
```typescript filename="src/http/ai/route.ts"
|
|
685
|
+
import { Controller, Post, Res } from 'exisjs/decorators'
|
|
686
|
+
import type { Response } from 'exisjs/router'
|
|
687
|
+
|
|
688
|
+
@Controller('/ai')
|
|
689
|
+
export default class AiController {
|
|
690
|
+
@Post('/chat')
|
|
691
|
+
chat(@Res() res: Response) {
|
|
692
|
+
res.sse(async (sse) => {
|
|
693
|
+
async function* generateTokens() {
|
|
694
|
+
yield 'Hello'
|
|
695
|
+
yield ' from'
|
|
696
|
+
yield ' ExisJS'
|
|
697
|
+
yield ' AI streaming!'
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
await sse.pipeFrom(generateTokens(), (token) => ({
|
|
701
|
+
event: 'delta',
|
|
702
|
+
data: { text: token }
|
|
703
|
+
}))
|
|
704
|
+
|
|
705
|
+
sse.close()
|
|
706
|
+
})
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
```
|
|
710
|
+
|
|
460
711
|
### SSE Options & Methods
|
|
461
712
|
|
|
462
713
|
- `res.sse(options?)`: Options include:
|