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.
Files changed (116) hide show
  1. package/README.md +195 -39
  2. package/dist/config/types.d.ts +2 -2
  3. package/dist/config/types.d.ts.map +1 -1
  4. package/dist/decorators/params.d.ts +21 -0
  5. package/dist/decorators/params.d.ts.map +1 -1
  6. package/dist/decorators/params.js +21 -1
  7. package/dist/decorators/params.js.map +1 -1
  8. package/dist/di/container.d.ts +15 -6
  9. package/dist/di/container.d.ts.map +1 -1
  10. package/dist/di/container.js +26 -3
  11. package/dist/di/container.js.map +1 -1
  12. package/dist/di/index.d.ts +1 -1
  13. package/dist/di/index.d.ts.map +1 -1
  14. package/dist/di/index.js.map +1 -1
  15. package/dist/di/inject.d.ts +2 -1
  16. package/dist/di/inject.d.ts.map +1 -1
  17. package/dist/di/inject.js +2 -1
  18. package/dist/di/inject.js.map +1 -1
  19. package/dist/docs/BENCHMARKS.md +4 -4
  20. package/dist/docs/content/README.md +43 -44
  21. package/dist/docs/content/boundaries.mdx +138 -61
  22. package/dist/docs/content/cli/commands.mdx +151 -128
  23. package/dist/docs/content/cli/overview.mdx +61 -47
  24. package/dist/docs/content/cli/usage.mdx +107 -30
  25. package/dist/docs/content/config.mdx +145 -158
  26. package/dist/docs/content/controllers.mdx +193 -464
  27. package/dist/docs/content/cron.mdx +183 -69
  28. package/dist/docs/content/decorators.mdx +449 -154
  29. package/dist/docs/content/errors.mdx +202 -305
  30. package/dist/docs/content/exisjs-mcp.mdx +59 -49
  31. package/dist/docs/content/installation.mdx +243 -157
  32. package/dist/docs/content/introduction.mdx +220 -48
  33. package/dist/docs/content/middleware.mdx +171 -497
  34. package/dist/docs/content/modules.mdx +141 -83
  35. package/dist/docs/content/providers.mdx +216 -180
  36. package/dist/docs/content/requests.mdx +168 -488
  37. package/dist/docs/content/responses.mdx +200 -533
  38. package/dist/docs/content/routing.mdx +235 -598
  39. package/dist/docs/content/security.mdx +233 -57
  40. package/dist/docs/content/structure.mdx +151 -240
  41. package/dist/docs/content/swagger.mdx +192 -91
  42. package/dist/docs/content/testing.mdx +205 -74
  43. package/dist/docs/content/validation.mdx +479 -341
  44. package/dist/docs/reference/README.md +34 -2
  45. package/dist/docs/reference/config.mdx +277 -75
  46. package/dist/docs/reference/context.mdx +197 -106
  47. package/dist/docs/reference/cron.mdx +266 -152
  48. package/dist/docs/reference/database.mdx +135 -53
  49. package/dist/docs/reference/decorators.mdx +234 -116
  50. package/dist/docs/reference/di.mdx +146 -112
  51. package/dist/docs/reference/error.mdx +155 -178
  52. package/dist/docs/reference/exis.mdx +141 -70
  53. package/dist/docs/reference/logger.mdx +110 -126
  54. package/dist/docs/reference/middleware.mdx +235 -158
  55. package/dist/docs/reference/plugin.mdx +136 -64
  56. package/dist/docs/reference/router.mdx +275 -293
  57. package/dist/docs/reference/sanitize.mdx +91 -87
  58. package/dist/docs/reference/swagger.mdx +280 -100
  59. package/dist/docs/reference/testing.mdx +223 -89
  60. package/dist/docs/reference/validator.mdx +194 -206
  61. package/dist/lib/start-server.js +3 -8
  62. package/dist/lib/start-server.js.map +1 -1
  63. package/dist/module/module.d.ts +6 -3
  64. package/dist/module/module.d.ts.map +1 -1
  65. package/dist/module/module.js +17 -3
  66. package/dist/module/module.js.map +1 -1
  67. package/dist/router/controller-registrar.d.ts.map +1 -1
  68. package/dist/router/controller-registrar.js +26 -4
  69. package/dist/router/controller-registrar.js.map +1 -1
  70. package/dist/server/app.d.ts +3 -1
  71. package/dist/server/app.d.ts.map +1 -1
  72. package/dist/server/app.js +24 -8
  73. package/dist/server/app.js.map +1 -1
  74. package/dist/server/request.d.ts +2 -0
  75. package/dist/server/request.d.ts.map +1 -1
  76. package/dist/server/request.js +11 -0
  77. package/dist/server/request.js.map +1 -1
  78. package/dist/swagger/generator.d.ts.map +1 -1
  79. package/dist/swagger/generator.js +130 -10
  80. package/dist/swagger/generator.js.map +1 -1
  81. package/dist/telemetry/index.d.ts +12 -0
  82. package/dist/telemetry/index.d.ts.map +1 -0
  83. package/dist/telemetry/index.js +121 -0
  84. package/dist/telemetry/index.js.map +1 -0
  85. package/dist/testing/client.d.ts +1 -0
  86. package/dist/testing/client.d.ts.map +1 -1
  87. package/dist/testing/client.js +4 -1
  88. package/dist/testing/client.js.map +1 -1
  89. package/dist/testing/index.d.ts.map +1 -1
  90. package/dist/testing/index.js +11 -4
  91. package/dist/testing/index.js.map +1 -1
  92. package/dist/validator/builder.d.ts +26 -1
  93. package/dist/validator/builder.d.ts.map +1 -1
  94. package/dist/validator/builder.js +31 -0
  95. package/dist/validator/builder.js.map +1 -1
  96. package/dist/validator/engine.d.ts +23 -0
  97. package/dist/validator/engine.d.ts.map +1 -1
  98. package/dist/validator/engine.js +228 -31
  99. package/dist/validator/engine.js.map +1 -1
  100. package/dist/validator/index.d.ts +8 -4
  101. package/dist/validator/index.d.ts.map +1 -1
  102. package/dist/validator/index.js +4 -1
  103. package/dist/validator/index.js.map +1 -1
  104. package/dist/validator/tex-types.d.ts +15 -0
  105. package/dist/validator/tex-types.d.ts.map +1 -1
  106. package/dist/validator/tex-types.js +102 -0
  107. package/dist/validator/tex-types.js.map +1 -1
  108. package/dist/validator/tex.d.ts +1 -1
  109. package/dist/validator/tex.d.ts.map +1 -1
  110. package/dist/validator/tex.js +3 -1
  111. package/dist/validator/tex.js.map +1 -1
  112. package/dist/validator/types.d.ts +1 -0
  113. package/dist/validator/types.d.ts.map +1 -1
  114. package/package.json +46 -14
  115. package/dist/docs/content/dataloader.mdx +0 -170
  116. package/dist/docs/reference/dataloader.mdx +0 -91
