exisjs 0.7.4 → 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/build.d.ts +4 -2
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +80 -22
- package/dist/cli/commands/build.js.map +1 -1
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +229 -64
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/doctor.d.ts.map +1 -1
- package/dist/cli/commands/doctor.js +5 -2
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/generate.d.ts +1 -0
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +25 -0
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/commands/start.d.ts.map +1 -1
- package/dist/cli/commands/start.js +65 -15
- package/dist/cli/commands/start.js.map +1 -1
- package/dist/cli/index.js +20 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/manifest.d.ts.map +1 -1
- package/dist/cli/manifest.js +118 -17
- 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/env.d.ts.map +1 -1
- package/dist/config/env.js +8 -0
- package/dist/config/env.js.map +1 -1
- package/dist/config/types.d.ts +2 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/decorators/constants.d.ts +2 -0
- package/dist/decorators/constants.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/boundaries.mdx +33 -3
- package/dist/docs/content/cli/commands.mdx +1 -1
- package/dist/docs/content/config.mdx +26 -0
- package/dist/docs/content/controllers.mdx +127 -10
- package/dist/docs/content/decorators.mdx +22 -4
- package/dist/docs/content/errors.mdx +38 -2
- package/dist/docs/content/introduction.mdx +7 -7
- package/dist/docs/content/middleware.mdx +269 -6
- package/dist/docs/content/requests.mdx +217 -10
- package/dist/docs/content/responses.mdx +323 -34
- package/dist/docs/content/routing.mdx +46 -0
- package/dist/docs/content/security.mdx +17 -4
- package/dist/docs/content/structure.mdx +91 -12
- package/dist/docs/content/validation.mdx +226 -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 +49 -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/rate-limit.d.ts.map +1 -1
- package/dist/middleware/rate-limit.js +9 -0
- package/dist/middleware/rate-limit.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 +25 -5
- 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 +1 -0
- package/dist/router/route-scanner.d.ts.map +1 -1
- package/dist/router/route-scanner.js +217 -10
- 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 +11 -1
- package/dist/router/types.d.ts.map +1 -1
- package/dist/sanitize/index.d.ts +6 -6
- package/dist/sanitize/index.d.ts.map +1 -1
- package/dist/sanitize/index.js +8 -8
- package/dist/sanitize/index.js.map +1 -1
- package/dist/server/app.d.ts +2 -0
- package/dist/server/app.d.ts.map +1 -1
- package/dist/server/app.js +14 -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/engines/node-engine.d.ts.map +1 -1
- package/dist/server/engines/node-engine.js +1 -0
- package/dist/server/engines/node-engine.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 +42 -18
- package/dist/server/request-handler.js.map +1 -1
- package/dist/server/request.d.ts +6 -0
- package/dist/server/request.d.ts.map +1 -1
- package/dist/server/request.js +112 -33
- package/dist/server/request.js.map +1 -1
- package/dist/server/response.d.ts +13 -2
- package/dist/server/response.d.ts.map +1 -1
- package/dist/server/response.js +250 -10
- 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 +38 -22
- package/dist/validator/tex.d.ts.map +1 -1
- package/dist/validator/tex.js +490 -40
- package/dist/validator/tex.js.map +1 -1
- package/package.json +10 -10
- 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
|
@@ -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:
|
|
@@ -210,10 +285,12 @@ res.status(404)
|
|
|
210
285
|
|
|
211
286
|
## 3. Cookies
|
|
212
287
|
|
|
213
|
-
ExisJS provides native helper methods for setting and clearing cookies safely.
|
|
288
|
+
ExisJS provides native helper methods for setting and clearing cookies safely, supporting modern web standards including CHIPS (Partitioned Cookies) and Priority hints.
|
|
289
|
+
|
|
290
|
+
### Setting Cookies
|
|
214
291
|
|
|
215
292
|
```typescript
|
|
216
|
-
// Set a secure, HTTP-only cookie
|
|
293
|
+
// Set a secure, HTTP-only session cookie
|
|
217
294
|
res.cookie('session_id', '123456', {
|
|
218
295
|
maxAge: 86400, // 1 day in seconds
|
|
219
296
|
httpOnly: true,
|
|
@@ -221,15 +298,49 @@ res.cookie('session_id', '123456', {
|
|
|
221
298
|
sameSite: 'Strict'
|
|
222
299
|
})
|
|
223
300
|
|
|
224
|
-
//
|
|
301
|
+
// Cross-site embedded iframe cookie with CHIPS (Partitioned)
|
|
302
|
+
res.cookie('widget_pref', 'dark_mode', {
|
|
303
|
+
sameSite: 'None', // Automatically forces `secure: true` per modern browser specs
|
|
304
|
+
partitioned: true, // Cookies Having Independent Partitioned State (CHIPS)
|
|
305
|
+
priority: 'high', // Hints browser cookie retention under storage pressure ('low' | 'medium' | 'high')
|
|
306
|
+
path: '/'
|
|
307
|
+
})
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
#### Cookie Options Reference
|
|
311
|
+
|
|
312
|
+
| Option | Type | Default | Description |
|
|
313
|
+
| :--- | :--- | :--- | :--- |
|
|
314
|
+
| `maxAge` | `number` | `undefined` | Lifetime of the cookie in seconds (sets both `Max-Age` and computed `Expires`). |
|
|
315
|
+
| `expires` | `Date` | `undefined` | Absolute expiration date for the cookie. |
|
|
316
|
+
| `httpOnly` | `boolean` | `false` | Mitigates XSS by preventing client-side JavaScript access. |
|
|
317
|
+
| `secure` | `boolean` | `false` | Requires HTTPS. *Automatically set to `true` if `sameSite: 'None'` is provided.* |
|
|
318
|
+
| `sameSite` | `'Strict' \| 'Lax' \| 'None' \| boolean` | `undefined` | Controls cross-site request cookie inclusion. |
|
|
319
|
+
| `partitioned` | `boolean` | `false` | Enables CHIPS for third-party contexts across top-level site partitions. |
|
|
320
|
+
| `priority` | `'low' \| 'medium' \| 'high'` | `undefined` | RFC 6265bis storage priority hint when client cookie capacity is exhausted. |
|
|
321
|
+
| `path` | `string` | `'/'` | Path where the cookie is valid. |
|
|
322
|
+
| `domain` | `string` | `undefined` | Domain scope for the cookie. |
|
|
323
|
+
|
|
324
|
+
### Clearing Cookies
|
|
325
|
+
|
|
326
|
+
When clearing cookies, browsers require the matching `path` and `domain` used when the cookie was originally set:
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
// Clear default root cookie
|
|
225
330
|
res.clearCookie('session_id')
|
|
331
|
+
|
|
332
|
+
// Clear scoped cookie matching original domain & path
|
|
333
|
+
res.clearCookie('tenant_token', {
|
|
334
|
+
path: '/api',
|
|
335
|
+
domain: '.example.com'
|
|
336
|
+
})
|
|
226
337
|
```
|
|
227
338
|
|
|
228
339
|
---
|
|
229
340
|
|
|
230
341
|
## 4. File Downloads & Streaming
|
|
231
342
|
|
|
232
|
-
ExisJS makes streaming files simple, bypassing V8 memory bottlenecks by piping native Node streams directly to the socket.
|
|
343
|
+
ExisJS makes streaming files simple, bypassing V8 memory bottlenecks by piping native Node streams directly to the socket with automated resource cleanup.
|
|
233
344
|
|
|
234
345
|
### `res.download()`
|
|
235
346
|
Automatically reads a file from disk, sets the `Content-Disposition` header to force a download in the browser, and infers the correct MIME type based on the file extension.
|
|
@@ -262,20 +373,28 @@ export default class ReportController {
|
|
|
262
373
|
}
|
|
263
374
|
```
|
|
264
375
|
|
|
265
|
-
### `res.sendStream()`
|
|
266
|
-
|
|
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 ... }`)
|
|
267
381
|
|
|
268
|
-
####
|
|
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.
|
|
269
386
|
|
|
270
|
-
|
|
387
|
+
#### 1. Streaming Node.js Readable Streams
|
|
388
|
+
|
|
389
|
+
##### Functional Paradigm
|
|
390
|
+
```typescript filename="src/http/video/route.ts"
|
|
271
391
|
import { controller, route } from 'exisjs/router'
|
|
272
392
|
import fs from 'node:fs'
|
|
273
393
|
|
|
274
394
|
export default controller({
|
|
275
|
-
streamVideo: route.get('/
|
|
395
|
+
streamVideo: route.get('/', {
|
|
276
396
|
handle(req, res) {
|
|
277
397
|
const videoStream = fs.createReadStream('/path/to/video.mp4')
|
|
278
|
-
|
|
279
398
|
res.type('video/mp4')
|
|
280
399
|
res.sendStream(videoStream)
|
|
281
400
|
}
|
|
@@ -283,36 +402,175 @@ export default controller({
|
|
|
283
402
|
})
|
|
284
403
|
```
|
|
285
404
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
```typescript
|
|
405
|
+
##### Class-Based (OOP) Paradigm
|
|
406
|
+
```typescript filename="src/http/video/route.ts"
|
|
289
407
|
import { Controller, Get, Res } from 'exisjs/decorators'
|
|
408
|
+
import type { Response } from 'exisjs/router'
|
|
290
409
|
import fs from 'node:fs'
|
|
291
410
|
|
|
292
|
-
@Controller()
|
|
411
|
+
@Controller('/video')
|
|
293
412
|
export default class VideoController {
|
|
294
|
-
@Get('/
|
|
295
|
-
|
|
413
|
+
@Get('/')
|
|
414
|
+
streamVideo(@Res() res: Response) {
|
|
296
415
|
const videoStream = fs.createReadStream('/path/to/video.mp4')
|
|
297
|
-
|
|
298
416
|
res.type('video/mp4')
|
|
299
417
|
res.sendStream(videoStream)
|
|
300
418
|
}
|
|
301
419
|
}
|
|
302
420
|
```
|
|
303
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
|
+
|
|
304
543
|
---
|
|
305
544
|
|
|
306
545
|
## 5. Redirects
|
|
307
546
|
|
|
308
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.
|
|
309
548
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
549
|
+
#### Functional Paradigm
|
|
550
|
+
```typescript filename="src/http/auth/route.ts"
|
|
551
|
+
import { controller, route } from 'exisjs/router'
|
|
313
552
|
|
|
314
|
-
|
|
315
|
-
|
|
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'
|
|
566
|
+
|
|
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
|
+
}
|
|
316
574
|
```
|
|
317
575
|
|
|
318
576
|
---
|
|
@@ -390,7 +648,9 @@ export default class EventsController {
|
|
|
390
648
|
|
|
391
649
|
### AI / LLM Streaming with `sse.pipeFrom()`
|
|
392
650
|
|
|
393
|
-
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
|
|
394
654
|
|
|
395
655
|
```typescript filename="src/http/ai/route.ts"
|
|
396
656
|
import { controller, route } from 'exisjs/router'
|
|
@@ -419,6 +679,35 @@ export default controller({
|
|
|
419
679
|
})
|
|
420
680
|
```
|
|
421
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
|
+
|
|
422
711
|
### SSE Options & Methods
|
|
423
712
|
|
|
424
713
|
- `res.sse(options?)`: Options include:
|
|
@@ -653,6 +653,51 @@ export default class OrdersController {
|
|
|
653
653
|
|
|
654
654
|
|
|
655
655
|
|
|
656
|
+
## Type-Safe Client Route Manifest Contract (`AppRouter`)
|
|
657
|
+
|
|
658
|
+
ExisJS generates a compile-time route manifest in `.exis/types.d.ts` (and exposed via root `exis.d.ts`), converting your filesystem routes into an `AppRouter` type definition.
|
|
659
|
+
|
|
660
|
+
### Zero-Cost Return Type & DTO Preservation
|
|
661
|
+
When you return data from `handle()`, ExisJS preserves the exact TypeScript return type (including Drizzle/Prisma models, Tex schemas, or custom records) on the route definition:
|
|
662
|
+
|
|
663
|
+
```typescript filename="src/http/users/route.ts"
|
|
664
|
+
import { controller, route } from 'exisjs/router'
|
|
665
|
+
import { tex } from 'exisjs/validator'
|
|
666
|
+
|
|
667
|
+
export default controller({
|
|
668
|
+
getUser: route.get('/:id', {
|
|
669
|
+
params: tex.object({ id: tex.string() }),
|
|
670
|
+
async handle({ params }) {
|
|
671
|
+
// Inferred return type is strictly preserved as { id: string, name: string, active: boolean }
|
|
672
|
+
return { id: params.id, name: 'Alice', active: true }
|
|
673
|
+
}
|
|
674
|
+
})
|
|
675
|
+
})
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
### Consuming `AppRouter` with `@exisjs/fetch`
|
|
679
|
+
You can import `AppRouter` in your frontend or external client applications for zero-cost end-to-end type safety and autocomplete:
|
|
680
|
+
|
|
681
|
+
```typescript filename="src/client/api.ts"
|
|
682
|
+
import { createClient } from '@exisjs/fetch'
|
|
683
|
+
import type { AppRouter } from '../server/.exis/types'
|
|
684
|
+
|
|
685
|
+
const api = createClient<AppRouter>({
|
|
686
|
+
baseUrl: 'http://localhost:4000',
|
|
687
|
+
})
|
|
688
|
+
|
|
689
|
+
// 1. Hierarchical dot-notation RPC call:
|
|
690
|
+
const user = await api.users('123').getUser.get()
|
|
691
|
+
|
|
692
|
+
// 2. Direct path index call:
|
|
693
|
+
const userByPath = await api['/users/:id'].getUser.get({ params: { id: '123' } })
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
### Dynamic Dev Watcher Sync
|
|
697
|
+
During `exis dev`, adding, renaming, moving, or deleting route files automatically updates `.exis/types.d.ts` and `.exis/types/index.d.ts` in real-time without requiring a dev server restart or manual `.exis` folder deletion.
|
|
698
|
+
|
|
699
|
+
---
|
|
700
|
+
|
|
656
701
|
## Modular Architecture
|
|
657
702
|
|
|
658
703
|
While putting all logic in `route.ts` is fine for small apps, ExisJS strongly encourages separating concerns alongside your route definition:
|
|
@@ -661,3 +706,4 @@ While putting all logic in `route.ts` is fine for small apps, ExisJS strongly en
|
|
|
661
706
|
- **`service.ts`**: Pure functions that interact with your database.
|
|
662
707
|
- **`controller.ts`**: Handles business orchestration.
|
|
663
708
|
- **`route.ts`**: Wires it to the HTTP network.
|
|
709
|
+
|
|
@@ -41,7 +41,7 @@ export default exis({
|
|
|
41
41
|
|
|
42
42
|
## 2. In-Memory Rate Limiting
|
|
43
43
|
|
|
44
|
-
The core `rateLimit` middleware provides high-performance in-memory and off-heap native rate limiting:
|
|
44
|
+
The core `rateLimit` middleware provides high-performance in-memory and off-heap native rate limiting with standard HTTP response headers:
|
|
45
45
|
|
|
46
46
|
```typescript filename="src/http/boundary.ts"
|
|
47
47
|
import { defineBoundary } from 'exisjs/router'
|
|
@@ -51,12 +51,25 @@ export const config = defineBoundary({})
|
|
|
51
51
|
|
|
52
52
|
// Apply rate limiting chain step across the boundary
|
|
53
53
|
export const limiter = rateLimit({
|
|
54
|
-
windowMs: 60 * 1000, // 1 minute
|
|
55
|
-
max: 100,
|
|
56
|
-
message: 'Too many requests from this IP, please try again after a minute'
|
|
54
|
+
windowMs: 60 * 1000, // 1 minute window
|
|
55
|
+
max: 100, // 100 requests per window
|
|
56
|
+
message: 'Too many requests from this IP, please try again after a minute',
|
|
57
|
+
keyGenerator: (req) => req.ip // Uses normalized IPv4/IPv6 client IP
|
|
57
58
|
})
|
|
58
59
|
```
|
|
59
60
|
|
|
61
|
+
### Standard Rate Limit Headers
|
|
62
|
+
|
|
63
|
+
`rateLimit` automatically emits standard IETF draft and legacy rate-limiting headers on every response:
|
|
64
|
+
|
|
65
|
+
| Header | Description | Example |
|
|
66
|
+
| :--- | :--- | :--- |
|
|
67
|
+
| `RateLimit-Limit` | Maximum request quota for the configured window. | `100` |
|
|
68
|
+
| `RateLimit-Remaining` | Remaining requests permitted within the current window. | `42` |
|
|
69
|
+
| `RateLimit-Reset` | Time remaining in seconds until the window quota resets. | `18` |
|
|
70
|
+
| `Retry-After` | Included on `429 Too Many Requests` (RFC 6585) indicating seconds to wait. | `18` |
|
|
71
|
+
| `X-RateLimit-*` | Legacy header equivalents for backwards compatibility. | `X-RateLimit-Reset: 18` |
|
|
72
|
+
|
|
60
73
|
> [!NOTE]
|
|
61
74
|
> The core `rateLimit` middleware runs using an in-memory native off-heap store. For distributed, multi-instance rate limiting backed by Redis, use `@exisjs/cache`.
|
|
62
75
|
|