exisjs 0.7.4 → 0.7.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/dist/cli/commands/build.d.ts +4 -2
  2. package/dist/cli/commands/build.d.ts.map +1 -1
  3. package/dist/cli/commands/build.js +80 -22
  4. package/dist/cli/commands/build.js.map +1 -1
  5. package/dist/cli/commands/dev.d.ts.map +1 -1
  6. package/dist/cli/commands/dev.js +229 -64
  7. package/dist/cli/commands/dev.js.map +1 -1
  8. package/dist/cli/commands/doctor.d.ts.map +1 -1
  9. package/dist/cli/commands/doctor.js +5 -2
  10. package/dist/cli/commands/doctor.js.map +1 -1
  11. package/dist/cli/commands/generate.d.ts +1 -0
  12. package/dist/cli/commands/generate.d.ts.map +1 -1
  13. package/dist/cli/commands/generate.js +25 -0
  14. package/dist/cli/commands/generate.js.map +1 -1
  15. package/dist/cli/commands/start.d.ts.map +1 -1
  16. package/dist/cli/commands/start.js +65 -15
  17. package/dist/cli/commands/start.js.map +1 -1
  18. package/dist/cli/index.js +20 -4
  19. package/dist/cli/index.js.map +1 -1
  20. package/dist/cli/manifest.d.ts.map +1 -1
  21. package/dist/cli/manifest.js +118 -17
  22. package/dist/cli/manifest.js.map +1 -1
  23. package/dist/cli/resolve-aliases.d.ts +1 -1
  24. package/dist/cli/resolve-aliases.d.ts.map +1 -1
  25. package/dist/cli/resolve-aliases.js +30 -13
  26. package/dist/cli/resolve-aliases.js.map +1 -1
  27. package/dist/config/config.d.ts +3 -1
  28. package/dist/config/config.d.ts.map +1 -1
  29. package/dist/config/config.js.map +1 -1
  30. package/dist/config/env.d.ts.map +1 -1
  31. package/dist/config/env.js +8 -0
  32. package/dist/config/env.js.map +1 -1
  33. package/dist/config/types.d.ts +2 -0
  34. package/dist/config/types.d.ts.map +1 -1
  35. package/dist/decorators/constants.d.ts +2 -0
  36. package/dist/decorators/constants.d.ts.map +1 -1
  37. package/dist/decorators/middleware.d.ts +65 -2
  38. package/dist/decorators/middleware.d.ts.map +1 -1
  39. package/dist/decorators/middleware.js +148 -62
  40. package/dist/decorators/middleware.js.map +1 -1
  41. package/dist/decorators/params.d.ts +5 -0
  42. package/dist/decorators/params.d.ts.map +1 -1
  43. package/dist/decorators/params.js +11 -1
  44. package/dist/decorators/params.js.map +1 -1
  45. package/dist/decorators/response.d.ts.map +1 -1
  46. package/dist/decorators/response.js +32 -22
  47. package/dist/decorators/response.js.map +1 -1
  48. package/dist/di/container.d.ts.map +1 -1
  49. package/dist/di/container.js +6 -1
  50. package/dist/di/container.js.map +1 -1
  51. package/dist/docs/content/boundaries.mdx +33 -3
  52. package/dist/docs/content/cli/commands.mdx +1 -1
  53. package/dist/docs/content/config.mdx +26 -0
  54. package/dist/docs/content/controllers.mdx +127 -10
  55. package/dist/docs/content/decorators.mdx +22 -4
  56. package/dist/docs/content/errors.mdx +38 -2
  57. package/dist/docs/content/introduction.mdx +7 -7
  58. package/dist/docs/content/middleware.mdx +269 -6
  59. package/dist/docs/content/requests.mdx +217 -10
  60. package/dist/docs/content/responses.mdx +323 -34
  61. package/dist/docs/content/routing.mdx +46 -0
  62. package/dist/docs/content/security.mdx +17 -4
  63. package/dist/docs/content/structure.mdx +91 -12
  64. package/dist/docs/content/validation.mdx +226 -11
  65. package/dist/docs/reference/context.mdx +11 -0
  66. package/dist/docs/reference/decorators.mdx +14 -4
  67. package/dist/docs/reference/error.mdx +44 -0
  68. package/dist/docs/reference/logger.mdx +175 -43
  69. package/dist/docs/reference/middleware.mdx +31 -0
  70. package/dist/docs/reference/router.mdx +22 -0
  71. package/dist/docs/reference/validator.mdx +83 -23
  72. package/dist/error/errors.d.ts.map +1 -1
  73. package/dist/error/errors.js +95 -43
  74. package/dist/error/errors.js.map +1 -1
  75. package/dist/error/index.d.ts +1 -0
  76. package/dist/error/index.d.ts.map +1 -1
  77. package/dist/error/index.js +11 -1
  78. package/dist/error/index.js.map +1 -1
  79. package/dist/error/overlay.d.ts +42 -0
  80. package/dist/error/overlay.d.ts.map +1 -0
  81. package/dist/error/overlay.js +306 -0
  82. package/dist/error/overlay.js.map +1 -0
  83. package/dist/index.d.ts +1 -0
  84. package/dist/index.d.ts.map +1 -1
  85. package/dist/index.js +22 -1
  86. package/dist/index.js.map +1 -1
  87. package/dist/lib/start-server.js +49 -7
  88. package/dist/lib/start-server.js.map +1 -1
  89. package/dist/logger/index.d.ts +1 -1
  90. package/dist/logger/index.d.ts.map +1 -1
  91. package/dist/logger/index.js +9 -1
  92. package/dist/logger/index.js.map +1 -1
  93. package/dist/middleware/guard.d.ts +2 -2
  94. package/dist/middleware/guard.d.ts.map +1 -1
  95. package/dist/middleware/guard.js.map +1 -1
  96. package/dist/middleware/ip-filter.d.ts +1 -0
  97. package/dist/middleware/ip-filter.d.ts.map +1 -1
  98. package/dist/middleware/ip-filter.js +2 -0
  99. package/dist/middleware/ip-filter.js.map +1 -1
  100. package/dist/middleware/middleware.d.ts +2 -2
  101. package/dist/middleware/middleware.d.ts.map +1 -1
  102. package/dist/middleware/middleware.js +20 -2
  103. package/dist/middleware/middleware.js.map +1 -1
  104. package/dist/middleware/rate-limit.d.ts.map +1 -1
  105. package/dist/middleware/rate-limit.js +9 -0
  106. package/dist/middleware/rate-limit.js.map +1 -1
  107. package/dist/middleware/security.d.ts +43 -1
  108. package/dist/middleware/security.d.ts.map +1 -1
  109. package/dist/middleware/security.js +101 -0
  110. package/dist/middleware/security.js.map +1 -1
  111. package/dist/response/index.d.ts +229 -6
  112. package/dist/response/index.d.ts.map +1 -1
  113. package/dist/response/index.js +272 -2
  114. package/dist/response/index.js.map +1 -1
  115. package/dist/router/boundary.d.ts +25 -5
  116. package/dist/router/boundary.d.ts.map +1 -1
  117. package/dist/router/boundary.js.map +1 -1
  118. package/dist/router/controller-registrar.d.ts.map +1 -1
  119. package/dist/router/controller-registrar.js +56 -1
  120. package/dist/router/controller-registrar.js.map +1 -1
  121. package/dist/router/index.d.ts +2 -2
  122. package/dist/router/index.d.ts.map +1 -1
  123. package/dist/router/index.js +2 -1
  124. package/dist/router/index.js.map +1 -1
  125. package/dist/router/route-builder.d.ts +70 -36
  126. package/dist/router/route-builder.d.ts.map +1 -1
  127. package/dist/router/route-builder.js +13 -15
  128. package/dist/router/route-builder.js.map +1 -1
  129. package/dist/router/route-scanner.d.ts +1 -0
  130. package/dist/router/route-scanner.d.ts.map +1 -1
  131. package/dist/router/route-scanner.js +217 -10
  132. package/dist/router/route-scanner.js.map +1 -1
  133. package/dist/router/router.d.ts.map +1 -1
  134. package/dist/router/router.js +35 -17
  135. package/dist/router/router.js.map +1 -1
  136. package/dist/router/types.d.ts +11 -1
  137. package/dist/router/types.d.ts.map +1 -1
  138. package/dist/sanitize/index.d.ts +6 -6
  139. package/dist/sanitize/index.d.ts.map +1 -1
  140. package/dist/sanitize/index.js +8 -8
  141. package/dist/sanitize/index.js.map +1 -1
  142. package/dist/server/app.d.ts +2 -0
  143. package/dist/server/app.d.ts.map +1 -1
  144. package/dist/server/app.js +14 -0
  145. package/dist/server/app.js.map +1 -1
  146. package/dist/server/bootstrapper.d.ts.map +1 -1
  147. package/dist/server/bootstrapper.js +91 -30
  148. package/dist/server/bootstrapper.js.map +1 -1
  149. package/dist/server/cluster.d.ts.map +1 -1
  150. package/dist/server/cluster.js +13 -2
  151. package/dist/server/cluster.js.map +1 -1
  152. package/dist/server/context.d.ts +8 -0
  153. package/dist/server/context.d.ts.map +1 -1
  154. package/dist/server/context.js +50 -6
  155. package/dist/server/context.js.map +1 -1
  156. package/dist/server/engines/node-engine.d.ts.map +1 -1
  157. package/dist/server/engines/node-engine.js +1 -0
  158. package/dist/server/engines/node-engine.js.map +1 -1
  159. package/dist/server/lifecycle.d.ts +22 -0
  160. package/dist/server/lifecycle.d.ts.map +1 -0
  161. package/dist/server/lifecycle.js +106 -0
  162. package/dist/server/lifecycle.js.map +1 -0
  163. package/dist/server/request-handler.d.ts +1 -0
  164. package/dist/server/request-handler.d.ts.map +1 -1
  165. package/dist/server/request-handler.js +42 -18
  166. package/dist/server/request-handler.js.map +1 -1
  167. package/dist/server/request.d.ts +6 -0
  168. package/dist/server/request.d.ts.map +1 -1
  169. package/dist/server/request.js +112 -33
  170. package/dist/server/request.js.map +1 -1
  171. package/dist/server/response.d.ts +13 -2
  172. package/dist/server/response.d.ts.map +1 -1
  173. package/dist/server/response.js +250 -10
  174. package/dist/server/response.js.map +1 -1
  175. package/dist/testing/index.d.ts.map +1 -1
  176. package/dist/testing/index.js +6 -1
  177. package/dist/testing/index.js.map +1 -1
  178. package/dist/testing/mocks.d.ts.map +1 -1
  179. package/dist/testing/mocks.js +15 -3
  180. package/dist/testing/mocks.js.map +1 -1
  181. package/dist/utils/logger.d.ts +8 -0
  182. package/dist/utils/logger.d.ts.map +1 -1
  183. package/dist/utils/logger.js +184 -1
  184. package/dist/utils/logger.js.map +1 -1
  185. package/dist/validator/tex-types.d.ts +2 -1
  186. package/dist/validator/tex-types.d.ts.map +1 -1
  187. package/dist/validator/tex-types.js +2 -1
  188. package/dist/validator/tex-types.js.map +1 -1
  189. package/dist/validator/tex.d.ts +38 -22
  190. package/dist/validator/tex.d.ts.map +1 -1
  191. package/dist/validator/tex.js +490 -40
  192. package/dist/validator/tex.js.map +1 -1
  193. package/package.json +10 -10
  194. package/dist/dev/error-overlay.d.ts +0 -3
  195. package/dist/dev/error-overlay.d.ts.map +0 -1
  196. package/dist/dev/error-overlay.js +0 -118
  197. package/dist/dev/error-overlay.js.map +0 -1
