exisjs 0.7.12 → 0.7.14

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.
Files changed (132) hide show
  1. package/README.md +195 -39
  2. package/dist/cli/commands/run.d.ts.map +1 -1
  3. package/dist/cli/commands/run.js +3 -1
  4. package/dist/cli/commands/run.js.map +1 -1
  5. package/dist/config/types.d.ts +2 -2
  6. package/dist/config/types.d.ts.map +1 -1
  7. package/dist/decorators/params.d.ts +21 -0
  8. package/dist/decorators/params.d.ts.map +1 -1
  9. package/dist/decorators/params.js +21 -1
  10. package/dist/decorators/params.js.map +1 -1
  11. package/dist/di/container.d.ts +15 -6
  12. package/dist/di/container.d.ts.map +1 -1
  13. package/dist/di/container.js +26 -3
  14. package/dist/di/container.js.map +1 -1
  15. package/dist/di/index.d.ts +1 -1
  16. package/dist/di/index.d.ts.map +1 -1
  17. package/dist/di/index.js.map +1 -1
  18. package/dist/di/inject.d.ts +2 -1
  19. package/dist/di/inject.d.ts.map +1 -1
  20. package/dist/di/inject.js +2 -1
  21. package/dist/di/inject.js.map +1 -1
  22. package/dist/docs/ARCHITECTURE.md +130 -78
  23. package/dist/docs/BENCHMARKS.md +4 -4
  24. package/dist/docs/content/README.md +43 -44
  25. package/dist/docs/content/boundaries.mdx +138 -61
  26. package/dist/docs/content/cli/commands.mdx +151 -128
  27. package/dist/docs/content/cli/overview.mdx +61 -47
  28. package/dist/docs/content/cli/usage.mdx +107 -30
  29. package/dist/docs/content/config.mdx +145 -158
  30. package/dist/docs/content/controllers.mdx +193 -464
  31. package/dist/docs/content/cron.mdx +183 -69
  32. package/dist/docs/content/decorators.mdx +449 -154
  33. package/dist/docs/content/errors.mdx +202 -305
  34. package/dist/docs/content/exisjs-mcp.mdx +59 -49
  35. package/dist/docs/content/installation.mdx +243 -157
  36. package/dist/docs/content/introduction.mdx +220 -48
  37. package/dist/docs/content/middleware.mdx +171 -497
  38. package/dist/docs/content/modules.mdx +141 -83
  39. package/dist/docs/content/providers.mdx +216 -180
  40. package/dist/docs/content/requests.mdx +168 -488
  41. package/dist/docs/content/responses.mdx +200 -533
  42. package/dist/docs/content/routing.mdx +235 -598
  43. package/dist/docs/content/security.mdx +233 -57
  44. package/dist/docs/content/structure.mdx +151 -240
  45. package/dist/docs/content/swagger.mdx +192 -91
  46. package/dist/docs/content/testing.mdx +205 -74
  47. package/dist/docs/content/validation.mdx +479 -341
  48. package/dist/docs/reference/README.md +34 -2
  49. package/dist/docs/reference/config.mdx +277 -75
  50. package/dist/docs/reference/context.mdx +197 -106
  51. package/dist/docs/reference/cron.mdx +266 -152
  52. package/dist/docs/reference/database.mdx +135 -53
  53. package/dist/docs/reference/decorators.mdx +234 -116
  54. package/dist/docs/reference/di.mdx +146 -112
  55. package/dist/docs/reference/error.mdx +155 -178
  56. package/dist/docs/reference/exis.mdx +141 -70
  57. package/dist/docs/reference/logger.mdx +110 -126
  58. package/dist/docs/reference/middleware.mdx +235 -158
  59. package/dist/docs/reference/plugin.mdx +136 -64
  60. package/dist/docs/reference/router.mdx +275 -293
  61. package/dist/docs/reference/sanitize.mdx +91 -87
  62. package/dist/docs/reference/swagger.mdx +280 -100
  63. package/dist/docs/reference/testing.mdx +223 -89
  64. package/dist/docs/reference/validator.mdx +194 -206
  65. package/dist/lib/start-server.js +22 -8
  66. package/dist/lib/start-server.js.map +1 -1
  67. package/dist/module/module.d.ts +6 -3
  68. package/dist/module/module.d.ts.map +1 -1
  69. package/dist/module/module.js +17 -3
  70. package/dist/module/module.js.map +1 -1
  71. package/dist/router/controller-registrar.d.ts.map +1 -1
  72. package/dist/router/controller-registrar.js +26 -4
  73. package/dist/router/controller-registrar.js.map +1 -1
  74. package/dist/router/scanner/fs-scanner.d.ts.map +1 -1
  75. package/dist/router/scanner/fs-scanner.js +22 -0
  76. package/dist/router/scanner/fs-scanner.js.map +1 -1
  77. package/dist/router/types.d.ts +6 -4
  78. package/dist/router/types.d.ts.map +1 -1
  79. package/dist/server/app.d.ts +3 -1
  80. package/dist/server/app.d.ts.map +1 -1
  81. package/dist/server/app.js +24 -8
  82. package/dist/server/app.js.map +1 -1
  83. package/dist/server/bootstrapper.d.ts.map +1 -1
  84. package/dist/server/bootstrapper.js +19 -0
  85. package/dist/server/bootstrapper.js.map +1 -1
  86. package/dist/server/request.d.ts +2 -0
  87. package/dist/server/request.d.ts.map +1 -1
  88. package/dist/server/request.js +11 -0
  89. package/dist/server/request.js.map +1 -1
  90. package/dist/server/response.d.ts +10 -5
  91. package/dist/server/response.d.ts.map +1 -1
  92. package/dist/server/response.js +65 -18
  93. package/dist/server/response.js.map +1 -1
  94. package/dist/swagger/generator.d.ts.map +1 -1
  95. package/dist/swagger/generator.js +130 -10
  96. package/dist/swagger/generator.js.map +1 -1
  97. package/dist/telemetry/index.d.ts +12 -0
  98. package/dist/telemetry/index.d.ts.map +1 -0
  99. package/dist/telemetry/index.js +121 -0
  100. package/dist/telemetry/index.js.map +1 -0
  101. package/dist/testing/client.d.ts +1 -0
  102. package/dist/testing/client.d.ts.map +1 -1
  103. package/dist/testing/client.js +4 -1
  104. package/dist/testing/client.js.map +1 -1
  105. package/dist/testing/index.d.ts.map +1 -1
  106. package/dist/testing/index.js +11 -4
  107. package/dist/testing/index.js.map +1 -1
  108. package/dist/validator/builder.d.ts +26 -1
  109. package/dist/validator/builder.d.ts.map +1 -1
  110. package/dist/validator/builder.js +31 -0
  111. package/dist/validator/builder.js.map +1 -1
  112. package/dist/validator/engine.d.ts +23 -0
  113. package/dist/validator/engine.d.ts.map +1 -1
  114. package/dist/validator/engine.js +228 -31
  115. package/dist/validator/engine.js.map +1 -1
  116. package/dist/validator/index.d.ts +8 -4
  117. package/dist/validator/index.d.ts.map +1 -1
  118. package/dist/validator/index.js +4 -1
  119. package/dist/validator/index.js.map +1 -1
  120. package/dist/validator/tex-types.d.ts +19 -4
  121. package/dist/validator/tex-types.d.ts.map +1 -1
  122. package/dist/validator/tex-types.js +102 -0
  123. package/dist/validator/tex-types.js.map +1 -1
  124. package/dist/validator/tex.d.ts +1 -1
  125. package/dist/validator/tex.d.ts.map +1 -1
  126. package/dist/validator/tex.js +3 -1
  127. package/dist/validator/tex.js.map +1 -1
  128. package/dist/validator/types.d.ts +1 -0
  129. package/dist/validator/types.d.ts.map +1 -1
  130. package/package.json +46 -14
  131. package/dist/docs/content/dataloader.mdx +0 -170
  132. package/dist/docs/reference/dataloader.mdx +0 -91