@@ -1,595 +1,275 @@
1
1
  ---
2
- nextUrl: "/docs/responses"
3
- nextTitle: "Responses"
2
+ title: 'Request Object (ExisRequest)'
3
+ description: 'API reference and usage guide for ExisRequest: parameters, query strings, headers, body parsing, multipart uploads, IP resolution, and abort signals.'
4
+ nextUrl: '/docs/responses'
5
+ nextTitle: 'Response Helpers & Streaming'
4
6
  ---
5
7
 
6
- # Requests
8
+ # Request Object (`ExisRequest`)
7
9
 
8
- ExisJS wraps the native Node.js `IncomingMessage` object into an `ExisRequest` instance. It abstracts away the complexities of stream parsing, multipart form processing, and header normalization, giving you a typed interface.
10
+ ExisJS wraps the standard Node.js `IncomingMessage` in an `ExisRequest` instance. It provides parsed properties, lazy body decoding, multipart upload helpers, IP resolution behind reverse proxies, and integration with the Web standard `AbortSignal`.
9
11
 
10
12
  ---
11
13
 
12
- ## 1. Body, Query, and Params
14
+ ## Core Request Properties
15
+
16
+ | Property | Type | Description |
17
+ | :--- | :--- | :--- |
18
+ | `req.params` | `TParams` | Dynamic route parameters extracted from URL segments (e.g. `/:id`) |
19
+ | `req.query` | `TQuery` | Parsed URL query parameters (e.g. `?page=1&limit=20`) |
20
+ | `req.body` | `TBody` | Parsed request body payload (JSON, URL-encoded, or multipart fields) |
21
+ | `req.headers` | `IncomingHttpHeaders` | Incoming HTTP headers map (case-insensitive keys) |
22
+ | `req.cookies` | `Record<string, string>` | Parsed request cookies dictionary |
23
+ | `req.file` | `ExisFile` | First uploaded file from multipart form data |
24
+ | `req.files` | `ExisFile[]` | All uploaded files from multipart form data |
25
+ | `req.ip` | `string` | Resolved client IP address (evaluates trusted proxy headers) |
26
+ | `req.ips` | `string[]` | Array of IP addresses when traversed through proxy hops |
27
+ | `req.hostname` | `string` | Resolved hostname (from `Host` or `X-Forwarded-Host`) |
28
+ | `req.protocol` | `'http' \| 'https'` | Request protocol |
29
+ | `req.secure` | `boolean` | `true` when request is over HTTPS (`req.protocol === 'https'`) |
30
+ | `req.method` | `string` | HTTP method verb (`GET`, `POST`, `PUT`, etc.) |
31
+ | `req.path` | `string` | Normalized pathname without query parameters |
32
+ | `req.originalUrl` | `string` | Raw URL string including query parameters |
33
+ | `req.signal` | `AbortSignal` | Standard `AbortSignal` triggered when client cancels connection |
34
+ | `req.fresh` | `boolean` | `true` if client cache matches current `ETag` or `Last-Modified` |
35
+ | `req.stale` | `boolean` | Inverse of `req.fresh` |
36
+ | `req.user` | `ExisUser` | Authenticated user identity attached by auth guards |
37
+ | `req.session` | `Record<string, any>` | Session data store when session middleware is enabled |
38
+ | `req.log` | `Logger` | Request-scoped Pino logger with trace metadata |
39
+ | `req.requestId` | `string` | Unique request identifier header |
40
+ | `req.raw` | `IncomingMessage` | Underlying Node.js `IncomingMessage` stream |
13
41
 