@@ -23,6 +23,7 @@ my-backend/
23
23
  ├── src/
24
24
  │ ├── http/
25
25
  │ │ ├── server.ts # Application entry point
26
+ │ │ ├── error.ts # Centralized framework exception handler (onError)
26
27
  │ │ ├── route.ts # Root routes (e.g. GET /api)
27
28
  │ │ ├── boundary.ts # Root boundary (config + request pipeline)
28
29
  │ │ ├── users/
@@ -72,6 +73,7 @@ Both styles are automatically detected by the route scanner, CLI manifest genera
72
73
  | `schema.ts` / `*.schema.ts` | `tex` validation schemas, imported by route files. Mandatory for every route accepting input. | Handlers, routing |
73
74
  | `service.ts` / `*.service.ts` | Business logic and data access. DI-injectable. | Routing, response shaping |
74
75
  | `boundary.ts` / `*.boundary.ts` | Folder-scoped config + request pipeline for everything in this folder and below. | — |
76
+ | `error.ts` / `*.error.ts` | Centralized exception and validation error handler (`onError`). Auto-mounted by the framework. | Business logic, routes |
75
77
 
76
78
  ---
77
79
 
@@ -172,14 +174,16 @@ A `boundary.ts` applies to every route in its folder and all subfolders. It has
172
174
  two independent halves:
173
175
 
174
176
  1. **Config** — pure data, via `defineBoundary()` (functional) or `@Boundary()`
175
- (OOP argument). CORS, headers, DI providers, excluded paths.
176
- 2. **Pipeline** — real code, auto-detected by function signature. No imports,
177
+ (OOP argument). CORS, headers, DI providers, excluded paths, and lifecycle hooks.
178
+ 2. **Pipeline & Hooks** — real code, auto-detected by function signature. No imports,
177
179
  no manual registration required.
178
180
 
179
- | Signature | Detected as | Runs |
180
- | ---------------------------------------------------------------------------- | ------------ | ---------------------------------------------------- |
181
- | `(req, res, next)` | A chain step | In file/declaration order, before the route |
182
- | `(ctx, next)` — default export (functional) or a method named `handle` (OOP) | The wrapper | Around all chain steps and the nested route/boundary |
181
+ | Signature | Detected as | Runs |
182
+ | :--- | :--- | :--- |
183
+ | `(req, res, next)` | Chain Step | In file/declaration order, before the route handler |
184
+ | `beforeHandle(req, res)` | Boundary Guard Hook | Immediately before route execution. Short-circuits on `false` or when headers sent |
185
+ | `afterHandle(req, res, data)` | Response Hook | Immediately after route execution. Receives returned data to transform or audit |
186
+ | `(ctx, next)` — default export (functional) or `handle(ctx, next)` (OOP) | Pipeline Wrapper | Around all chain steps and the nested route/boundary |
183
187
 
184
188
  ### Functional
185
189
 
@@ -190,11 +194,25 @@ import type { Request, Response, Next, BoundaryContext } from "exisjs/router";
190
194
  export const config = defineBoundary({
191
195
  cors: { origin: ["https://myapp.com"], credentials: true },
192
196
  headers: { "X-Powered-By": "ExisJS" },
193
- exclude: [{ path: "/health", method: "GET" }],
197
+ exclude: [{ path: "/admin/health", method: "GET" }],
198
+
199
+ // Pre-handler hook (returns false to block with 403, or sends response)
200
+ beforeHandle(req, res) {
201
+ if (req.headers["x-role"] !== "admin") {
202
+ res.status(403).json({ error: "Admin role required" });
203
+ return false;
204
+ }
205
+ },
206
+
207
+ // Post-handler hook (transforms returned payload or performs async audit)
208
+ afterHandle(req, res, data) {
209
+ res.setHeader("X-Audited-By", "AdminBoundary");
210
+ return { ...data, auditedAt: Date.now() };
211
+ },
194
212
  });
195
213
 
