exisjs 0.7.12 → 0.7.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +195 -39
- package/dist/config/types.d.ts +2 -2
- package/dist/config/types.d.ts.map +1 -1
- package/dist/decorators/params.d.ts +21 -0
- package/dist/decorators/params.d.ts.map +1 -1
- package/dist/decorators/params.js +21 -1
- package/dist/decorators/params.js.map +1 -1
- package/dist/di/container.d.ts +15 -6
- package/dist/di/container.d.ts.map +1 -1
- package/dist/di/container.js +26 -3
- package/dist/di/container.js.map +1 -1
- package/dist/di/index.d.ts +1 -1
- package/dist/di/index.d.ts.map +1 -1
- package/dist/di/index.js.map +1 -1
- package/dist/di/inject.d.ts +2 -1
- package/dist/di/inject.d.ts.map +1 -1
- package/dist/di/inject.js +2 -1
- package/dist/di/inject.js.map +1 -1
- package/dist/docs/BENCHMARKS.md +4 -4
- package/dist/docs/content/README.md +43 -44
- package/dist/docs/content/boundaries.mdx +138 -61
- package/dist/docs/content/cli/commands.mdx +151 -128
- package/dist/docs/content/cli/overview.mdx +61 -47
- package/dist/docs/content/cli/usage.mdx +107 -30
- package/dist/docs/content/config.mdx +145 -158
- package/dist/docs/content/controllers.mdx +193 -464
- package/dist/docs/content/cron.mdx +183 -69
- package/dist/docs/content/decorators.mdx +449 -154
- package/dist/docs/content/errors.mdx +202 -305
- package/dist/docs/content/exisjs-mcp.mdx +59 -49
- package/dist/docs/content/installation.mdx +243 -157
- package/dist/docs/content/introduction.mdx +220 -48
- package/dist/docs/content/middleware.mdx +171 -497
- package/dist/docs/content/modules.mdx +141 -83
- package/dist/docs/content/providers.mdx +216 -180
- package/dist/docs/content/requests.mdx +168 -488
- package/dist/docs/content/responses.mdx +200 -533
- package/dist/docs/content/routing.mdx +235 -598
- package/dist/docs/content/security.mdx +233 -57
- package/dist/docs/content/structure.mdx +151 -240
- package/dist/docs/content/swagger.mdx +192 -91
- package/dist/docs/content/testing.mdx +205 -74
- package/dist/docs/content/validation.mdx +479 -341
- package/dist/docs/reference/README.md +34 -2
- package/dist/docs/reference/config.mdx +277 -75
- package/dist/docs/reference/context.mdx +197 -106
- package/dist/docs/reference/cron.mdx +266 -152
- package/dist/docs/reference/database.mdx +135 -53
- package/dist/docs/reference/decorators.mdx +234 -116
- package/dist/docs/reference/di.mdx +146 -112
- package/dist/docs/reference/error.mdx +155 -178
- package/dist/docs/reference/exis.mdx +141 -70
- package/dist/docs/reference/logger.mdx +110 -126
- package/dist/docs/reference/middleware.mdx +235 -158
- package/dist/docs/reference/plugin.mdx +136 -64
- package/dist/docs/reference/router.mdx +275 -293
- package/dist/docs/reference/sanitize.mdx +91 -87
- package/dist/docs/reference/swagger.mdx +280 -100
- package/dist/docs/reference/testing.mdx +223 -89
- package/dist/docs/reference/validator.mdx +194 -206
- package/dist/lib/start-server.js +3 -8
- package/dist/lib/start-server.js.map +1 -1
- package/dist/module/module.d.ts +6 -3
- package/dist/module/module.d.ts.map +1 -1
- package/dist/module/module.js +17 -3
- package/dist/module/module.js.map +1 -1
- package/dist/router/controller-registrar.d.ts.map +1 -1
- package/dist/router/controller-registrar.js +26 -4
- package/dist/router/controller-registrar.js.map +1 -1
- package/dist/server/app.d.ts +3 -1
- package/dist/server/app.d.ts.map +1 -1
- package/dist/server/app.js +24 -8
- package/dist/server/app.js.map +1 -1
- package/dist/server/request.d.ts +2 -0
- package/dist/server/request.d.ts.map +1 -1
- package/dist/server/request.js +11 -0
- package/dist/server/request.js.map +1 -1
- package/dist/swagger/generator.d.ts.map +1 -1
- package/dist/swagger/generator.js +130 -10
- package/dist/swagger/generator.js.map +1 -1
- package/dist/telemetry/index.d.ts +12 -0
- package/dist/telemetry/index.d.ts.map +1 -0
- package/dist/telemetry/index.js +121 -0
- package/dist/telemetry/index.js.map +1 -0
- package/dist/testing/client.d.ts +1 -0
- package/dist/testing/client.d.ts.map +1 -1
- package/dist/testing/client.js +4 -1
- package/dist/testing/client.js.map +1 -1
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +11 -4
- package/dist/testing/index.js.map +1 -1
- package/dist/validator/builder.d.ts +26 -1
- package/dist/validator/builder.d.ts.map +1 -1
- package/dist/validator/builder.js +31 -0
- package/dist/validator/builder.js.map +1 -1
- package/dist/validator/engine.d.ts +23 -0
- package/dist/validator/engine.d.ts.map +1 -1
- package/dist/validator/engine.js +228 -31
- package/dist/validator/engine.js.map +1 -1
- package/dist/validator/index.d.ts +8 -4
- package/dist/validator/index.d.ts.map +1 -1
- package/dist/validator/index.js +4 -1
- package/dist/validator/index.js.map +1 -1
- package/dist/validator/tex-types.d.ts +15 -0
- package/dist/validator/tex-types.d.ts.map +1 -1
- package/dist/validator/tex-types.js +102 -0
- package/dist/validator/tex-types.js.map +1 -1
- package/dist/validator/tex.d.ts +1 -1
- package/dist/validator/tex.d.ts.map +1 -1
- package/dist/validator/tex.js +3 -1
- package/dist/validator/tex.js.map +1 -1
- package/dist/validator/types.d.ts +1 -0
- package/dist/validator/types.d.ts.map +1 -1
- package/package.json +46 -14
- package/dist/docs/content/dataloader.mdx +0 -170
- package/dist/docs/reference/dataloader.mdx +0 -91
|
@@ -1,253 +1,170 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
title: 'Response Helpers & Streaming'
|
|
3
|
+
description: 'API reference for ExisResponse: automatic JSON serialization, response builders, problem details, cookies, file downloads, streaming, and Server-Sent Events.'
|
|
4
|
+
nextUrl: '/docs/boundaries'
|
|
5
|
+
nextTitle: 'Boundaries & Middleware Cascading'
|
|
4
6
|
---
|
|
5
7
|
|
|
6
|
-
#
|
|
8
|
+
# Response Helpers & Streaming (`ExisResponse`)
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
While ExisJS routes allow you to simply `return { data: ... }` (which automatically serializes to JSON), the `res` object gives you granular control over headers, cookies, streams, and file downloads.
|
|
10
|
+
ExisJS wraps standard Node.js `ServerResponse` objects in an `ExisResponse` instance (`res`). Handlers can return JavaScript objects or arrays directly for automatic JSON serialization, or use `res` methods for status codes, headers, cookie management, file downloads, streaming, and Server-Sent Events (SSE).
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
-
##
|
|
14
|
+
## Automatic JSON Serialization
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
When a route handler returns an object or array, ExisJS automatically serializes the data, sets `Content-Type: application/json; charset=utf-8`, and finishes the response:
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
### Functional Route
|
|
19
19
|
|
|
20
|
-
```typescript filename="src/http/route.ts"
|
|
20
|
+
```typescript filename="src/http/users/route.ts"
|
|
21
21
|
import { controller, route } from 'exisjs/router'
|
|
22
22
|
|
|
23
23
|
export default controller({
|
|
24
|
-
|
|
25
|
-
handle(
|
|
26
|
-
//
|
|
27
|
-
return [
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
#### Class-Based (OOP) Paradigm
|
|
34
|
-
|
|
35
|
-
```typescript filename="src/http/route.ts"
|
|
36
|
-
import { Controller, Get } from 'exisjs/decorators'
|
|
37
|
-
|
|
38
|
-
@Controller()
|
|
39
|
-
export default class UsersController {
|
|
40
|
-
@Get('/')
|
|
41
|
-
async getUsers() {
|
|
42
|
-
// ExisJS automatically handles JSON serialization
|
|
43
|
-
return [{ id: 1, name: "Alice" }]
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
However, you can completely bypass this and use the `res` object manually:
|
|
49
|
-
|
|
50
|
-
#### Functional Paradigm
|
|
51
|
-
|
|
52
|
-
```typescript filename="src/http/route.ts"
|
|
53
|
-
export default controller({
|
|
54
|
-
getUsers: route.get('/', {
|
|
55
|
-
handle(req, res) {
|
|
56
|
-
// Chainable response API
|
|
57
|
-
res.status(201).json([{ id: 1, name: "Alice" }])
|
|
58
|
-
}
|
|
59
|
-
})
|
|
24
|
+
listUsers: route.get('/', {
|
|
25
|
+
handle: async () => {
|
|
26
|
+
// Automatically serialized as JSON with 200 OK
|
|
27
|
+
return [
|
|
28
|
+
{ id: 1, name: 'Alice' },
|
|
29
|
+
{ id: 2, name: 'Bob' },
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
}),
|
|
60
33
|
})
|
|
61
34
|
```
|
|
62
35
|
|
|
63
|
-
|
|
36
|
+
### Class-Based (OOP) Controller
|
|
64
37
|
|
|
65
|
-
```typescript filename="src/http/route.ts"
|
|
66
|
-
import { Controller, Get,
|
|
38
|
+
```typescript filename="src/http/users/route.ts"
|
|
39
|
+
import { Controller, Get, ApiOperation } from 'exisjs/decorators'
|
|
67
40
|
|
|
68
41
|
@Controller()
|
|
69
42
|
export default class UsersController {
|
|
70
43
|
@Get('/')
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
44
|
+
@ApiOperation({ summary: 'List users' })
|
|
45
|
+
async listUsers() {
|
|
46
|
+
return [
|
|
47
|
+
{ id: 1, name: 'Alice' },
|
|
48
|
+
{ id: 2, name: 'Bob' },
|
|
49
|
+
]
|
|
74
50
|
}
|
|
75
51
|
}
|
|
76
52
|
```
|
|
77
53
|
|
|
78
54
|
---
|
|
79
55
|
|
|
80
|
-
##
|
|
56
|
+
## Response Envelope Builders (`exisjs/response`)
|
|
81
57
|
|
|
82
|
-
|
|
58
|
+
ExisJS exports standardized response builders from `exisjs/response` to provide uniform `{ success: boolean, data?: any, error?: any, meta?: any }` structures across all endpoints:
|
|
83
59
|
|
|
84
|
-
|
|
60
|
+
| Builder | HTTP Status | Response Structure | Description |
|
|
61
|
+
| :--- | :--- | :--- | :--- |
|
|
62
|
+
| `success(data, message?, meta?)` | `200 OK` | `{ success: true, data, message?, meta? }` | Standard successful response |
|
|
63
|
+
| `created(data, message?, meta?)` | `201 Created` | `{ success: true, data, message: 'Created', meta? }` | Newly created entity |
|
|
64
|
+
| `accepted(data?, message?, meta?)` | `202 Accepted` | `{ success: true, data, message: 'Accepted', meta? }` | Asynchronous or queued action |
|
|
65
|
+
| `noContent()` | `204 No Content` | `{ success: true, data: null }` | Action succeeded with no payload |
|
|
66
|
+
| `error(message, code?, details?, status?)` | `4xx / 5xx` | `{ success: false, error: { message, code?, details? } }` | Structured error envelope |
|
|
67
|
+
| `problem(title, status, detail?, opts?)` | Custom | RFC 7807 / RFC 9457 Problem Details object | Standardized HTTP Problem Details |
|
|
68
|
+
| `paginate(items, total, options)` | `200 OK` | `{ success: true, data, pagination: { ... } }` | Paginated response envelope |
|
|
85
69
|
|
|
86
|
-
###
|
|
70
|
+
### Example Usage
|
|
87
71
|
|
|
88
|
-
|
|
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
|
|
102
|
-
|
|
103
|
-
```typescript filename="src/http/route.ts"
|
|
72
|
+
```typescript filename="src/http/users/route.ts"
|
|
104
73
|
import { controller, route } from 'exisjs/router'
|
|
105
74
|
import { success, created, error, HttpStatus } from 'exisjs/response'
|
|
106
75
|
|
|
107
76
|
export default controller({
|
|
108
77
|
getUser: route.get('/:id', {
|
|
109
|
-
handle(
|
|
110
|
-
const user = db.users.find(
|
|
111
|
-
|
|
78
|
+
handle: async ({ params, res }) => {
|
|
79
|
+
const user = await db.users.find(params.id)
|
|
112
80
|
if (!user) {
|
|
113
81
|
res.status(HttpStatus.NOT_FOUND)
|
|
114
|
-
|
|
115
|
-
return error("User not found", "NOT_FOUND")
|
|
82
|
+
return error('User not found', 'NOT_FOUND')
|
|
116
83
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
return success(user, "Found user")
|
|
120
|
-
}
|
|
84
|
+
return success(user, 'User retrieved')
|
|
85
|
+
},
|
|
121
86
|
}),
|
|
122
87
|
|
|
123
88
|
createUser: route.post('/', {
|
|
124
|
-
handle(
|
|
125
|
-
const user = db.users.create(
|
|
89
|
+
handle: async ({ body, res }) => {
|
|
90
|
+
const user = await db.users.create(body)
|
|
126
91
|
res.status(HttpStatus.CREATED)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
})
|
|
92
|
+
return created(user, 'User registered')
|
|
93
|
+
},
|
|
94
|
+
}),
|
|
131
95
|
})
|
|
132
96
|
```
|
|
133
97
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
```typescript filename="src/http/route.ts"
|
|
137
|
-
import { Controller, Get, Post, Param, Body, Res } from 'exisjs/decorators'
|
|
138
|
-
import { success, created, error, HttpStatus } from 'exisjs/response'
|
|
139
|
-
|
|
140
|
-
@Controller()
|
|
141
|
-
export default class UsersController {
|
|
142
|
-
@Get('/:id')
|
|
143
|
-
async getUser(@Param('id') id: string, @Res() res: any) {
|
|
144
|
-
const user = db.users.find(id)
|
|
145
|
-
|
|
146
|
-
if (!user) {
|
|
147
|
-
res.status(HttpStatus.NOT_FOUND)
|
|
148
|
-
return error("User not found", "NOT_FOUND")
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return success(user, "Found user")
|
|
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
|
-
}
|
|
160
|
-
}
|
|
161
|
-
```
|
|
98
|
+
---
|
|
162
99
|
|
|
163
|
-
|
|
100
|
+
## RFC 7807 / RFC 9457 Problem Details
|
|
164
101
|
|
|
165
|
-
For
|
|
102
|
+
For public or enterprise REST APIs, `problem()` generates standard Problem Details objects:
|
|
166
103
|
|
|
167
|
-
```typescript
|
|
104
|
+
```typescript filename="src/http/checkout/route.ts"
|
|
105
|
+
import { controller, route } from 'exisjs/router'
|
|
168
106
|
import { problem, HttpStatus } from 'exisjs/response'
|
|
169
107
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
108
|
+
export default controller({
|
|
109
|
+
checkout: route.post('/', {
|
|
110
|
+
handle: async ({ req, res }) => {
|
|
111
|
+
res.status(HttpStatus.UNPROCESSABLE_ENTITY)
|
|
112
|
+
return problem(
|
|
113
|
+
'Out of Stock',
|
|
114
|
+
422,
|
|
115
|
+
'One or more requested items are no longer available in inventory.',
|
|
116
|
+
{
|
|
117
|
+
type: 'https://api.example.com/errors/inventory',
|
|
118
|
+
instance: req.path,
|
|
119
|
+
unavailableItems: ['item_101', 'item_204'],
|
|
120
|
+
}
|
|
121
|
+
)
|
|
122
|
+
},
|
|
123
|
+
}),
|
|
178
124
|
})
|
|
179
125
|
```
|
|
180
126
|
|
|
181
|
-
|
|
182
127
|
---
|
|
183
128
|
|
|
184
|
-
##
|
|
185
|
-
|
|
186
|
-
To eliminate pagination inconsistency across different endpoints, ExisJS exports standard `paginate()` and `getPaginationSkip()` utilities directly from `exisjs/validator` (as well as `exisjs/response`).
|
|
129
|
+
## Standardized Pagination (`paginate`)
|
|
187
130
|
|
|
188
|
-
|
|
131
|
+
The `paginate()` helper combines a data array with calculated total counts to produce a uniform pagination object:
|
|
189
132
|
|
|
190
133
|
```typescript filename="src/http/products/route.ts"
|
|
191
134
|
import { controller, route } from 'exisjs/router'
|
|
192
|
-
import {
|
|
135
|
+
import { paginate, getPaginationSkip, tex } from 'exisjs/validator'
|
|
136
|
+
|
|
137
|
+
const QuerySchema = tex.pagination({ defaultLimit: 20, maxLimit: 100 })
|
|
193
138
|
|
|
194
139
|
export default controller({
|
|
195
|
-
|
|
196
|
-
query:
|
|
197
|
-
async
|
|
198
|
-
// 1. Automatically extracts { skip, limit, page } with fallback defaults
|
|
140
|
+
list: route.get('/', {
|
|
141
|
+
query: QuerySchema,
|
|
142
|
+
handle: async ({ query }) => {
|
|
199
143
|
const { skip, limit, page } = getPaginationSkip(query)
|
|
200
144
|
|
|
201
|
-
// 2. Fetch items and count total from your database
|
|
202
145
|
const [items, total] = await Promise.all([
|
|
203
|
-
db.
|
|
204
|
-
db.
|
|
146
|
+
db.products.findMany({ skip, take: limit }),
|
|
147
|
+
db.products.count(),
|
|
205
148
|
])
|
|
206
149
|
|
|
207
|
-
// 3. Return standardized pagination envelope
|
|
208
150
|
return paginate(items, total, { page, limit })
|
|
209
|
-
}
|
|
210
|
-
})
|
|
151
|
+
},
|
|
152
|
+
}),
|
|
211
153
|
})
|
|
212
154
|
```
|
|
213
155
|
|
|
214
|
-
|
|
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
|
-
|
|
238
|
-
### The Standardized Pagination Envelope
|
|
239
|
-
|
|
240
|
-
Calling `paginate(items, total, { page, limit })` produces a uniform envelope:
|
|
156
|
+
### JSON Output Format
|
|
241
157
|
|
|
242
158
|
```json
|
|
243
159
|
{
|
|
160
|
+
"success": true,
|
|
244
161
|
"data": [
|
|
245
|
-
{ "id":
|
|
246
|
-
{ "id":
|
|
162
|
+
{ "id": "p1", "name": "Keyboard" },
|
|
163
|
+
{ "id": "p2", "name": "Mouse" }
|
|
247
164
|
],
|
|
248
165
|
"pagination": {
|
|
249
166
|
"page": 1,
|
|
250
|
-
"limit":
|
|
167
|
+
"limit": 20,
|
|
251
168
|
"total": 42,
|
|
252
169
|
"totalPages": 3,
|
|
253
170
|
"hasNext": true,
|
|
@@ -258,478 +175,228 @@ Calling `paginate(items, total, { page, limit })` produces a uniform envelope:
|
|
|
258
175
|
}
|
|
259
176
|
```
|
|
260
177
|
|
|
261
|
-
Both `hasNext`/`hasPrev` and `hasNextPage`/`hasPrevPage` are provided out-of-the-box, ensuring full compatibility with both cursor-style and page-number frontend UI components.
|
|
262
|
-
|
|
263
178
|
---
|
|
264
179
|
|
|
265
|
-
##
|
|
180
|
+
## Setting Headers & Status Codes
|
|
266
181
|
|
|
267
|
-
|
|
182
|
+
`ExisResponse` methods can be chained:
|
|
268
183
|
|
|
269
184
|
```typescript
|
|
270
185
|
res.status(404)
|
|
271
|
-
.set('X-Custom-Header', '
|
|
272
|
-
.append('Set-Cookie', '
|
|
186
|
+
.set('X-Custom-Header', 'custom-value')
|
|
187
|
+
.append('Set-Cookie', 'pref=dark')
|
|
273
188
|
.json({ error: 'Not Found' })
|
|
274
189
|
```
|
|
275
190
|
|
|
276
|
-
###
|
|
277
|
-
|
|
278
|
-
- `res.
|
|
279
|
-
- `res.
|
|
280
|
-
- `res.
|
|
281
|
-
- `res.type(mimeType)`:
|
|
282
|
-
- `res.
|
|
191
|
+
### Header Methods
|
|
192
|
+
|
|
193
|
+
- `res.status(code)`: Sets the numeric HTTP status code.
|
|
194
|
+
- `res.set(name, value)` / `res.header(name, value)`: Sets a header.
|
|
195
|
+
- `res.append(name, value)`: Appends to a header (e.g. multiple `Set-Cookie` directives).
|
|
196
|
+
- `res.type(mimeType)`: Shorthand for setting `Content-Type` (e.g. `res.type('html')` $\to$ `text/html`).
|
|
197
|
+
- `res.removeHeader(name)`: Removes a previously set header.
|
|
198
|
+
- `res.vary(field)`: Adds a field to the `Vary` caching header.
|
|
283
199
|
|
|
284
200
|
---
|
|
285
201
|
|
|
286
|
-
##
|
|
202
|
+
## Cookie Management
|
|
287
203
|
|
|
288
|
-
|
|
204
|
+
Set and clear cookies with modern web standard options (including CHIPS Partitioning and Priority hints):
|
|
289
205
|
|
|
290
206
|
### Setting Cookies
|
|
291
207
|
|
|
292
208
|
```typescript
|
|
293
|
-
//
|
|
294
|
-
res.cookie('
|
|
295
|
-
maxAge: 86400, //
|
|
209
|
+
// Standard secure HTTP-only cookie
|
|
210
|
+
res.cookie('token', 'xyz123', {
|
|
211
|
+
maxAge: 86400, // 24 hours in seconds
|
|
296
212
|
httpOnly: true,
|
|
297
213
|
secure: true,
|
|
298
|
-
sameSite: 'Strict'
|
|
214
|
+
sameSite: 'Strict',
|
|
215
|
+
path: '/',
|
|
299
216
|
})
|
|
300
217
|
|
|
301
|
-
// Cross-site
|
|
302
|
-
res.cookie('
|
|
303
|
-
sameSite: 'None', //
|
|
218
|
+
// Cross-site iframe cookie with CHIPS partitioning
|
|
219
|
+
res.cookie('embed_pref', 'theme_dark', {
|
|
220
|
+
sameSite: 'None', // Forces secure: true automatically
|
|
304
221
|
partitioned: true, // Cookies Having Independent Partitioned State (CHIPS)
|
|
305
|
-
priority: 'high',
|
|
306
|
-
path: '/'
|
|
222
|
+
priority: 'high',
|
|
223
|
+
path: '/',
|
|
307
224
|
})
|
|
308
225
|
```
|
|
309
226
|
|
|
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
227
|
### Clearing Cookies
|
|
325
228
|
|
|
326
|
-
When clearing cookies, browsers require the matching `path` and `domain` used when the cookie was originally set:
|
|
327
|
-
|
|
328
229
|
```typescript
|
|
329
|
-
|
|
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
|
-
})
|
|
230
|
+
res.clearCookie('token', { path: '/' })
|
|
337
231
|
```
|
|
338
232
|
|
|
339
|
-
|
|
233
|
+
#### Cookie Options
|
|
340
234
|
|
|
341
|
-
|
|
235
|
+
| Option | Type | Description |
|
|
236
|
+
| :--- | :--- | :--- |
|
|
237
|
+
| `maxAge` | `number` | Expiration time in seconds from current time |
|
|
238
|
+
| `expires` | `Date` | Absolute expiration date |
|
|
239
|
+
| `httpOnly` | `boolean` | Flags cookie as inaccessible to client scripts |
|
|
240
|
+
| `secure` | `boolean` | Requires HTTPS transport (forced if `sameSite: 'None'`) |
|
|
241
|
+
| `sameSite` | `'Strict' \| 'Lax' \| 'None' \| boolean` | Cross-site request cookie behavior |
|
|
242
|
+
| `partitioned` | `boolean` | Enables CHIPS cookie partitioning |
|
|
243
|
+
| `priority` | `'low' \| 'medium' \| 'high'` | Storage priority under client disk pressure |
|
|
244
|
+
| `path` | `string` | URL path scope (default: `'/'`) |
|
|
245
|
+
| `domain` | `string` | Domain scope for the cookie |
|
|
342
246
|
|
|
343
|
-
|
|
247
|
+
---
|
|
344
248
|
|
|
345
|
-
|
|
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.
|
|
249
|
+
## File Downloads & Streaming
|
|
347
250
|
|
|
348
|
-
|
|
251
|
+
### File Downloads with `res.download()`
|
|
349
252
|
|
|
350
|
-
|
|
253
|
+
Reads a file from disk, sets `Content-Disposition: attachment`, and infers MIME type:
|
|
254
|
+
|
|
255
|
+
```typescript filename="src/http/reports/route.ts"
|
|
351
256
|
import { controller, route } from 'exisjs/router'
|
|
352
257
|
|
|
353
258
|
export default controller({
|
|
354
|
-
|
|
355
|
-
handle(
|
|
356
|
-
res.download('/
|
|
357
|
-
}
|
|
358
|
-
})
|
|
259
|
+
download: route.get('/annual', {
|
|
260
|
+
handle: async ({ res }) => {
|
|
261
|
+
res.download('./storage/annual-report.pdf', 'Annual-Report-2026.pdf')
|
|
262
|
+
},
|
|
263
|
+
}),
|
|
359
264
|
})
|
|
360
265
|
```
|
|
361
266
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
```typescript
|
|
365
|
-
import { Controller, Get, Res } from 'exisjs/decorators'
|
|
366
|
-
|
|
367
|
-
@Controller()
|
|
368
|
-
export default class ReportController {
|
|
369
|
-
@Get('/report')
|
|
370
|
-
async downloadReport(@Res() res: any) {
|
|
371
|
-
res.download('/absolute/path/to/report.pdf', 'Q4-Report.pdf')
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
```
|
|
267
|
+
---
|
|
375
268
|
|
|
376
|
-
### `res.
|
|
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 ... }`)
|
|
269
|
+
### Stream Piping (`res.stream()` / `res.sendStream()`)
|
|
381
270
|
|
|
382
|
-
|
|
383
|
-
-
|
|
384
|
-
-
|
|
385
|
-
-
|
|
271
|
+
ExisJS supports direct streaming for:
|
|
272
|
+
- Node.js `Readable` streams (`fs.createReadStream`, database cursor streams)
|
|
273
|
+
- Web standard `ReadableStream` (OpenAI SDK, fetch responses)
|
|
274
|
+
- `AsyncIterable` and async generators
|
|
386
275
|
|
|
387
276
|
#### 1. Streaming Node.js Readable Streams
|
|
388
277
|
|
|
389
|
-
|
|
390
|
-
```typescript filename="src/http/video/route.ts"
|
|
278
|
+
```typescript filename="src/http/export/route.ts"
|
|
391
279
|
import { controller, route } from 'exisjs/router'
|
|
392
280
|
import fs from 'node:fs'
|
|
393
281
|
|
|
394
282
|
export default controller({
|
|
395
|
-
|
|
396
|
-
handle(
|
|
397
|
-
const
|
|
398
|
-
res.type('
|
|
399
|
-
res.
|
|
400
|
-
}
|
|
401
|
-
})
|
|
283
|
+
exportCsv: route.get('/', {
|
|
284
|
+
handle: ({ res }) => {
|
|
285
|
+
const stream = fs.createReadStream('./data/export.csv')
|
|
286
|
+
res.type('text/csv')
|
|
287
|
+
res.stream(stream)
|
|
288
|
+
},
|
|
289
|
+
}),
|
|
402
290
|
})
|
|
403
291
|
```
|
|
404
292
|
|
|
405
|
-
|
|
406
|
-
```typescript filename="src/http/video/route.ts"
|
|
407
|
-
import { Controller, Get, Res } from 'exisjs/decorators'
|
|
408
|
-
import type { Response } from 'exisjs/router'
|
|
409
|
-
import fs from 'node:fs'
|
|
293
|
+
#### 2. Streaming Web Standard Streams (AI Chat & Fetch)
|
|
410
294
|
|
|
411
|
-
@Controller('/video')
|
|
412
|
-
export default class VideoController {
|
|
413
|
-
@Get('/')
|
|
414
|
-
streamVideo(@Res() res: Response) {
|
|
415
|
-
const videoStream = fs.createReadStream('/path/to/video.mp4')
|
|
416
|
-
res.type('video/mp4')
|
|
417
|
-
res.sendStream(videoStream)
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
```
|
|
421
|
-
|
|
422
|
-
#### 2. Streaming Web Standard `ReadableStream` (AI SDKs & Fetch)
|
|
423
|
-
|
|
424
|
-
##### Functional Paradigm
|
|
425
295
|
```typescript filename="src/http/ai/route.ts"
|
|
426
296
|
import { controller, route } from 'exisjs/router'
|
|
427
297
|
|
|
428
298
|
export default controller({
|
|
429
299
|
chat: route.post('/', {
|
|
430
|
-
async
|
|
431
|
-
const
|
|
300
|
+
handle: async ({ req, res }) => {
|
|
301
|
+
const upstream = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
432
302
|
method: 'POST',
|
|
433
|
-
headers: {
|
|
434
|
-
|
|
303
|
+
headers: {
|
|
304
|
+
'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`,
|
|
305
|
+
'Content-Type': 'application/json',
|
|
306
|
+
},
|
|
307
|
+
body: JSON.stringify({
|
|
308
|
+
model: 'gpt-4o',
|
|
309
|
+
stream: true,
|
|
310
|
+
messages: req.body.messages,
|
|
311
|
+
}),
|
|
435
312
|
})
|
|
436
313
|
|
|
437
314
|
res.type('text/event-stream')
|
|
438
|
-
res.stream(
|
|
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
|
-
})
|
|
315
|
+
res.stream(upstream.body!)
|
|
316
|
+
},
|
|
317
|
+
}),
|
|
485
318
|
})
|
|
486
319
|
```
|
|
487
320
|
|
|
488
|
-
|
|
489
|
-
```typescript filename="src/http/feed/route.ts"
|
|
490
|
-
import { Controller, Get, Res } from 'exisjs/decorators'
|
|
491
|
-
import type { Response } from 'exisjs/router'
|
|
321
|
+
#### 3. Automatic Return Stream Piping
|
|
492
322
|
|
|
493
|
-
|
|
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
|
-
}
|
|
323
|
+
If a route handler returns a `Readable` stream directly, ExisJS detects and pipes it automatically:
|
|
503
324
|
|
|
504
|
-
|
|
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"
|
|
325
|
+
```typescript filename="src/http/files/route.ts"
|
|
515
326
|
import { controller, route } from 'exisjs/router'
|
|
516
327
|
import fs from 'node:fs'
|
|
517
328
|
|
|
518
329
|
export default controller({
|
|
519
|
-
|
|
520
|
-
handle() {
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
})
|
|
330
|
+
getFile: route.get('/', {
|
|
331
|
+
handle: () => {
|
|
332
|
+
return fs.createReadStream('./data/report.csv')
|
|
333
|
+
},
|
|
334
|
+
}),
|
|
525
335
|
})
|
|
526
336
|
```
|
|
527
337
|
|
|
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
|
-
|
|
543
338
|
---
|
|
544
339
|
|
|
545
|
-
##
|
|
340
|
+
## Server-Sent Events (SSE)
|
|
546
341
|
|
|
547
|
-
|
|
548
|
-
|
|
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'
|
|
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
|
-
}
|
|
574
|
-
```
|
|
575
|
-
|
|
576
|
-
---
|
|
577
|
-
|
|
578
|
-
## 6. Safe Response Serialization (BSON, Dates & Prototype Auto-Unwrapping)
|
|
579
|
-
|
|
580
|
-
ExisJS includes a robust, zero-crash serialization engine that automatically prepares complex objects before sending them down the wire:
|
|
581
|
-
|
|
582
|
-
- **Mongoose / ORM Documents**: Automatically unwraps documents by invoking `.toObject({ virtuals: true, getters: true })` or `.toJSON()`. You no longer need to call `.lean()` or sanitize documents manually.
|
|
583
|
-
- **MongoDB BSON Types**: Automatically converts `ObjectId`, `Decimal128`, `Long`, and `Binary` buffers to their clean string / base64 representations.
|
|
584
|
-
- **BigInt & Dates**: Converts native `BigInt` values to strings and `Date` instances to ISO 8601 strings.
|
|
585
|
-
- **Maps & Sets**: Automatically converts `Map` to plain objects and `Set` to arrays.
|
|
586
|
-
- **Circular Reference Guard**: Circular object references are safely detected and replaced with `'[Circular]'` rather than throwing fatal V8 JSON serialization exceptions.
|
|
587
|
-
- **Enhanced Diagnostics**: If an unhandled serialization issue occurs, ExisJS logs the exact property path (e.g. `'recentTransactions[0].accountId'`) for immediate debugging clarity.
|
|
588
|
-
|
|
589
|
-
---
|
|
590
|
-
|
|
591
|
-
## 7. ETags & Conditional Requests (304 Not Modified)
|
|
592
|
-
|
|
593
|
-
ExisJS features native ETag generation.
|
|
594
|
-
|
|
595
|
-
By default, ETags are disabled (`etag: false`) on dynamic REST API endpoints to minimize hashing CPU overhead and provide transparent logging timings.
|
|
596
|
-
|
|
597
|
-
When enabled globally (`etag: true` in `exis.config.ts`) or on a per-route basis (`res.etagEnabled = true`):
|
|
598
|
-
- If the client sends an `If-None-Match` header matching the response ETag, ExisJS immediately sends `304 Not Modified` with an empty body, saving bandwidth.
|
|
599
|
-
|
|
600
|
-
---
|
|
601
|
-
|
|
602
|
-
## 8. Server-Sent Events (SSE) & AI Streaming
|
|
603
|
-
|
|
604
|
-
ExisJS provides a first-class `res.sse()` method for real-time live events and AI chat completions (OpenAI, Anthropic, Ollama, LangChain).
|
|
605
|
-
|
|
606
|
-
When called, `res.sse()` automatically configures and flushes the required HTTP headers:
|
|
607
|
-
- `Content-Type: text/event-stream; charset=utf-8`
|
|
608
|
-
- `Cache-Control: no-cache, no-transform`
|
|
609
|
-
- `Connection: keep-alive`
|
|
610
|
-
- `X-Accel-Buffering: no` (disables NGINX proxy buffering)
|
|
611
|
-
|
|
612
|
-
You can import `SSEStream`, `formatSSEEvent`, `SSEMessage`, and `SSEOptions` from `exisjs/router`.
|
|
613
|
-
|
|
614
|
-
### Basic SSE Stream
|
|
615
|
-
|
|
616
|
-
#### Functional Paradigm
|
|
342
|
+
Use `res.sse()` to open a real-time event stream:
|
|
617
343
|
|
|
618
344
|
```typescript filename="src/http/events/route.ts"
|
|
619
345
|
import { controller, route } from 'exisjs/router'
|
|
620
346
|
|
|
621
347
|
export default controller({
|
|
622
|
-
|
|
623
|
-
handle(
|
|
624
|
-
// Initialize the stream (heartbeat ping every 15s by default)
|
|
348
|
+
liveFeed: route.get('/', {
|
|
349
|
+
handle: ({ res }) => {
|
|
625
350
|
const sse = res.sse()
|
|
626
351
|
|
|
627
352
|
sse.send({ event: 'connected', data: { timestamp: Date.now() } })
|
|
628
353
|
|
|
629
354
|
const interval = setInterval(() => {
|
|
630
|
-
sse.send({ event: 'price
|
|
355
|
+
sse.send({ event: 'price', data: { symbol: 'BTC', price: 95000 } })
|
|
631
356
|
}, 1000)
|
|
632
357
|
|
|
633
|
-
// Automatically clean up intervals when the client disconnects or closes the tab
|
|
634
358
|
sse.onClose(() => {
|
|
635
359
|
clearInterval(interval)
|
|
636
360
|
})
|
|
637
|
-
}
|
|
638
|
-
})
|
|
361
|
+
},
|
|
362
|
+
}),
|
|
639
363
|
})
|
|
640
364
|
```
|
|
641
365
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
```typescript filename="src/http/events/route.ts"
|
|
645
|
-
import { Controller, Get, Res } from 'exisjs/decorators'
|
|
646
|
-
import type { Response } from 'exisjs/router'
|
|
647
|
-
|
|
648
|
-
@Controller()
|
|
649
|
-
export default class EventsController {
|
|
650
|
-
@Get('/events')
|
|
651
|
-
streamEvents(@Res() res: Response) {
|
|
652
|
-
const sse = res.sse()
|
|
653
|
-
|
|
654
|
-
sse.send({ event: 'connected', data: { status: 'ok' } })
|
|
655
|
-
|
|
656
|
-
sse.onClose(() => {
|
|
657
|
-
console.log('Client disconnected from SSE stream')
|
|
658
|
-
})
|
|
659
|
-
}
|
|
660
|
-
}
|
|
661
|
-
```
|
|
662
|
-
|
|
663
|
-
### AI / LLM Streaming with `sse.pipeFrom()`
|
|
664
|
-
|
|
665
|
-
You can directly stream chunks from any `AsyncIterable` (such as the OpenAI SDK, Anthropic SDK, or custom async generators) using `sse.pipeFrom()`:
|
|
366
|
+
### Streaming Async Iterables with `sse.pipeFrom()`
|
|
666
367
|
|
|
667
|
-
|
|
368
|
+
Stream chunks from an async generator or AI SDK directly:
|
|
668
369
|
|
|
669
370
|
```typescript filename="src/http/ai/route.ts"
|
|
670
371
|
import { controller, route } from 'exisjs/router'
|
|
671
372
|
|
|
672
373
|
export default controller({
|
|
673
|
-
|
|
674
|
-
handle(
|
|
374
|
+
streamTokens: route.post('/', {
|
|
375
|
+
handle: ({ res }) => {
|
|
675
376
|
res.sse(async (sse) => {
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
yield '
|
|
679
|
-
yield '
|
|
680
|
-
yield ' ExisJS'
|
|
681
|
-
yield ' AI streaming!'
|
|
377
|
+
async function* tokenGenerator() {
|
|
378
|
+
yield 'Hello '
|
|
379
|
+
yield 'from '
|
|
380
|
+
yield 'ExisJS SSE!'
|
|
682
381
|
}
|
|
683
382
|
|
|
684
|
-
await sse.pipeFrom(
|
|
685
|
-
event: '
|
|
686
|
-
data: { text:
|
|
383
|
+
await sse.pipeFrom(tokenGenerator(), (chunk) => ({
|
|
384
|
+
event: 'token',
|
|
385
|
+
data: { text: chunk },
|
|
687
386
|
}))
|
|
688
387
|
|
|
689
388
|
sse.close()
|
|
690
389
|
})
|
|
691
|
-
}
|
|
692
|
-
})
|
|
390
|
+
},
|
|
391
|
+
}),
|
|
693
392
|
})
|
|
694
393
|
```
|
|
695
394
|
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
```typescript filename="src/http/ai/route.ts"
|
|
699
|
-
import { Controller, Post, Res } from 'exisjs/decorators'
|
|
700
|
-
import type { Response } from 'exisjs/router'
|
|
701
|
-
|
|
702
|
-
@Controller('/ai')
|
|
703
|
-
export default class AiController {
|
|
704
|
-
@Post('/chat')
|
|
705
|
-
chat(@Res() res: Response) {
|
|
706
|
-
res.sse(async (sse) => {
|
|
707
|
-
async function* generateTokens() {
|
|
708
|
-
yield 'Hello'
|
|
709
|
-
yield ' from'
|
|
710
|
-
yield ' ExisJS'
|
|
711
|
-
yield ' AI streaming!'
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
await sse.pipeFrom(generateTokens(), (token) => ({
|
|
715
|
-
event: 'delta',
|
|
716
|
-
data: { text: token }
|
|
717
|
-
}))
|
|
718
|
-
|
|
719
|
-
sse.close()
|
|
720
|
-
})
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
```
|
|
724
|
-
|
|
725
|
-
### SSE Options & Methods
|
|
395
|
+
---
|
|
726
396
|
|
|
727
|
-
|
|
728
|
-
- `heartbeat`: Heartbeat interval in milliseconds (default: `15000`). Pass `false` to disable.
|
|
729
|
-
- `heartbeatComment`: Custom comment string for keep-alive pings (default: `": ping\n\n"`).
|
|
730
|
-
- `headers`: Additional custom HTTP headers.
|
|
731
|
-
- `sse.send(message)`: Sends an event. Accepts string, number, object (auto JSON-stringified), or `{ event, data, id, retry }`.
|
|
732
|
-
- `sse.comment(text)`: Sends a raw SSE comment line (e.g. `: ping\n\n`) without triggering client-side event listeners.
|
|
733
|
-
- `sse.onClose(callback)`: Registers a callback fired when the client disconnects.
|
|
734
|
-
- `sse.close()`: Gracefully terminates the stream and closes the HTTP response.
|
|
397
|
+
## Next Steps
|
|
735
398
|
|
|
399
|
+
- **[Boundaries & Cascading](/docs/boundaries)**: Folder-level middleware, security guards, and error boundaries.
|
|
400
|
+
- **[Validation Guide](/docs/validation)**: Input validation with `tex` schema builder.
|
|
401
|
+
- **[Controllers](/docs/controllers)**: Working with class-based response decorators.
|
|
402
|
+
- **[Requests](/docs/requests)**: Inspecting request details, headers, and abort signals.
|