14
- The most common request properties are automatically parsed and strongly typed (especially when using the [Schema Validation](/docs/middleware#8-schema-validation) middleware).
42
+ ---
43
+
44
+ ## Accessing Request Data
45
+
46
+ ### In Functional Routes
15
47
 
16
- - **`req.body`**: The parsed body of the request (JSON, URL-encoded, or Multipart).
17
- - **`req.query`**: An object containing the URL query string parameters (e.g., `?id=123`).
18
- - **`req.params`**: An object containing route parameters (e.g., `/:id`).
48
+ In functional route handlers, `req` is available directly on the handler context alongside destructured convenience properties (`body`, `query`, `params`, `headers`):
19
49
 
20
- #### Functional Paradigm
50
+ ```typescript filename="src/http/users/route.ts"
51
+ import { controller, route } from 'exisjs/router'
21
52
 
22
- ```typescript filename="src/http/route.ts"
23
53
  export default controller({
24
- updateUser: route.post('/users/:id', {
25
- handle(req, res) {
26
- const id = req.params.id // "123"
27
- const force = req.query.force // "true"
28
- const payload = req.body // { email: "alice@exisjs.com" }
29
- }
30
- })
54
+ updateUser: route.put('/:id', {
55
+ handle: async ({ params, query, body, req }) => {
56
+ const id = params.id
57
+ const force = query.force === 'true'
58
+ const clientIp = req.ip
59
+
60
+ return { id, force, clientIp, updated: body }
61
+ },
62
+ }),
31
63
  })
32
64
  ```
33
65
 
34
- #### Class-Based (OOP) Paradigm
66
+ ### In Class-Based (OOP) Controllers
67
+
68
+ In OOP controllers, access `req` using the `@Req()` decorator or extract specific properties using `@Param()`, `@Query()`, `@Body()`, and `@Headers()`:
35
69
 
36
- ```typescript filename="src/http/route.ts"
37
- import { Controller, Post, Param, Query, Body } from 'exisjs/decorators'
70
+ ```typescript filename="src/http/users/route.ts"
71
+ import { Controller, Put, Param, Query, Body, Req } from 'exisjs/decorators'
72
+ import type { Request } from 'exisjs/router'
38
73
 
39
74
  @Controller()
40
75
  export default class UsersController {
41
- @Post('/users/:id')
76
+ @Put('/:id')
42
77
  async updateUser(
43
78
  @Param('id') id: string,
44
79
  @Query('force') force: string,
45
- @Body() payload: any
80
+ @Body() body: any,
81
+ @Req() req: Request
46
82
  ) {
47
- // Variables are automatically injected
83
+ const clientIp = req.ip
84
+ return { id, force: force === 'true', clientIp, updated: body }
48
85
  }
49
86
  }
50
87
  ```
51
88
 
52
89
  ---
53
90
 
54
- ## 2. On-Demand Parsing
91
+ ## On-Demand Body Parsing & Raw Streams
55
92
 
56
- To maximize performance, ExisJS does not parse the request body immediately for every route. If you aren't using the Validation middleware, you can manually trigger parsing on-demand.
57
-
58
- #### Functional Paradigm
93
+ When dealing with webhooks (e.g. Stripe, GitHub), HMAC signature verification, or custom binary payloads, you can access the unparsed stream directly:
59
94
 
60
95
  ```typescript
61
- export default controller({
62
- processData: route.post('/process', {
63
- async handle(req, res) {
64
- // Parses application/json
65
- const data = await req.json<{ key: string }>()
66
-
67
- // Parses raw text (text/plain)
68
- const text = await req.text()
69
- }
70
- })
71
- })
72
- ```
96
+ // 1. Read raw binary Buffer (ideal for webhook signature verification)
97
+ const rawBuffer = await req.buffer()
73
98
 
74
- #### Class-Based (OOP) Paradigm
99
+ // 2. Read raw UTF-8 string
100
+ const rawText = await req.text()
75
101
 
76
- ```typescript filename="src/http/route.ts"
77
- import { Controller, Post, Req } from 'exisjs/decorators'
102
+ // 3. Parse JSON body on demand
103
+ const data = await req.json<{ username: string }>()
78
104
 
79
- @Controller()
80
- export default class ProcessController {
81
- @Post('/process')
82
- async processData(@Req() req: any) {
83
- // Parses application/json
84
- const data = await req.json<{ key: string }>()
85
-
86
- // Parses raw text (text/plain)
87
- const text = await req.text()
88
- }
89
- }
105
+ // 4. Parse multipart/form-data or x-www-form-urlencoded
106
+ const { fields, files } = await req.formData()
90
107
  ```
91
108
 
92
- ---
93
-
94
- ## 3. File Uploads & Multipart Form Data
109
+ ### Webhooks with `@RawBody()` (Class Controllers)
95
110
 
96
- Handling `multipart/form-data` in Node.js can be difficult. ExisJS provides multiple robust, high-performance approaches:
111
+ In class-based controllers, use the `@RawBody()` parameter decorator to inject the raw `Buffer` or decoded string directly:
97
112
 
98
- ### A. Declarative Route Uploads (Recommended)
113
+ ```typescript filename="src/http/webhooks/route.ts"
114
+ import { Controller, Post, RawBody, Headers } from 'exisjs/decorators'
99
115
 
100
- You can declare file upload requirements directly inside your route schema or decorators. ExisJS automatically executes the multipart parser, validates MIME types, enforces byte caps, and populates `req.file` or `req.files` along with non-file fields in `req.body`:
101
-
102
- #### Functional Paradigm
103
-
104
- ```typescript filename="src/http/upload/route.ts"
105
- import { controller, route } from 'exisjs/router'
106
- import { tex } from 'exisjs/validator'
107
-
108
- export default controller({
109
- uploadPhoto: route.post('/', {
110
- upload: {
111
- field: 'photo',
112
- maxBytes: 10 * 1024 * 1024, // 10MB limit
113
- mimeTypes: ['image/jpeg', 'image/png', 'image/webp'],
114
- },
115
- body: tex.object({
116
- title: tex.string({ min: 1 }),
117
- }),
118
- handle({ req }) {
119
- const file = req.file! // Typed UploadedFile
120
- console.log(`Uploaded ${file.originalname} (${file.size} bytes)`)
121
-
122
- return {
123
- success: true,
124
- filename: file.filename,
125
- mimetype: file.mimetype,
126
- title: req.body.title,
127
- }
128
- }
129
- })
130
- })
131
- ```
132
-
133
- #### Class-Based (OOP) Paradigm
134
-
135
- ```typescript filename="src/http/upload/route.ts"
136
- import { Controller, Post, UseUpload, UploadedFile, Body } from 'exisjs/decorators'
137
- import { tex } from 'exisjs/validator'
138
- import type { UploadedFile as IUploadedFile } from 'exisjs/router'
139
-
140
- const TitleSchema = tex.object({
141
- title: tex.string({ min: 1 }),
142
- })
143
-
144
- @Controller('/upload')
145
- export default class UploadController {
146
- @Post('/')
147
- @UseUpload({
148
- field: 'photo',
149
- maxBytes: 10 * 1024 * 1024,
150
- mimeTypes: ['image/jpeg', 'image/png', 'image/webp'],
151
- })
152
- async uploadPhoto(
153
- @UploadedFile() file: IUploadedFile,
154
- @Body(TitleSchema) body: { title: string }
116
+ @Controller('/webhooks')
117
+ export default class WebhookController {
118
+ @Post('/stripe')
119
+ async handleStripe(
120
+ @RawBody() payload: Buffer,
121
+ @Headers('stripe-signature') sig: string
155
122
  ) {
156
- console.log(`Uploaded ${file.originalname} (${file.size} bytes)`)
157
-
158
- return {
159
- success: true,
160
- filename: file.filename,
161
- mimetype: file.mimetype,
162
- title: body.title,
163
- }
164
- }
165
- }
166
- ```
167
-
168
- ### B. First-Party `fileUpload` Middleware
169
-
170
- ExisJS exports a dedicated `fileUpload` middleware from `exisjs/middleware` supporting single, multiple, and multi-field uploads:
171
-
172
- #### Functional Paradigm
173
-
174
- ```typescript filename="src/http/documents/route.ts"
175
- import { controller, route } from 'exisjs/router'
176
- import { fileUpload } from 'exisjs/middleware'
177
-
178
- export default controller({
179
- // Single file upload
180
- single: route.post('/single',
181
- fileUpload.single('document', {
182
- maxBytes: 15 * 1024 * 1024, // 15MB
183
- dest: './uploads/docs',
184
- }),
185
- ({ req }) => {
186
- return { file: req.file }
187
- }
188
- ),
189
-
190
- // Multiple files with the same field name
191
- gallery: route.post('/gallery',
192
- fileUpload.array('photos', 5, {
193
- mimeTypes: ['image/*'],
194
- }),
195
- ({ req }) => {
196
- const files = req.files // UploadedFile[]
197
- return { count: files?.length }
198
- }
199
- ),
200
-
201
- // Multiple distinct fields
202
- profile: route.post('/profile',
203
- fileUpload.fields([
204
- { name: 'avatar', maxCount: 1 },
205
- { name: 'cover', maxCount: 1 },
206
- ]),
207
- ({ req }) => {
208
- // req.files is a dictionary Record<string, UploadedFile[]>
209
- return { files: req.files }
210
- }
211
- ),
212
- })
213
- ```
214
-
215
- #### Class-Based (OOP) Paradigm
216
-
217
- ```typescript filename="src/http/documents/route.ts"
218
- import { Controller, Post, Use, UploadedFile, UploadedFiles } from 'exisjs/decorators'
219
- import { fileUpload } from 'exisjs/middleware'
220
- import type { UploadedFile as IUploadedFile } from 'exisjs/router'
221
-
222
- @Controller('/documents')
223
- export default class DocumentsController {
224
- // Single file upload
225
- @Post('/single')
226
- @Use(fileUpload.single('document', { maxBytes: 15 * 1024 * 1024, dest: './uploads/docs' }))
227
- async uploadSingle(@UploadedFile() file: IUploadedFile) {
228
- return { file }
229
- }
230
-
231
- // Multiple files with the same field name
232
- @Post('/gallery')
233
- @Use(fileUpload.array('photos', 5, { mimeTypes: ['image/*'] }))
234
- async uploadGallery(@UploadedFiles() files: IUploadedFile[]) {
235
- return { count: files.length, files }
236
- }
237
-
238
- // Multiple distinct fields
239
- @Post('/profile')
240
- @Use(fileUpload.fields([{ name: 'avatar', maxCount: 1 }, { name: 'cover', maxCount: 1 }]))
241
- async uploadProfile(@UploadedFiles() files: Record<string, IUploadedFile[]>) {
242
- return { files }
243
- }
244
- }
245
- ```
246
-
247
- ### C. Integrating Express/Connect Middleware (e.g. Multer)
248
-
249
- If your enterprise project relies on existing Express middleware like **Multer** (for custom storage engines, S3 direct-to-cloud streams, or specialized filters), ExisJS provides the `fromExpress()` bridge:
250
-
251
- #### Functional Paradigm
252
-
253
- ```typescript filename="src/http/legacy/route.ts"
254
- import { controller, route } from 'exisjs/router'
255
- import { fromExpress } from 'exisjs/middleware'
256
- import multer from 'multer'
257
-
258
- const upload = multer({
259
- storage: multer.memoryStorage(),
260
- limits: { fileSize: 10 * 1024 * 1024 },
261
- })
262
-
263
- export default controller({
264
- upload: route.post('/',
265
- fromExpress(upload.single('file')),
266
- ({ req }) => {
267
- // Access multer's req.file or req.body seamlessly
268
- const file = (req.raw as any).file || req.file
269
- return { success: true, originalname: file?.originalname }
270
- }
271
- )
272
- })
273
- ```
274
-
275
- #### Class-Based (OOP) Paradigm
276
-
277
- ```typescript filename="src/http/legacy/route.ts"
278
- import { Controller, Post, Use, Req } from 'exisjs/decorators'
279
- import { fromExpress } from 'exisjs/middleware'
280
- import type { Request } from 'exisjs/router'
281
- import multer from 'multer'
282
-
283
- const upload = multer({
284
- storage: multer.memoryStorage(),
285
- limits: { fileSize: 10 * 1024 * 1024 },
286
- })
287
-
288
- @Controller('/legacy')
289
- export default class LegacyUploadController {
290
- @Post('/upload')
291
- @Use(fromExpress(upload.single('file')))
292
- async upload(@Req() req: Request) {
293
- const file = (req.raw as any).file || req.file
294
- return { success: true, originalname: file?.originalname }
295
- }
296
- }
297
- ```
298
-
299
- ### D. Manual `req.formData()` & Streaming Uploads
300
-
301
- For manual control over incoming multipart streams without route middleware:
302
-
303
- #### Functional Paradigm
304
-
305
- ```typescript filename="src/http/upload/route.ts"
306
- import { controller, route } from 'exisjs/router'
307
-
308
- export default controller({
309
- uploadAvatar: route.post('/avatar', {
310
- async handle({ req }) {
311
- // Parse on-demand
312
- const { fields, files } = await req.formData()
313
- const uploadedFile = files['profile_pic']
314
-
315
- if (uploadedFile) {
316
- // Save to disk safely with automated collision-free naming
317
- const savedPath = await uploadedFile.saveToDisk('./uploads/avatars')
318
- return { success: true, path: savedPath }
319
- }
320
-
321
- return { success: false, error: 'No file uploaded' }
322
- }
323
- })
324
- })
325
- ```
326
-
327
- #### Class-Based (OOP) Paradigm
328
-
329
- ```typescript filename="src/http/upload/route.ts"
330
- import { Controller, Post, Req } from 'exisjs/decorators'
331
- import type { Request } from 'exisjs/router'
332
-
333
- @Controller('/upload')
334
- export default class ManualUploadController {
335
- @Post('/avatar')
336
- async uploadAvatar(@Req() req: Request) {
337
- const { fields, files } = await req.formData()
338
- const uploadedFile = files['profile_pic']
339
-
340
- if (uploadedFile) {
341
- const savedPath = await uploadedFile.saveToDisk('./uploads/avatars')
342
- return { success: true, path: savedPath }
343
- }
344
-
345
- return { success: false, error: 'No file uploaded' }
123
+ return stripe.webhooks.constructEvent(payload, sig, process.env.STRIPE_SECRET!)
346
124
  }
347
125
  }
348
126
  ```
349
127
 
350
128
  ---
351
129
 
352
- ## 4. Body Size Limits & HTTP 413
130
+ ## Headers & Content Negotiation
353
131
 
354
- ExisJS comes pre-configured with a generous **10MB default body limit** (`10485760` bytes) across JSON, form-data, and raw payloads, preventing premature drops during photo or document uploads.
132
+ ExisJS provides helper methods for header inspection and RFC content negotiation:
355
133
 
356
- If a client sends a payload exceeding the configured threshold, ExisJS halts parsing immediately and responds with `413 Payload Too Large`:
134
+ ```typescript
135
+ // Case-insensitive header retrieval
136
+ const auth = req.get('Authorization') // or req.header('authorization')
357
137
 
358
- ```json
359
- {
360
- "statusCode": 413,
361
- "error": "Payload Too Large",
362
- "message": "Request payload exceeded size limit of 10485760 bytes. Increase server.bodyLimit in exis.config.ts to allow larger payloads."
138
+ // Check Content-Type
139
+ if (req.is('json')) {
140
+ // Content-Type contains application/json
363
141
  }
364
- ```
365
142
 
366
- To configure custom limits globally across your app, update `exis.config.ts`:
367
-
368
- ```typescript filename="exis.config.ts"
369
- export default {
370
- server: {
371
- // Set custom maximum payload limit in bytes (e.g. 50MB)
372
- bodyLimit: 50 * 1024 * 1024,
373
- }
374
- }
143
+ // Content negotiation - returns best supported match
144
+ const format = req.accepts('json', 'html', 'text')
145
+ const language = req.acceptsLanguages('en', 'es', 'de')
375
146
  ```
376
147
 
377
148
  ---
378
149
 
379
- ## 5. Dynamic Request Timeouts
380
-
381
- ExisJS ships with a **60-second default request timeout** in its `timeout()` middleware, offering ample headroom for slow network clients and third-party cloud operations (Cloudinary, AWS S3, OpenAI, Stripe).
382
-
383
- ### Dynamic In-Handler Control
384
-
385
- Handlers can dynamically extend or cancel the active timeout on the fly using `req.setTimeout()` and `req.clearTimeout()`, or declaratively via `@Timeout(ms)`:
386
-
387
- #### Functional Paradigm
388
-
389
- ```typescript filename="src/http/ai/route.ts"
390
- import { controller, route } from 'exisjs/router'
391
-
392
- export default controller({
393
- generateVideo: route.post('/', {
394
- timeoutMs: 180000, // 1. Declarative 3-minute timeout for this endpoint
395
- async handle({ req }) {
396
- // 2. Or dynamically adjust timeout during processing
397
- req.setTimeout(240000)
398
-
399
- try {
400
- const result = await longRunningAiTask()
401
- return { success: true, result }
402
- } finally {
403
- req.clearTimeout()
404
- }
405
- }
406
- })
407
- })
408
- ```
150
+ ## Proxy & IP Address Resolution
409
151
 
410
- #### Class-Based (OOP) Paradigm
152
+ When running behind reverse proxies, load balancers, or CDNs (such as AWS ALB, Cloudflare, NGINX, or Render), configure `trustProxy` in `exis.config.ts`:
411
153
 
412
- ```typescript filename="src/http/ai/route.ts"
413
- import { Controller, Post, Timeout, Req } from 'exisjs/decorators'
414
- import type { Request } from 'exisjs/router'
154
+ ```typescript filename="exis.config.ts"
155
+ import type { ExisConfig } from 'exisjs/config'
415
156
 
416
- @Controller('/ai')
417
- export default class AiTaskController {
418
- @Post('/generate-video')
419
- @Timeout(180000) // 1. Declarative 3-minute timeout
420
- async generateVideo(@Req() req: Request) {
421
- // 2. Or dynamically adjust on the fly
422
- req.setTimeout(240000)
423
-
424
- try {
425
- const result = await longRunningAiTask()
426
- return { success: true, result }
427
- } finally {
428
- req.clearTimeout()
429
- }
430
- }
157
+ const config: ExisConfig = {
158
+ trustProxy: true, // Evaluates CF-Connecting-IP, X-Real-IP, and X-Forwarded-For
431
159
  }
432
- ```
433
160
 
434
- ---
435
-
436
- ## 6. HTTP Headers & Content Negotiation
437
-
438
- ```typescript
439
- // Get a specific header (case-insensitive)
440
- const authHeader = req.get('Authorization')
441
- // or
442
- const authHeader = req.header('authorization')
443
-
444
- // Check the Content-Type
445
- if (req.is('application/json')) { ... }
446
-
447
- // Content Negotiation (returns the best match)
448
- const format = req.accepts('html', 'json', 'text')
449
- const language = req.acceptsLanguages('en', 'es', 'fr')
161
+ export default config
450
162
  ```
451
163
 
452
- ---
453
-
454
- ## 7. Network Information & Reverse Proxy IP Resolution
455
-
456
- ExisJS handles reverse proxies, CDNs, and dual-stack IP networks automatically. When deployed behind Cloudflare, AWS ALB, NGINX, Render, or GCP, set `trustProxy: true` in your `exis.config.ts` or define `TRUST_PROXY=true` in your environment variables.
457
-
458
- When proxy trust is enabled, ExisJS evaluates industry-standard proxy and CDN headers in priority order:
459
- 1. `CF-Connecting-IP` (Cloudflare)
460
- 2. `True-Client-IP` (Akamai / Cloudflare Enterprise)
461
- 3. `X-Real-IP` / `X-Client-IP` (NGINX / Reverse Proxies)
462
- 4. `X-Forwarded-For` (Standard multi-hop proxy chains)
463
-
464
- - **`req.ip`**: The real client's IP address. Automatically strips proxy hops, normalizes IPv6-mapped IPv4 dual-stack addresses (e.g., `::ffff:192.168.1.50` $\to$ `192.168.1.50`), and converts IPv6 loopback `::1` $\to$ `127.0.0.1` for consistent rate limiting and geo-IP lookups.
465
- - **`req.ips`**: An array of IPs if multiple proxies were traversed, each normalized cleanly.
466
- - **`req.hostname`**: The resolved hostname (from `Host` or `X-Forwarded-Host`).
467
- - **`req.protocol`**: Returns `'http'` or `'https'`.
468
- - **`req.secure`**: A boolean (true if `req.protocol === 'https'`).
164
+ When enabled:
165
+ - `req.ip` automatically resolves the client IP, normalizes IPv6 loopback (`::1` $\to$ `127.0.0.1`), and strips IPv4-mapped IPv6 prefixes (`::ffff:192.168.1.1` $\to$ `192.168.1.1`).
166
+ - `req.ips` provides the complete upstream proxy chain.
167
+ - `req.protocol` resolves to `'https'` when `X-Forwarded-Proto: https` is present.
469
168
 
470
169
  ---
471
170
 
472
- ## 8. Request Cancellation & Abort Signals (`req.signal`)
473
-
474
- ExisJS provides a first-class standard Web API `req.signal: AbortSignal` tied directly to the underlying HTTP connection lifecycle.
171
+ ## Connection Cancellation & `req.signal`
475
172
 
476
- When a client closes their browser tab, cancels a download, or aborts an HTTP request, `req.signal` immediately triggers an `abort` event. You can pass `req.signal` directly to ORMs, database drivers, and HTTP clients to cancel expensive in-flight operations and eliminate wasted server resources.
173
+ ExisJS links `req.signal` to the lifecycle of the incoming HTTP connection. If a client closes their browser, navigates away, or aborts an HTTP request, `req.signal` fires an abort event.
477
174
 
478
- #### Functional Paradigm
175
+ Pass `req.signal` to downstream database queries or outgoing HTTP calls to avoid running orphaned operations:
479
176
 
480
177
  ```typescript filename="src/http/reports/route.ts"
481
178
  import { controller, route } from 'exisjs/router'
482
179
 
483
180
  export default controller({
484
- heavyReport: route.get('/', {
485
- async handle({ req }) {
486
- // 1. Pass req.signal to fetch() — if the client leaves, fetch aborts immediately
487
- const upstream = await fetch('https://api.external.com/data', {
488
- signal: req.signal
181
+ exportReport: route.get('/', {
182
+ handle: async ({ req }) => {
183
+ // 1. Pass signal to fetch calls
184
+ const upstream = await fetch('https://analytics.internal/data', {
185
+ signal: req.signal,
489
186
  })
490
- const payload = await upstream.json()
187
+ const data = await upstream.json()
491
188
 
492
- // 2. Pass req.signal to your database queries (Prisma, Drizzle, pg, etc.)
493
- const results = await db.query(sql`SELECT * FROM heavy_analytics`, {
494
- signal: req.signal
189
+ // 2. Pass signal to database queries
190
+ const report = await db.query('SELECT * FROM large_dataset', {
191
+ signal: req.signal,
495
192
  })
496
193
 
497
- return { payload, results }
498
- }
499
- })
194
+ return { data, report }
195
+ },
196
+ }),
500
197
  })
501
198
  ```
502
199
 
503
- #### Class-Based (OOP) Paradigm
200
+ ---
504
201
 
505
- ```typescript filename="src/http/reports/route.ts"
506
- import { Controller, Get, Req } from 'exisjs/decorators'
507
- import type { Request } from 'exisjs/router'
202
+ ## Request Timeouts
508
203
 
509
- @Controller('/reports')
510
- export default class ReportsController {
511
- @Get('/')
512
- async heavyReport(@Req() req: Request) {
513
- // 1. Pass req.signal to fetch()
514
- const upstream = await fetch('https://api.external.com/data', {
515
- signal: req.signal,
516
- })
517
- const payload = await upstream.json()
518
-
519
- // 2. Pass req.signal to your database queries
520
- const results = await db.query(sql`SELECT * FROM heavy_analytics`, {
521
- signal: req.signal,
522
- })
523
-
524
- return { payload, results }
525
- }
526
- }
527
- ```
204
+ Dynamic timeout controls allow extending or resetting execution limits for long-running batch operations:
528
205
 
529
- ---
206
+ ```typescript
207
+ // Set active request timeout to 3 minutes
208
+ req.setTimeout(180000)
530
209
 
531
- ## 9. Context & Metadata
210
+ // Clear the active timeout
211
+ req.clearTimeout()
212
+ ```
532
213
 
533
- As your request moves through ExisJS Middlewares, various properties are attached to `req` for your convenience:
214
+ ---
534
215
 
535
- - **`req.log`**: A request-scoped instance of the Pino logger (via `requestLogger`).
536
- - **`req.requestId`**: The unique Trace ID (via `requestId`).
537
- - **`req.user`**: The authenticated user payload (via `JWT` auth middleware).
538
- - **`req.session`**: The stateful session object (via `session` middleware).
539
- - **`req.fresh`**: Returns `true` if the client's cache is still valid (ETag match), meaning you can send a 304 response.
216
+ ## Custom Context Typing (`TContext`)
540
217
 
541
- ### Inline Context Typing (Per-Route)
218
+ Middlewares can attach custom data to the request. You can strongly type this context either per-route or globally:
542
219
 
543
- If you don't want to create a globally typed router with `createRouter`, ExisJS natively types `req.user` as `any` by default so you never get annoying compiler errors (e.g. `req.user.userId` just works!).
220
+ ### Per-Route Generic Context
544
221
 
545
- However, if you want **strict** type safety on a specific route without global configuration, you can pass your custom context directly to the route's 4th generic parameter:
222
+ ```typescript filename="src/http/admin/route.ts"
223
+ import { controller, route } from 'exisjs/router'
546
224
 
547
- ```typescript
548
- import { route } from "exisjs/router";
225
+ interface AdminContext {
226
+ adminUser: { id: string; role: 'superadmin' | 'moderator' }
227
+ }
549
228
 
550
- export default route.get<any, any, any, { user: { userId: string } }>("/", {
551
- async handle({ req }) {
552
- // Typed. No "as any" needed.
553
- const id = req.user.userId;
554
- }
229
+ export default controller({
230
+ dashboard: route.get<unknown, Record<string, string>, Record<string, string>, AdminContext>('/', {
231
+ handle: async ({ adminUser, req }) => {
232
+ return { activeAdmin: adminUser.id }
233
+ },
234
+ }),
555
235
  })
556
236
  ```
557
237
 
558
- ### Strongly-Typed Context & Native Destructuring (App-Wide)
559
-
560
- To provide a better developer experience, ExisJS allows you to define a **custom typed context** for your application. This completely eliminates the need for global `.d.ts` augmentations and `as any` casts.
238
+ ### App-Wide Context with `createRouter`
561
239
 
562
- To do this, create a custom router factory (e.g., `src/http/router.ts`) and define the exact shape of your application's context (like the user object, workspace, tenant ID, etc).
240
+ Define a typed router factory to eliminate type assertions across your entire project:
563
241
 
564
242
  ```typescript filename="src/http/router.ts"
565
- import { createRouter } from "exisjs/router";
243
+ import { createRouter } from 'exisjs/router'
566
244
 
567
- // 1. Define everything your middlewares will attach to the request
568
- interface AppContext {
569
- user: { id: string; role: string };
570
- workspace: string;
245
+ export interface AppContext {
246
+ user: { id: string; email: string }
247
+ tenantId: string
571
248
  }
572
249
 
573
- // 2. Export a custom typed route and controller
574
- export const { route, controller } = createRouter<AppContext>();
250
+ export const { route, controller } = createRouter<AppContext>()
575
251
  ```
576
252
 
577
- Now, instead of importing from `exisjs/router`, import from your **local** router. Your handlers can natively destructure your custom properties.
253
+ Use your local router in route files to automatically infer injected context:
578
254
 
579
- ```typescript filename="src/http/users/route.ts"
580
- import { route, controller } from "../../router.ts";
255
+ ```typescript filename="src/http/profile/route.ts"
256
+ import { controller, route } from '../router'
581
257
 
582
258
  export default controller({
583
- getProfile: route.get("/", {
584
- async handle({ user, workspace }) {
585
- // Strongly typed.
586
- // No more `req.user`, no more `as any`.
587
- console.log(user.role, workspace);
588
- }
589
- })
590
- });
259
+ getProfile: route.get('/', {
260
+ handle: async ({ user, tenantId }) => {
261
+ // user and tenantId are strongly typed without casting
262
+ return { user, tenantId }
263
+ },
264
+ }),
265
+ })
591
266
  ```
592
267
 
593
- <Warning>
594
- **Important Middleware Rule:** In order for destructuring to work (e.g. `handle({ user })`), the `user` property MUST be populated **before** the handler executes. This means you must populate it inside a `middleware` instead of calling an inline function inside the handler.
595
- </Warning>
268
+ ---
269
+
270
+ ## Next Steps
271
+
272
+ - **[Response Helpers & Streaming](/docs/responses)**: Formatting JSON envelopes, custom headers, cookies, and SSE.
273
+ - **[Validation Guide](/docs/validation)**: Building validation schemas with `tex`.
274
+ - **[Controllers](/docs/controllers)**: Parameter decorators and class-based controllers.
275
+ - **[Boundaries](/docs/boundaries)**: Attaching authentication context via middleware cascades.