@@ -1,253 +1,170 @@
1
1
  ---
2
- nextUrl: "/docs/prisma"
3
- nextTitle: "Prisma Integration"
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
- # Responses
8
+ # Response Helpers & Streaming (`ExisResponse`)
7
9
 
8
- Unlike standard Node.js `ServerResponse` objects, ExisJS wraps every response in an `ExisResponse` class. It provides a chainable API while maintaining the raw performance of native Node.
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
- ## 1. Returning Data vs Sending Data
14
+ ## Automatic JSON Serialization
15
15
 
16
- The most common way to respond to a request in ExisJS is to simply **return an object or array**. ExisJS intercepts the return value, automatically serializes it to JSON, sets the `Content-Type`, calculates the `Content-Length`, and sends it down the wire.
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
- #### Functional Paradigm
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
- getUsers: route.get('/', {
25
- handle(req, res) {
26
- // ExisJS automatically handles JSON serialization
27
- return [{ id: 1, name: "Alice" }]
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
- #### Class-Based (OOP) Paradigm
36
+ ### Class-Based (OOP) Controller
64
37
 
65
- ```typescript filename="src/http/route.ts"
66
- import { Controller, Get, Res } from 'exisjs/decorators'
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
- async getUsers(@Res() res: any) {
72
- // Chainable response API
73
- res.status(201).json([{ id: 1, name: "Alice" }])
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
- ## 2. Standardized JSON Responses (`exisjs/response`)
56
+ ## Response Envelope Builders (`exisjs/response`)
81
57
 
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).
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
- 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.
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
- ### Available Response Builders
70
+ ### Example Usage
87
71
 
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
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(req, res) {
110
- const user = db.users.find(req.params.id)
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
- // Generates: { success: false, error: { message: "User not found", code: "NOT_FOUND" } }
115
- return error("User not found", "NOT_FOUND")
82
+ return error('User not found', 'NOT_FOUND')
116
83
  }
117
-
118
- // Generates: { success: true, data: { id: 1, name: "Alice" }, message: "Found user" }
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(req, res) {
125
- const user = db.users.create(req.body)
89
+ handle: async ({ body, res }) => {
90
+ const user = await db.users.create(body)
126
91
  res.status(HttpStatus.CREATED)
127
- // Generates: { success: true, data: user, message: "User registered" }
128
- return created(user, "User registered")
129
- }
130
- })
92
+ return created(user, 'User registered')
93
+ },
94
+ }),
131
95
  })
132
96
  ```
133
97
 
134
- #### Class-Based (OOP) Paradigm Example
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
- ### RFC 7807 / RFC 9457 Problem Details (`problem`)
100
+ ## RFC 7807 / RFC 9457 Problem Details
164
101
 
165
- For enterprise and public REST APIs requiring standardized error models, `problem()` produces RFC-compliant Problem Details:
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
- 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
- ]
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
- ## 3. Standardized Pagination Envelopes (`paginate`)
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
- #### Functional Paradigm
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 { tex, paginate, getPaginationSkip } from 'exisjs/validator'
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
- listProducts: route.get('/', {
196
- query: tex.pagination({ defaultLimit: 15, maxLimit: 50 }),
197
- async handle({ query }) {
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.product.findMany({ skip, take: limit, orderBy: { createdAt: 'desc' } }),
204
- db.product.count(),
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
- #### 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
-
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": 1, "name": "Mechanical Keyboard" },
246
- { "id": 2, "name": "Ergonomic Mouse" }
162
+ { "id": "p1", "name": "Keyboard" },
163
+ { "id": "p2", "name": "Mouse" }
247
164
  ],
248
165
  "pagination": {
249
166
  "page": 1,
250
- "limit": 15,
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
- ## 4. Setting Headers & Status
180
+ ## Setting Headers & Status Codes
266
181
 
267
- The `ExisResponse` object implements a chainable API for HTTP headers and status codes.
182
+ `ExisResponse` methods can be chained:
268
183
 
269
184
  ```typescript
270
185
  res.status(404)
271
- .set('X-Custom-Header', 'exis-is-awesome')
272
- .append('Set-Cookie', 'foo=bar')
186
+ .set('X-Custom-Header', 'custom-value')
187
+ .append('Set-Cookie', 'pref=dark')
273
188
  .json({ error: 'Not Found' })
274
189
  ```
275
190
 
276
- ### Supported Header Methods
277
- - `res.status(code)`: Sets the HTTP status code.
278
- - `res.set(key, value)` (or `res.header()`): Sets an HTTP header.
279
- - `res.append(key, value)`: Appends to an existing header (e.g., multiple `Set-Cookie` directives).
280
- - `res.removeHeader(key)`: Deletes a header.
281
- - `res.type(mimeType)`: A shorthand for setting the `Content-Type` header (e.g., `res.type('html')` becomes `text/html`).
282
- - `res.vary(field)`: Adds a field to the `Vary` header for caching.
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
- ## 3. Cookies
202
+ ## Cookie Management
287
203
 
288
- ExisJS provides native helper methods for setting and clearing cookies safely, supporting modern web standards including CHIPS (Partitioned Cookies) and Priority hints.
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
- // Set a secure, HTTP-only session cookie
294
- res.cookie('session_id', '123456', {
295
- maxAge: 86400, // 1 day in seconds
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 embedded iframe cookie with CHIPS (Partitioned)
302
- res.cookie('widget_pref', 'dark_mode', {
303
- sameSite: 'None', // Automatically forces `secure: true` per modern browser specs
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', // Hints browser cookie retention under storage pressure ('low' | 'medium' | '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
- // Clear default root cookie
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
- ## 4. File Downloads & Streaming
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
- ExisJS makes streaming files simple, bypassing V8 memory bottlenecks by piping native Node streams directly to the socket with automated resource cleanup.
247
+ ---
344
248
 
345
- ### `res.download()`
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
- #### Functional Paradigm
251
+ ### File Downloads with `res.download()`
349
252
 
350
- ```typescript
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
- downloadReport: route.get('/report', {
355
- handle(req, res) {
356
- res.download('/absolute/path/to/report.pdf', 'Q4-Report.pdf')
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
- #### Class-Based (OOP) Paradigm
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.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 ... }`)
269
+ ### Stream Piping (`res.stream()` / `res.sendStream()`)
381
270
 
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.
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
- ##### Functional Paradigm
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
- streamVideo: route.get('/', {
396
- handle(req, res) {
397
- const videoStream = fs.createReadStream('/path/to/video.mp4')
398
- res.type('video/mp4')
399
- res.sendStream(videoStream)
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
- ##### Class-Based (OOP) Paradigm
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 handle(req, res) {
431
- const response = await fetch('https://api.openai.com/v1/chat/completions', {
300
+ handle: async ({ req, res }) => {
301
+ const upstream = await fetch('https://api.openai.com/v1/chat/completions', {
432
302
  method: 'POST',
433
- headers: { 'Authorization': `Bearer ${process.env.OPENAI_API_KEY}` },
434
- body: JSON.stringify({ model: 'gpt-4o', stream: true, messages: req.body.messages }),
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(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
- })
315
+ res.stream(upstream.body!)
316
+ },
317
+ }),
485
318
  })
486
319
  ```
487
320
 
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'
321
+ #### 3. Automatic Return Stream Piping
492
322
 
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
- }
323
+ If a route handler returns a `Readable` stream directly, ExisJS detects and pipes it automatically:
503
324
 
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"
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
- 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
- })
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
- ## 5. Redirects
340
+ ## Server-Sent Events (SSE)
546
341
 
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.
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
- liveEvents: route.get('/', {
623
- handle(req, res) {
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-update', data: { symbol: 'BTC', price: 95000 } })
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
- #### Class-Based (OOP) Paradigm
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
- #### Functional Paradigm
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
- chat: route.post('/', {
674
- handle(req, res) {
374
+ streamTokens: route.post('/', {
375
+ handle: ({ res }) => {
675
376
  res.sse(async (sse) => {
676
- // Stream directly from an async generator or AI SDK
677
- async function* generateTokens() {
678
- yield 'Hello'
679
- yield ' from'
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(generateTokens(), (token) => ({
685
- event: 'delta',
686
- data: { text: token }
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
- #### Class-Based (OOP) Paradigm
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
- - `res.sse(options?)`: Options include:
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.