196
- export function auth(req: Request, res: Response, next: Next) {
197
- if (!req.headers["x-api-key"]) throw new UnauthorizedError("Missing API key");
214
+ export function logStep(req: Request, res: Response, next: Next) {
215
+ console.log(`[Admin] ${req.method} ${req.path}`);
198
216
  next();
199
217
  }
200
218
 
@@ -216,14 +234,29 @@ import type { Request, Response, Next, BoundaryContext } from "exisjs/router";
216
234
  @Boundary({
217
235
  cors: { origin: ["https://myapp.com"], credentials: true },
218
236
  headers: { "X-Powered-By": "ExisJS" },
237
+ exclude: ["/admin/health"],
219
238
  })
220
239
  export default class AdminBoundary {
221
- auth(req: Request, res: Response, next: Next) {
222
- if (!req.headers["x-api-key"])
223
- throw new UnauthorizedError("Missing API key");
240
+ // Pre-handler hook
241
+ beforeHandle(req: Request, res: Response) {
242
+ if (req.headers["x-role"] !== "admin") {
243
+ res.status(403).json({ error: "Admin role required" });
244
+ return false;
245
+ }
246
+ }
247
+
248
+ // Post-handler hook
249
+ afterHandle(req: Request, res: Response, data: any) {
250
+ return { ...data, auditedAt: Date.now() };
251
+ }
252
+
253
+ // Chain step
254
+ logStep(req: Request, res: Response, next: Next) {
255
+ console.log(`[Admin] ${req.method} ${req.path}`);
224
256
  next();
225
257
  }
226
258
 
259
+ // Wrapper
227
260
  async handle(ctx: BoundaryContext, next: Next) {
228
261
  try {
229
262
  return { success: true, data: await next() };
@@ -234,6 +267,52 @@ export default class AdminBoundary {
234
267
  }
235
268
  ```
236
269
 
270
+ ---
271
+
272
+ ## `src/http/error.ts` (Framework-Level Exception Handler)
273
+
274
+ ExisJS natively discovers and mounts `src/http/error.ts` (or `error.js`) at boot. It exports an `onError(err, req, res)` handler, giving developers a single, standard place to intercept, log, and format all unhandled or validation errors without writing custom `app.onError` boilerplate inside `server.ts`.
275
+
276
+ ### Functional
277
+
278
+ ```typescript filename="src/http/error.ts"
279
+ import type { Request, Response } from "exisjs";
280
+
281
+ export function onError(err: any, req: Request, res: Response) {
282
+ const status = err.statusCode || err.status || 500;
283
+
284
+ // Format consistent error response across the entire API
285
+ res.status(status).json({
286
+ success: false,
287
+ error: {
288
+ message: err.message || "Internal Server Error",
289
+ code: err.code || "INTERNAL_SERVER_ERROR",
290
+ path: req.path,
291
+ },
292
+ });
293
+ }
294
+ ```
295
+
296
+ ### Class-Based (OOP)
297
+
298
+ ```typescript filename="src/http/error.ts"
299
+ import type { Request, Response } from "exisjs";
300
+
301
+ export default class GlobalErrorHandler {
302
+ onError(err: any, req: Request, res: Response) {
303
+ const status = err.statusCode || err.status || 500;
304
+ res.status(status).json({
305
+ success: false,
306
+ error: {
307
+ message: err.message || "Internal Server Error",
308
+ code: err.code || "INTERNAL_ERROR",
309
+ path: req.path,
310
+ },
311
+ });
312
+ }
313
+ }
314
+ ```
315
+
237
316
  A nested folder's `boundary.ts` always runs _inside_ its parent's `next()` —
238
317
  pipeline depth mirrors folder depth automatically.
239
318
 
@@ -32,20 +32,34 @@ const safeData = userSchema.parse(rawInput)
32
32
 
33
33
  ExisJS supports a comprehensive set of strict validation primitives:
34
34
 
35
- - `tex.string()` - String validation (supports options like `{ min: 5, max: 10 }`).
36
- - `tex.email()` - Validates standard email formatting.
37
- - `tex.number()` - Strict number validation (fails if passed a string).
35
+ - `tex.string()` - String validation (supports options like `{ min, max, trim, toLowerCase, toUpperCase, escapeHtml, stripHtml, slugify, mask, default }`).
36
+ - `tex.email()` - Validates standard email formatting (supports `{ trim, toLowerCase, mask, default }`).
37
+ - `tex.uuid()` - Validates UUID strings (supports `{ version: 1 | 4, default }`).
38
+ - `tex.cuid()` - Validates CUID identifiers (supports `{ default }`).
39
+ - `tex.password()` - Validates password complexity (`{ min, max, requireNumbers, requireSymbols, requireUppercase, requireLowercase }`).
40
+ - `tex.creditCard()` - Validates credit card number formats with optional masking (`{ mask: true }`).
41
+ - `tex.number()` - Strict number validation (fails if passed a string unless `coerce: true`).
42
+ - `tex.number({ coerce: true, min: 0, max: 100, default: 10 })` - Automatically coerces incoming string representations (e.g., query params) into native numbers with optional default fallback.
38
43
  - `tex.boolean()` - Strict boolean validation.
39
- - `tex.number({ coerce: true })` - Automatically coerces incoming string representations into native JavaScript numbers.
40
- - `tex.boolean({ coerce: true })` - Coerces `"true"`/`"1"` and `"false"`/`"0"` into native booleans.
41
- - `tex.array(schema)` - Validates an array of items. Accepts primitive types, `tex.object(...)`, or shorthand plain objects `{ ... }`.
44
+ - `tex.boolean({ coerce: true, default: false })` - Coerces `"true"`/`"1"` and `"false"`/`"0"` into native booleans.
45
+ - `tex.date()` - Strict Date instance validation.
46
+ - `tex.date({ coerce: true, minDate: '2020-01-01', maxDate: '2030-01-01', default: () => new Date() })` - Automatically parses ISO 8601 strings, UNIX timestamps, or date strings into native JavaScript `Date` objects with optional boundary checks and lazy default factories.
47
+ - `tex.array(schema, { coerce: true, min: 1, max: 10, default: [] })` - Validates an array of items. When `{ coerce: true }` is enabled, automatically converts single items or comma-separated query strings (`"apple,banana"`) into typed arrays. Accepts primitive types, `tex.object(...)`, or shorthand plain objects `{ ... }`.
48
+ - `tex.enum(['admin', 'user'], { default: 'user' })` - Ensures the value matches one of the provided exact strings.
49
+ - `tex.literal(value)` - Validates against an exact primitive literal (`string | number | boolean`).
50
+ - `tex.record(schema)` - Validates a dictionary/record of key-value pairs matching the value schema.
51
+ - `tex.any()` - Allows any payload value without constraint.
52
+ - `tex.file({ maxSize: 5 * 1024 * 1024, mimeTypes: ['image/png', 'image/jpeg'] })` - Validates uploaded file payloads (size and MIME type).
42
53
  - `tex.object({ ... })` - Validates structured JSON objects.
43
- - `tex.enum(['A', 'B'])` - Ensures the value matches one of the provided exact strings.
44
54
 
45
55
  All primitives accept modifier options or chained helper methods:
46
56
  - `.optional()` / `{ optional: true }` - Allows `undefined`.
47
57
  - `.nullable()` / `{ nullable: true }` - Allows `null`.
48
58
  - `.nullish()` / `{ nullish: true }` - Allows both `null` and `undefined`.
59
+ - `{ default: value | (() => value) }` - Provides a static default or dynamic factory function when the field is omitted or `undefined`. Automatically marks the field as optional in schemas and infers concrete types in TypeScript.
60
+
61
+ > [!TIP]
62
+ > **Unified Defaults without Method Chaining**: ExisJS avoids `.default()` chaining overhead in favor of declarative options objects (e.g. `tex.number({ default: 3000 })` or `tex.date({ default: () => new Date() })`). Dynamic factory functions evaluate per request so timestamps and UUIDs remain fresh!
49
63
 
50
64
  > [!TIP]
51
65
  > **Automatic Empty String `""` Normalization**: When forms or query parameters pass empty strings `""` for fields marked as `.optional()`, `.nullable()`, or `.nullish()`, ExisJS automatically converts them to `undefined` or `null` before running type validation. This prevents false positive type errors on numeric, UUID, or enum fields!
@@ -254,12 +268,75 @@ export const RegisterUserSchema = tex.object({
254
268
  - **Stringified JSON Support**: When submitting `multipart/form-data`, stringified JSON arrays (e.g. `items: "[{\"sku\":\"ABC\",\"quantity\":2}]"`) or stringified objects are automatically deserialized and validated.
255
269
  - **Type Inference**: TypeScript automatically infers the shape of array elements for both `tex.array(tex.object(...))` and `tex.array({ ... })`, ensuring full autocomplete without falling back to `never`.
256
270
 
271
+ ### Query String Array & Date Coercion
272
+
273
+ Query strings in HTTP cannot natively represent arrays or date instances. ExisJS solves this elegantly through `{ coerce: true }`:
274
+
275
+ #### Functional Paradigm
276
+
277
+ ```typescript filename="src/http/events/route.ts"
278
+ import { controller, route } from 'exisjs/router'
279
+ import { tex } from 'exisjs/validator'
280
+
281
+ const FilterSchema = tex.object({
282
+ // Coerces ISO 8601 strings (?from=2026-09-01T00:00:00Z) into Date instances
283
+ from: tex.date({ coerce: true, optional: true }),
284
+ to: tex.date({ coerce: true, optional: true }),
285
+
286
+ // Coerces both comma-separated strings (?tags=tech,ai) AND repeated params (?tags=tech&tags=ai)
287
+ // or single values (?tags=tech -> ['tech']) into a typed array of strings
288
+ tags: tex.array(tex.string(), { coerce: true, optional: true }),
289
+
290
+ // Numbers within comma-separated arrays (?ids=1,2,3 -> [1, 2, 3]) are also coerced
291
+ ids: tex.array(tex.number({ coerce: true }), { coerce: true, optional: true })
292
+ })
293
+
294
+ export default controller({
295
+ listEvents: route.get('/events', {
296
+ query: FilterSchema,
297
+ handle({ query, res }) {
298
+ // query.from is Date | undefined
299
+ // query.tags is string[] | undefined
300
+ // query.ids is number[] | undefined
301
+ res.json({ filter: query })
302
+ }
303
+ })
304
+ })
305
+ ```
306
+
307
+ #### Class-Based (OOP) Paradigm
308
+
309
+ ```typescript filename="src/http/events/route.ts"
310
+ import { Controller, Get, Query } from 'exisjs/decorators'
311
+ import { tex } from 'exisjs/validator'
312
+ import type { ResolveSchema } from 'exisjs/validator'
313
+
314
+ const FilterSchema = tex.object({
315
+ from: tex.date({ coerce: true, optional: true }),
316
+ to: tex.date({ coerce: true, optional: true }),
317
+ tags: tex.array(tex.string(), { coerce: true, optional: true }),
318
+ ids: tex.array(tex.number({ coerce: true }), { coerce: true, optional: true }),
319
+ })
320
+
321
+ type FilterDto = ResolveSchema<typeof FilterSchema>
322
+
323
+ @Controller('/events')
324
+ export default class EventsController {
325
+ @Get('/', { query: FilterSchema })
326
+ listEvents(@Query() query: FilterDto) {
327
+ return { filter: query }
328
+ }
329
+ }
330
+ ```
331
+
257
332
  ---
258
333
 
259
334
  ## Pagination Schemas (`tex.pagination`)
260
335
 
261
336
  ExisJS provides a dedicated schema primitive for query pagination. It automatically coerces incoming string query parameters (`?page=2&limit=25&sort=name&order=asc`) into typed numbers and strings while enforcing sensible bounds:
262
337
 
338
+ #### Functional Paradigm
339
+
263
340
  ```typescript filename="src/http/items/route.ts"
264
341
  import { controller, route } from 'exisjs/router'
265
342
  import { tex, paginate, getPaginationSkip } from 'exisjs/validator'
@@ -283,11 +360,35 @@ export default controller({
283
360
  })
284
361
  ```
285
362
 
363
+ #### Class-Based (OOP) Paradigm
364
+
365
+ ```typescript filename="src/http/items/route.ts"
366
+ import { Controller, Get, Query } from 'exisjs/decorators'
367
+ import { tex, paginate, getPaginationSkip } from 'exisjs/validator'
368
+
369
+ const ItemQuerySchema = tex.pagination({
370
+ defaultLimit: 20,
371
+ maxLimit: 100,
372
+ })
373
+
374
+ @Controller('/items')
375
+ export default class ItemsController {
376
+ @Get('/', { query: ItemQuerySchema })
377
+ async list(@Query() query: any) {
378
+ const { skip, limit, page } = getPaginationSkip(query)
379
+ const items = await fetchItems({ skip, limit, sort: query.sort, order: query.order })
380
+ const total = await countItems()
381
+
382
+ return paginate(items, total, { page, limit })
383
+ }
384
+ }
385
+ ```
386
+
286
387
  ---
287
388
 
288
389
  ## Error Handling & Developer Diagnostics
289
390
 
290
- If a request fails validation, ExisJS intercepts the rejection and returns a standardized `400 Bad Request` containing a machine-readable `errors: Record<string, string>` dictionary for frontend form field mapping:
391
+ If a request fails validation, ExisJS intercepts the rejection and returns a standardized `400 Bad Request` containing both a legacy `errors: Record<string, string>` dictionary and a structured `details: ValidationErrorDescriptor[]` array for frontend form field mapping:
291
392
 
292
393
  ```json filename="HTTP 400 Bad Request Response"
293
394
  {
@@ -297,16 +398,32 @@ If a request fails validation, ExisJS intercepts the rejection and returns a sta
297
398
  "errors": {
298
399
  "username": "Field must be at least 4 characters",
299
400
  "profile.tags": "Expected array of strings"
300
- }
401
+ },
402
+ "details": [
403
+ {
404
+ "path": "username",
405
+ "message": "Field must be at least 4 characters",
406
+ "expected": "min: 4",
407
+ "received": "abc",
408
+ "code": "VALUE_TOO_SMALL"
409
+ },
410
+ {
411
+ "path": "profile.tags",
412
+ "message": "Expected array of strings",
413
+ "expected": "Array of string",
414
+ "received": "number",
415
+ "code": "INVALID_TYPE"
416
+ }
417
+ ]
301
418
  }
302
419
  ```
303
420
 
304
421
  ### Dev Mode Terminal Diagnostics
305
422
 
306
- When validation fails in development mode, ExisJS prints an immediate tabular diagnostic to your terminal displaying the failing HTTP part, exact field path, received value, and expected schema constraint:
423
+ When validation fails in development mode, ExisJS prints an immediate tabular diagnostic to your terminal displaying the HTTP method, route path, failing HTTP part (`body`, `query`, `params`, `headers`), exact field path, received value, and expected schema constraint:
307
424
 
308
425
  ```text
309
- [VALIDATION FAILED] POST /visits (part: body)
426
+ [VALIDATION FAILED] POST /api/v1/visits (part: body)
310
427
  ┌─────────────┬───────────────────────────┬──────────────────┐
311
428
  │ Field │ Received │ Expected │
312
429
  ├─────────────┼───────────────────────────┼──────────────────┤
@@ -315,3 +432,101 @@ When validation fails in development mode, ExisJS prints an immediate tabular di
315
432
  └─────────────┴───────────────────────────┴──────────────────┘
316
433
  ```
317
434
 
435
+ ---
436
+
437
+ ## Environment Variable Validation (`tex.env`)
438
+
439
+ Because `process.env` always supplies raw string values (e.g. `"4000"`, `"true"`, `"1"`), ExisJS includes a dedicated `tex.env()` schema parser. It automatically injects coercion for numbers, booleans, dates, and arrays, allowing you to validate your environment variables on boot without writing `{ coerce: true }` on every field:
440
+
441
+ ```typescript filename="src/config/env.ts"
442
+ import { tex } from 'exisjs/validator'
443
+
444
+ export const env = tex.env({
445
+ PORT: tex.number({ default: 4000 }), // Automatically coerced from "4000" -> 4000
446
+ DEBUG: tex.boolean({ default: false }), // Parsed from "true" | "1" | "false" | "0"
447
+ DATABASE_URL: tex.string(),
448
+ JWT_SECRET: tex.string({ min: 32 }),
449
+ ALLOWED_ORIGINS: tex.array(tex.string()), // Parsed from "http://localhost:3000,https://app.com"
450
+ NODE_ENV: tex.enum(['development', 'production', 'test'] as const, {
451
+ default: 'development',
452
+ }),
453
+ }).parse(process.env)
454
+ ```
455
+
456
+ > [!TIP]
457
+ > **Zero-Boilerplate Boot Safety**: `tex.env()` automatically validates `process.env` on server startup. If a required environment variable is missing, it crashes immediately with a clean diagnostic log before the HTTP listener opens.
458
+
459
+ ---
460
+
461
+ ## Third-Party Validator Interoperability (Zod, Valibot, ArkType)
462
+
463
+ While ExisJS provides the high-performance native `tex` engine out of the box, it is **100% interoperable with external schema libraries like Zod, Valibot, or ArkType** via Duck Typing.
464
+
465
+ Any object with a `.parse()` or `.safeParse()` method can be passed directly as a route schema or used in `src/config/env.ts`.
466
+
467
+ ### Using Zod in Routes
468
+
469
+ #### Functional Paradigm
470
+
471
+ ```typescript filename="src/http/users/route.ts"
472
+ import { controller, route } from 'exisjs/router'
473
+ import { z } from 'zod'
474
+
475
+ const CreateUserSchema = z.object({
476
+ name: z.string().min(3),
477
+ email: z.string().email(),
478
+ role: z.enum(['admin', 'user']),
479
+ })
480
+
481
+ export default controller({
482
+ create: route.post('/', {
483
+ body: CreateUserSchema, // 👈 Directly pass your Zod schema!
484
+ async handle({ body, res }) {
485
+ return res.status(201).json({ success: true, user: body })
486
+ }
487
+ })
488
+ })
489
+ ```
490
+
491
+ #### Class-Based (OOP) Paradigm
492
+
493
+ ```typescript filename="src/http/users/route.ts"
494
+ import { Controller, Post, Body, HttpCode } from 'exisjs/decorators'
495
+ import { z } from 'zod'
496
+
497
+ const CreateUserSchema = z.object({
498
+ name: z.string().min(3),
499
+ email: z.string().email(),
500
+ role: z.enum(['admin', 'user']),
501
+ })
502
+
503
+ type CreateUserDto = z.infer<typeof CreateUserSchema>
504
+
505
+ @Controller('/users')
506
+ export default class UsersController {
507
+ @Post('/')
508
+ @HttpCode(201)
509
+ async create(@Body(CreateUserSchema) body: CreateUserDto) {
510
+ return { success: true, user: body }
511
+ }
512
+ }
513
+ ```
514
+
515
+ ### Using Zod for Environment Validation
516
+
517
+ ```typescript filename="src/config/env.ts"
518
+ import { z } from 'zod'
519
+
520
+ const EnvSchema = z.object({
521
+ PORT: z.coerce.number().default(4000),
522
+ NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
523
+ DATABASE_URL: z.string().url(),
524
+ JWT_SECRET: z.string().min(32),
525
+ })
526
+
527
+ export const env = EnvSchema.parse(process.env)
528
+ ```
529
+
530
+ > [!NOTE]
531
+ > **Universal Error Normalization**: Regardless of which library you use (**Zod**, **Valibot**, **Yup**, **Joi**, **ArkType**, or any Standard Schema-compliant library), ExisJS inspects the error structure (`issues`, `errors`, `inner`) and automatically normalizes it into standardized `400 Bad Request` JSON responses with complete field mappings and dev-mode terminal overlays.
532
+
@@ -166,3 +166,14 @@ export default class UserController {
166
166
 
167
167
  > [!TIP]
168
168
  > For deep services inside an OOP architecture, you can still seamlessly use `getContext()`, `getRequest()`, or `after()` from `exisjs/router` to access the async context without prop-drilling!
169
+
170
+ ---
171
+
172
+ ## Context Isolation & Memory Leak Safety
173
+
174
+ In high-concurrency micro-benchmarks or long-lived streaming connections (SSE / WebSockets), holding references in `AsyncLocalStorage` without explicit teardown can lead to memory retention. ExisJS enforces a deterministic context cleanup lifecycle:
175
+
176
+ 1. **Deterministic Teardown (`cleanupContext`)**: When a response emits the `'finish'` or `'close'` event, ExisJS automatically executes all remaining `after()` callbacks, clears request-scoped dependency injection instances (`diCache`), purges the `state` dictionary, and unlinks circular references (`req = null!`, `res = null!`, `app = null!`).
177
+ 2. **`try / finally` Dispatch Boundary**: The router wraps every handler dispatch in a `try ... finally` boundary, guaranteeing that unhandled exceptions or rejected promises cannot leak state into subsequent async continuations.
178
+ 3. **Recycling Pool Memory Scrubbing**: When recycled `ExisRequest` and `ExisResponse` instances return to the internal object pool, their internal dependency injection caches and request metadata are scrubbed clean.
179
+
@@ -68,6 +68,10 @@ Parameter decorators automatically extract data from the incoming request and in
68
68
  - `@Param(nameOrPipe?)`: Injects a URL path parameter (e.g., `id` from `/:id`).
69
69
  - `@Headers(nameOrPipe?)`: Injects an HTTP header by name.
70
70
  - `@QueryParam(nameOrPipe?)` / `@Query(nameOrPipe?)`: Injects a URL query parameter (e.g., `?search=term`).
71
+ - `@Cookies(nameOrPipe?)` / `@Cookie(nameOrPipe?)`: Injects incoming parsed cookies or a specific cookie value.
72
+ - `@State(keyOrPipe?)`: Injects request state from execution context or middleware.
73
+ - `@AppCtx()`: Injects the root ExisJS Application instance.
74
+ - `@Fields(nameOrPipe?)`: Injects non-file multipart form fields.
71
75
  - `@UploadedFile()`: Injects the uploaded file (`req.file`) into the method argument.
72
76
  - `@UploadedFiles()`: Injects the uploaded files array or dictionary (`req.files`) into the method argument.
73
77
  - `@HostParam(nameOrPipe?)`: Injects a subdomain parameter.
@@ -83,8 +87,14 @@ Parameter decorators automatically extract data from the incoming request and in
83
87
 
84
88
  - `@Use(...middlewaresOrGuardsOrInterceptorsOrFilters)`: Unified decorator that binds middlewares, guards (`canActivate`), interceptors (`intercept`), or exception filters (`catch`) to a controller class or method.
85
89
  - `@Upload(fieldName, options?)` / `@UseUpload(...)`: Configures multipart file upload handling for a method, populating `@UploadedFile()` or `@UploadedFiles()`.
86
- - `@Timeout(ms: number | TimeoutOptions)`: Overrides the request timeout for this method (e.g. `@Timeout(120000)`). Passing `0` disables timeouts for long-running streaming or exports.
87
- - `@Idempotent(options?)`: Caches responses based on the `Idempotency-Key` header using native in-memory off-heap storage.
90
+ - `@Timeout(ms: number | TimeoutOptions)`: Overrides the request timeout for a class or method (e.g. `@Timeout(120000)`). Passing `0` disables timeouts for long-running streaming or exports.
91
+ - `@Idempotent(options?)`: Caches responses based on the `Idempotency-Key` header using native in-memory off-heap storage. Works at class and method level.
92
+ - `@RateLimit(options?)`: Enforces rate limits (e.g. `@RateLimit({ max: 100, windowMs: 60000 })`) at class or method level.
93
+ - `@Cors(options?)`: Enforces Cross-Origin Resource Sharing rules at class or method level.
94
+ - `@IpFilter(options)`: Enforces CIDR and IP allow/deny lists at class or method level.
95
+ - `@Compress()`: Enables automatic Gzip and Brotli compression on responses.
96
+ - `@Dedupe(options)`: Deduplicates concurrent identical requests sharing the same key.
97
+ - `@BlockProbes(options?)`: Intercepts vulnerability scanner probes (`.env`, `/.git`, `/.DS_Store`) and blackholes them without triggering log noise.
88
98
 
89
99
  ---
90
100
 
@@ -108,8 +118,8 @@ Parameter decorators automatically extract data from the incoming request and in
108
118
 
109
119
  - `@Module({ imports?, controllers?, providers?, exports? })`: OOP class decorator equivalent to `defineModule()` for structuring application modules.
110
120
  - `@Injectable(options?)`: Marks a service class as an IoC provider managed by the container. Supports `{ scope: 'singleton' | 'request' | 'transient' }`.
111
- - `@Inject(token)`: Constructor parameter decorator for injecting dependencies by class, string, or symbol token.
112
- - `@Optional()`: Constructor parameter decorator resolving missing dependencies to `undefined` without throwing.
121
+ - `@Inject(token)`: Constructor parameter decorator from `exisjs/di` for injecting dependencies by class, string, or symbol token.
122
+ - `@Optional()`: Constructor parameter decorator from `exisjs/di` resolving missing dependencies to `undefined` without throwing.
113
123
  - `@Boundary(config)`: Marks a class as a folder-scoped Boundary configuring CORS, headers, exclusions, and pipeline steps.
114
124
  - `@Server(config)`: Marks a class as a root server definition.
115
125
 
@@ -170,3 +170,47 @@ app.use((err, req, res, next) => {
170
170
  ExisJS natively intercepts and handles Promise rejections at the core routing level.
171
171
 
172
172
  Unlike Express, you do **not** need to wrap your routes in an `asyncHandler`. However, for strict compatibility with older Express middleware ecosystems, `exisjs/error` does export an `asyncHandler` wrapper. Note that using it in ExisJS is entirely redundant and merely adds unnecessary Promise allocations.
173
+
174
+ ---
175
+
176
+ ## Developer Error Formatting & Overlay Utilities
177
+
178
+ `exisjs/error` also exports powerful error diagnostic utilities that you can use in custom error handlers, CLI scripts, background workers, and testing:
179
+
180
+ ```typescript
181
+ import {
182
+ formatDevError,
183
+ renderDevErrorToString,
184
+ categorizeError,
185
+ generateErrorHint,
186
+ buildCodeFrame,
187
+ stripInternalStackFrames,
188
+ devErrorResponse,
189
+ type ParsedError,
190
+ } from 'exisjs/error'
191
+ ```
192
+
193
+ ### Utility Functions
194
+
195
+ | Function | Signature | Description |
196
+ | :--- | :--- | :--- |
197
+ | `formatDevError` | `(err: Error, optionsOrFile?: string \| { routeFile?: string; title?: string }) => void` | Prints a formatted terminal error envelope with code frames and actionable hints |
198
+ | `renderDevErrorToString` | `(err: Error, optionsOrFile?: ...) => string` | Returns the formatted terminal error envelope string |
199
+ | `categorizeError` | `(err: Error) => string` | Intelligently classifies errors into domain categories (`Validation Schema Error`, `Environment Configuration Error`, etc.) |
200
+ | `generateErrorHint` | `(err: Error) => string \| null` | Generates actionable hints for common coding mistakes |
201
+ | `buildCodeFrame` | `(filePath: string, line: number, col?: number) => string` | Builds a source code snippet visualizer with gutter line numbers and caret indicator |
202
+ | `stripInternalStackFrames` | `(stack?: string) => string[]` | Strips internal Node.js and framework runtime stack frames |
203
+ | `devErrorResponse` | `(err: Error, routeFile?: string) => object` | Generates a structured developer error envelope payload for JSON API responses |
204
+
205
+ ### Example: Custom Worker or Script Error Handling
206
+
207
+ ```typescript filename="src/workers/import-csv.ts"
208
+ import { formatDevError, categorizeError } from 'exisjs/error'
209
+
210
+ try {
211
+ await processCsvImport(filePath)
212
+ } catch (err: any) {
213
+ // Prints a clean code frame with line numbers and actionable hints directly in terminal
214
+ formatDevError(err, { routeFile: filePath, title: categorizeError(err) })
215
+ }
216
+ ```