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,403 +1,598 @@
1
1
  ---
2
+ title: "Validation and Schemas"
3
+ description: "Declare, validate, and sanitize request payloads, query strings, parameters, and environment variables using the native tex schema builder."
2
4
  nextUrl: "/docs/errors"
3
5
  nextTitle: "Error Handling"
4
6
  ---
5
7
 
6
- # Validation & Schemas
8
+ # Validation and Schemas
7
9
 
8
- If you are coming from frameworks like Fastify, you are likely used to writing validation schemas using JSON Schema strings and relying on external dependencies like AJV.
10
+ ExisJS includes a built-in schema validation and sanitization engine named `tex`. It provides compile-time TypeScript type inference and runtime payload validation without requiring external schema dependencies.
9
11
 
10
- ExisJS takes a strictly-typed approach. It includes a native validation engine out of the box, powered by Rust for maximum performance. You don't need any external dependencies, and you avoid string-based references.
12
+ ---
11
13
 
12
- ## The Native Validator (`tex`)
14
+ ## The `tex` Validator
13
15
 
14
- ExisJS exports a global validator object `tex` that allows you to fluently build complex validation schemas using pure, typesafe JavaScript.
16
+ Import `tex` from `exisjs/validator` to declare schemas for request bodies, query strings, path parameters, and headers.
15
17
 
16
- ```typescript filename="example.ts"
18
+ ```typescript filename="src/http/users/schema.ts"
17
19
  import { tex } from 'exisjs/validator'
20
+ import type { ResolveSchema } from 'exisjs/validator'
18
21
 
19
- // Build a strict schema intuitively
20
- const userSchema = tex.object({
21
- name: tex.string({ min: 3 }),
22
- age: tex.number({ optional: true }),
23
- role: tex.enum(['admin', 'user']),
24
- email: tex.email()
22
+ export const CreateUserSchema = tex.object({
23
+ name: tex.string({ min: 3, max: 50, trim: true }),
24
+ email: tex.email({ toLowerCase: true }),
25
+ age: tex.number({ optional: true, min: 18 }),
26
+ role: tex.enum(['admin', 'member'], { default: 'member' }),
25
27
  })
26
28
 
27
- // You can natively parse data directly!
28
- const safeData = userSchema.parse(rawInput)
29
+ export type CreateUserInput = ResolveSchema<typeof CreateUserSchema>
29
30
  ```
30
31
 
31
- ### Validation Primitives
32
-
33
- ExisJS supports a comprehensive set of strict validation primitives:
34
-
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.
43
- - `tex.boolean()` - Strict boolean validation.
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).
53
- - `tex.object({ ... })` - Validates structured JSON objects.
54
-
55
- All primitives accept modifier options or chained helper methods:
56
- - `.optional()` / `{ optional: true }` - Allows `undefined`.
57
- - `.nullable()` / `{ nullable: true }` - Allows `null`.
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!
63
-
64
- > [!TIP]
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!
66
-
67
- ### Data Sanitization (Trimming & Formatting)
68
-
69
- ExisJS allows you to mutate and sanitize incoming data in real-time before it reaches your controller. It includes a native Rust-powered sanitization engine for Strings and Arrays.
32
+ You can validate data manually using `.parse()`:
70
33
 
71
34
  ```typescript filename="example.ts"
72
- import { tex } from 'exisjs/validator'
73
- import { sanitize } from 'exisjs/sanitize'
35
+ const validated = CreateUserSchema.parse(rawInput)
36
+ ```
74
37
 
75
- const loginSchema = tex.object({
76
- // Automatically trim whitespace, convert to lowercase, and prevent HTML injections
77
- email: tex.email().sanitize(sanitize.trim, sanitize.toLowerCase, sanitize.escapeHtml),
78
- password: tex.string({ min: 8 })
79
- })
38
+ If validation fails, `.parse()` throws a `ValidatorError` containing field-level error descriptors. When used inside routes, ExisJS catches validation failures automatically and returns an HTTP `400 Bad Request` response.
80
39
 
81
- const articleSchema = tex.object({
82
- title: tex.string().sanitize(sanitize.collapseWhitespace, sanitize.truncate(100)),
83
- // Auto-generate slugs from the title using sanitize.slugify!
84
- slug: tex.string().sanitize(sanitize.slugify),
85
- tags: tex.array(tex.string()).sanitize(sanitize.dedupe, sanitize.compact)
86
- })
87
- ```
40
+ ---
88
41
 
89
- ### Standalone Sanitization Usage
42
+ ## Schema Primitives Reference
43
+
44
+ ExisJS provides type-safe primitives that accept modifier options and configuration objects.
45
+
46
+ | Primitive | Signature | Description |
47
+ | :--- | :--- | :--- |
48
+ | `tex.string()` | `tex.string(options?)` | Validates strings. Supports `{ min, max, trim, toLowerCase, toUpperCase, collapseWhitespace, escapeHtml, stripHtml, slugify, mask, preventSql, preventTraversal, coerce, default }`. |
49
+ | `tex.number()` | `tex.number(options?)` | Validates numbers. Supports `{ min, max, coerce, default }`. |
50
+ | `tex.boolean()` | `tex.boolean(options?)` | Validates booleans. Supports `{ coerce, default }`. Coerces `"true"`, `"false"`, `"1"`, `"0"`. |
51
+ | `tex.email()` | `tex.email(options?)` | Validates RFC-compliant email formatting. Supports `{ trim, toLowerCase, mask, default }`. |
52
+ | `tex.uuid()` | `tex.uuid(options?)` | Validates UUID strings. Supports `{ version: 1 \| 4, default }`. |
53
+ | `tex.cuid()` | `tex.cuid(options?)` | Validates CUID identifiers. Supports `{ default }`. |
54
+ | `tex.password()` | `tex.password(options?)` | Validates password complexity. Supports `{ min, max, requireNumbers, requireSymbols, requireUppercase, requireLowercase }`. |
55
+ | `tex.creditCard()` | `tex.creditCard(options?)` | Validates credit card numbers using the Luhn algorithm. Supports `{ mask: true }`. |
56
+ | `tex.date()` | `tex.date(options?)` | Validates JavaScript `Date` instances or ISO strings with `{ coerce: true, minDate, maxDate, default }`. |
57
+ | `tex.array()` | `tex.array(schema, options?)` | Validates arrays of items. Accepts primitive schemas, `tex.object(...)`, or shorthand `{ ... }`. Supports `{ min, max, dedupe, coerce, default }`. |
58
+ | `tex.enum()` | `tex.enum(values, options?)` | Validates against an allowed list of string literals. Supports `{ default }`. |
59
+ | `tex.literal()` | `tex.literal(value, options?)` | Validates against an exact primitive value (`string \| number \| boolean`). |
60
+ | `tex.union()` | `tex.union(schemas, options?)` | Validates against any matching schema in the provided tuple. |
61
+ | `tex.discriminatedUnion()` | `tex.discriminatedUnion(tag, variants)` | Validates polymorphic object variants discriminated by a literal or enum key. |
62
+ | `tex.record()` | `tex.record(valueSchema, options?)` | Validates dynamic key-value dictionaries (`Record<string, T>`). |
63
+ | `tex.any()` | `tex.any(options?)` | Pass-through validator without schema constraints. |
64
+ | `tex.file()` | `tex.file(options?)` | Validates multipart file uploads. Supports `{ maxSize, mimeTypes }`. |
65
+ | `tex.object()` | `tex.object(shape, options?)` | Compiles an object schema. Supports `{ strict: true }` to reject unlisted keys. |
66
+ | `tex.coerce.*` | `tex.coerce.number() \| boolean() \| date() \| string()` | Explicit coercion namespace for query strings, route parameters, and form inputs. |
67
+ | `tex.pagination()` | `tex.pagination(options?)` | Predefined schema for `page`, `limit`, `sort`, and `order` query parameters. |
68
+ | `tex.env()` | `tex.env(shape, options?)` | Environment variable schema parser with automatic type coercion from string values. |
90
69
 
91
- Because the sanitization engine is designed as a collection of pure, independent functions, **you don't have to use it with the Validator**. If you ever need to manually sanitize data deep inside your services, background jobs, or generic Node.js scripts, you can import the engine directly:
70
+ ---
92
71
 
93
- ```typescript filename="src/services/formatter.ts"
94
- import { sanitize } from 'exisjs/sanitize'
72
+ ## Modifiers and Defaults
95
73
 
96
- export function formatArticleTitle(rawInput: string) {
97
- let clean = sanitize.stripHtml(rawInput)
98
- clean = sanitize.collapseWhitespace(clean)
99
- return sanitize.trim(clean)
100
- }
74
+ All primitive builders accept modifier options directly in their configuration argument, or through chaining helper methods:
75
+
76
+ | Modifier | Options Argument | Method Helper | Behavior |
77
+ | :--- | :--- | :--- | :--- |
78
+ | **Optional** | `{ optional: true }` | `.optional()` | Allows `undefined`. |
79
+ | **Nullable** | `{ nullable: true }` | `.nullable()` | Allows `null`. |
80
+ | **Nullish** | `{ nullish: true }` | `.nullish()` | Allows both `null` and `undefined`. |
81
+ | **Default** | `{ default: value \| () => value }` | N/A | Evaluates static value or dynamic factory function when field is missing. Automatically marks the field optional in schemas. |
82
+
83
+ ```typescript filename="example.ts"
84
+ import { tex } from 'exisjs/validator'
85
+
86
+ const Schema = tex.object({
87
+ nickname: tex.string({ optional: true }),
88
+ avatarUrl: tex.string().nullable(),
89
+ bio: tex.string().nullish(),
90
+ retryCount: tex.number({ default: 3 }),
91
+ createdAt: tex.date({ default: () => new Date() }),
92
+ })
101
93
  ```
102
94
 
103
- ## Schema Composition & Features
95
+ > [!NOTE]
96
+ > **Empty String Normalization**: When query parameters or form submissions supply empty strings `""` for fields marked `.optional()`, `.nullable()`, or `.nullish()`, ExisJS converts them to `undefined` or `null` before validation to avoid type conversion errors.
104
97
 
105
- ExisJS validation provides robust features to handle real-world use cases efficiently and seamlessly.
98
+ ---
106
99
 
107
- ### Schema Composition
100
+ ## Route Validation
108
101
 
109
- You can construct powerful shared schemas easily using composition methods (`.partial()`, `.omit()`, `.pick()`). This prevents schema duplication.
102
+ Pass schemas directly into route handler configurations or decorators. ExisJS validates incoming requests before the handler executes.
110
103
 
111
- ```typescript filename="example.ts"
112
- const UserSchema = tex.object({
113
- id: tex.string(),
114
- name: tex.string(),
104
+ ### Functional Paradigm
105
+
106
+ ```typescript filename="src/http/users/route.ts"
107
+ import { controller, route } from 'exisjs/router'
108
+ import { tex } from 'exisjs/validator'
109
+
110
+ const CreateUserBody = tex.object({
111
+ username: tex.string({ min: 4, max: 20 }),
115
112
  email: tex.email(),
116
- password: tex.string({ min: 8 })
113
+ password: tex.password({ min: 8, requireNumbers: true }),
117
114
  })
118
115
 
119
- // Create an update schema (all fields optional)
120
- const UpdateUserSchema = UserSchema.partial()
116
+ const UserQuery = tex.object({
117
+ source: tex.string({ optional: true }),
118
+ })
119
+
120
+ export default controller({
121
+ create: route.post('/', {
122
+ body: CreateUserBody,
123
+ query: UserQuery,
124
+ async handle({ body, query, res }) {
125
+ // body: { username: string; email: string; password: string }
126
+ // query: { source?: string }
127
+ res.status(201).json({ created: body.username, source: query.source })
128
+ },
129
+ }),
130
+ })
131
+ ```
132
+
133
+ ### Class-Based (OOP) Paradigm
134
+
135
+ ```typescript filename="src/http/users/route.ts"
136
+ import { Controller, Post, Body, Query, HttpCode } from 'exisjs/decorators'
137
+ import { tex } from 'exisjs/validator'
138
+ import type { ResolveSchema } from 'exisjs/validator'
139
+
140
+ const CreateUserBody = tex.object({
141
+ username: tex.string({ min: 4, max: 20 }),
142
+ email: tex.email(),
143
+ password: tex.password({ min: 8, requireNumbers: true }),
144
+ })
121
145
 
122
- // Create a safe response schema (omit password)
123
- const PublicUserSchema = UserSchema.omit(['password'])
146
+ const UserQuery = tex.object({
147
+ source: tex.string({ optional: true }),
148
+ })
124
149
 
125
- // Create a specialized schema
126
- const LoginSchema = UserSchema.pick(['email', 'password'])
150
+ type CreateUserDto = ResolveSchema<typeof CreateUserBody>
151
+ type UserQueryDto = ResolveSchema<typeof UserQuery>
127
152
 
128
- // Chain multiple schemas using tex.union()
129
- const FlexibleIdSchema = tex.union([tex.string(), tex.number()])
153
+ @Controller('/users')
154
+ export default class UsersController {
155
+ @Post('/')
156
+ @HttpCode(201)
157
+ async create(
158
+ @Body(CreateUserBody) body: CreateUserDto,
159
+ @Query(UserQuery) query: UserQueryDto
160
+ ) {
161
+ return { created: body.username, source: query.source }
162
+ }
163
+ }
130
164
  ```
131
165
 
132
- ### Asynchronous Validation
166
+ ---
133
167
 
134
- Need to check a database during validation? No problem! Use `.refineAsync()` to run complex async logic seamlessly within your validation schemas. The ExisJS router will automatically wait for the async checks to resolve before executing your route handler.
168
+ ## Query String and Form Coercion
135
169
 
136
- ```typescript filename="example.ts"
137
- const registerSchema = tex.object({
138
- email: tex.email()
139
- .refineAsync(async (val) => {
140
- const exists = await db.users.find({ email: val })
141
- return !exists // Return false if email is taken
142
- }, 'Email is already taken'),
143
- password: tex.string({ min: 8 })
170
+ Query string parameters and `multipart/form-data` payloads arrive as strings. Use `{ coerce: true }` to convert string representations into numbers, booleans, dates, or arrays.
171
+
172
+ ### Query Array and Date Coercion
173
+
174
+ ```typescript filename="src/http/events/route.ts"
175
+ import { controller, route } from 'exisjs/router'
176
+ import { tex } from 'exisjs/validator'
177
+
178
+ const EventFilterSchema = tex.object({
179
+ // Parses ISO 8601 strings (?from=2026-09-01T00:00:00Z) into Date instances
180
+ from: tex.date({ coerce: true, optional: true }),
181
+ to: tex.date({ coerce: true, optional: true }),
182
+
183
+ // Parses comma-separated (?tags=tech,ai) or repeated (?tags=tech&tags=ai) parameters
184
+ tags: tex.array(tex.string(), { coerce: true, optional: true }),
185
+
186
+ // Parses comma-separated numeric IDs (?ids=1,2,3 -> [1, 2, 3])
187
+ ids: tex.array(tex.number({ coerce: true }), { coerce: true, optional: true }),
188
+ })
189
+
190
+ export default controller({
191
+ list: route.get('/', {
192
+ query: EventFilterSchema,
193
+ handle({ query, res }) {
194
+ res.json({ filter: query })
195
+ },
196
+ }),
144
197
  })
145
198
  ```
146
199
 
147
- ### Response Stripping & Serialization
200
+ ### Form and Multipart Data Coercion
148
201
 
149
- Because `TexEngine` validates by strictly iterating over its defined schema keys, it naturally **strips** any extra or unknown properties from the resulting object.
202
+ When working with `multipart/form-data`:
203
+ - Single item payloads passed to array fields (e.g. `{ item: { sku: "A1" } }`) are coerced into single-element arrays `[{ sku: "A1" }]`.
204
+ - JSON-encoded strings submitted within form fields (e.g. `items: "[{\"sku\":\"A1\"}]"`) are deserialized and validated against nested object/array schemas.
150
205
 
151
- This provides immense security out of the box when used as a `response` schema, ensuring you never accidentally leak sensitive data (like passwords or internal IDs) to the client. The ExisJS router handles this stripping serialization natively behind the scenes.
206
+ ---
152
207
 
153
- ### Separating Schemas
208
+ ## Schema Composition
154
209
 
155
- To keep your route files clean and maintainable, we highly recommend defining your schemas in a dedicated `schema.ts` file alongside your routes.
210
+ Reuse and extend existing object schemas using `.partial()`, `.pick()`, and `.omit()`.
156
211
 
157
212
  ```typescript filename="src/http/users/schema.ts"
158
213
  import { tex } from 'exisjs/validator'
159
214
  import type { ResolveSchema } from 'exisjs/validator'
160
215
 
161
- export const UserParamsSchema = tex.object({
162
- id: tex.number({ coerce: true })
216
+ export const UserSchema = tex.object({
217
+ id: tex.string(),
218
+ name: tex.string({ min: 2 }),
219
+ email: tex.email(),
220
+ password: tex.password({ min: 8 }),
221
+ role: tex.enum(['admin', 'member'], { default: 'member' }),
163
222
  })
164
223
 
165
- export type UserParams = ResolveSchema<typeof UserParamsSchema>
224
+ // All fields become optional
225
+ export const UpdateUserSchema = UserSchema.partial()
226
+
227
+ // Retain only specified fields
228
+ export const LoginSchema = UserSchema.pick(['email', 'password'])
229
+
230
+ // Exclude sensitive fields for responses
231
+ export const PublicUserSchema = UserSchema.omit(['password'])
232
+
233
+ export type User = ResolveSchema<typeof UserSchema>
234
+ export type UpdateUserInput = ResolveSchema<typeof UpdateUserSchema>
235
+ export type LoginInput = ResolveSchema<typeof LoginSchema>
236
+ export type PublicUser = ResolveSchema<typeof PublicUserSchema>
166
237
  ```
167
238
 
168
- To validate incoming HTTP requests, ExisJS provides built-in validation configurations. Whether you use the Functional paradigm or the Class-based (OOP) paradigm, ExisJS validates incoming payloads and types your handlers.
239
+ ---
169
240
 
170
- #### Functional Paradigm
241
+ ## Data Sanitization
171
242
 
172
- ```typescript filename="src/http/users/route.ts"
173
- import { controller, route } from 'exisjs/router'
243
+ ExisJS allows transformations and sanitizations to run during schema validation or as standalone utility functions via `exisjs/sanitize`.
244
+
245
+ ### Chaining Sanitizers with `.sanitize()`
246
+
247
+ ```typescript filename="src/http/articles/schema.ts"
174
248
  import { tex } from 'exisjs/validator'
249
+ import { sanitize } from 'exisjs/sanitize'
175
250
 
176
- export default controller({
177
- create: route.post('/', {
178
- // 1. The validation schema is passed directly in the config
179
- body: tex.object({
180
- username: tex.string({ min: 4 }),
181
- password: tex.string({ min: 8 })
182
- }),
183
- query: tex.object({
184
- source: tex.string({ optional: true })
185
- }),
186
-
187
- // 2. Your handler only runs if validation succeeds
188
- async handle({ body, query, req, res }) {
189
- // body and query are strongly-typed here
190
- await db.users.create(body)
191
- res.status(201).json({ success: true })
192
- }
193
- })
251
+ export const CreateArticleSchema = tex.object({
252
+ title: tex.string({ min: 5 }).sanitize(
253
+ sanitize.collapseWhitespace,
254
+ sanitize.stripHtml
255
+ ),
256
+ slug: tex.string().sanitize(sanitize.slugify),
257
+ content: tex.string().sanitize(sanitize.escapeHtml),
258
+ tags: tex.array(tex.string()).sanitize(
259
+ sanitize.dedupe,
260
+ sanitize.compact
261
+ ),
194
262
  })
195
263
  ```
196
264
 
197
- #### Class-Based (OOP) Paradigm
265
+ ### Standalone Sanitizer Usage
198
266
 
199
- ```typescript filename="src/http/api/books/route.ts"
200
- import { Controller, Use, Post, Body, Req } from 'exisjs/decorators'
267
+ You can import `sanitize` directly in services or background jobs:
268
+
269
+ ```typescript filename="src/services/formatter.ts"
270
+ import { sanitize } from 'exisjs/sanitize'
271
+
272
+ export function sanitizeTitle(input: string): string {
273
+ const stripped = sanitize.stripHtml(input)
274
+ const collapsed = sanitize.collapseWhitespace(stripped)
275
+ return sanitize.trim(collapsed)
276
+ }
277
+ ```
278
+
279
+ ### Available Sanitizers Reference
280
+
281
+ | Category | Function | Description |
282
+ | :--- | :--- | :--- |
283
+ | **Security** | `sanitize.escapeHtml(val)` | Escapes HTML entities (`&`, `<`, `>`, `"`, `'`). |
284
+ | **Security** | `sanitize.stripHtml(val)` | Removes HTML tags and element markup. |
285
+ | **Security** | `sanitize.preventSql(val)` | Neutralizes common SQL injection sequences. |
286
+ | **Security** | `sanitize.preventTraversal(val)` | Strips directory traversal sequences (`../`, `..\\`). |
287
+ | **Masking** | `sanitize.maskEmail(val)` | Masks email local-parts (e.g. `u***r@example.com`). |
288
+ | **Masking** | `sanitize.maskString(val)` | Masks characters leaving only boundary characters visible. |
289
+ | **Formatting** | `sanitize.trim(val)` | Trims leading and trailing whitespace. |
290
+ | **Formatting** | `sanitize.toLowerCase(val)` | Converts string to lowercase. |
291
+ | **Formatting** | `sanitize.toUpperCase(val)` | Converts string to uppercase. |
292
+ | **Formatting** | `sanitize.collapseWhitespace(val)` | Replaces consecutive whitespace with a single space. |
293
+ | **Formatting** | `sanitize.slugify(val)` | Converts string into URL-friendly slug format. |
294
+ | **Formatting** | `sanitize.truncate(length)(val)` | Truncates string to specified maximum length. |
295
+ | **Arrays & Objects** | `sanitize.dedupe(array)` | Removes duplicate elements using a `Set`. |
296
+ | **Arrays & Objects** | `sanitize.compact(array)` | Removes `null`, `undefined`, and empty string values. |
297
+ | **Arrays & Objects** | `sanitize.omit(keys)(obj)` | Omits specified keys from an object. |
298
+
299
+ ---
300
+
301
+ ## Custom and Asynchronous Refinements
302
+
303
+ Add synchronous validation checks with `.refine()` or asynchronous database/service checks with `.refineAsync()`.
304
+
305
+ ```typescript filename="src/http/auth/schema.ts"
306
+ import { tex } from 'exisjs/validator'
307
+ import { UserService } from '../../services/user.service'
308
+
309
+ export const RegisterSchema = tex.object({
310
+ email: tex.email().refineAsync(async (email) => {
311
+ const existing = await UserService.findByEmail(email)
312
+ return !existing // Returns false if email already exists
313
+ }, 'Email address is already in use'),
314
+
315
+ password: tex.password({ min: 8 }),
316
+ confirmPassword: tex.string(),
317
+ }).refine((data) => data.password === data.confirmPassword, {
318
+ message: 'Passwords must match',
319
+ path: 'confirmPassword',
320
+ })
321
+ ```
322
+
323
+ ---
324
+
325
+ ## Discriminated Unions (`tex.discriminatedUnion`)
326
+
327
+ When validating polymorphic payloads where the object structure depends on a discriminator tag field (e.g. event streams, action payloads, webhook types), use `tex.discriminatedUnion()`.
328
+
329
+ ```typescript filename="src/http/events/schema.ts"
201
330
  import { tex } from 'exisjs/validator'
202
331
  import type { ResolveSchema } from 'exisjs/validator'
203
332
 
204
- // 1. Define your schema
205
- const CreateBookSchema = tex.object({
206
- title: tex.string(),
207
- caption: tex.string(),
208
- rating: tex.number(),
209
- image: tex.string(),
333
+ const CreateEvent = tex.object({
334
+ type: tex.literal('create'),
335
+ payload: tex.object({
336
+ username: tex.string(),
337
+ email: tex.email(),
338
+ }),
210
339
  })
211
340
 
212
- // 2. Infer the TypeScript type!
213
- type CreateBookDto = ResolveSchema<typeof CreateBookSchema>
341
+ const UpdateEvent = tex.object({
342
+ type: tex.literal('update'),
343
+ payload: tex.object({
344
+ id: tex.uuid(),
345
+ fields: tex.record(tex.any()),
346
+ }),
347
+ })
214
348
 
215
- @Use(protectRoute)
216
- @Controller()
217
- export default class BooksController {
218
-
219
- // 3. Inject the schema directly into the @Body() decorator!
220
- @Post('/')
221
- async create(@Body(CreateBookSchema) body: CreateBookDto, @Req() req: any) {
222
- // If validation fails, this block is never reached!
223
- // body is strictly typed as CreateBookDto
224
-
225
- const newBook = new Book({
226
- title: body.title,
227
- caption: body.caption,
228
- rating: body.rating,
229
- image: body.image,
230
- user: req.user._id,
231
- })
232
-
233
- await newBook.save()
234
- return newBook
235
- }
349
+ const DeleteEvent = tex.object({
350
+ type: tex.literal('delete'),
351
+ payload: tex.object({
352
+ id: tex.uuid(),
353
+ reason: tex.string({ optional: true }),
354
+ }),
355
+ })
356
+
357
+ export const EventSchema = tex.discriminatedUnion('type', [
358
+ CreateEvent,
359
+ UpdateEvent,
360
+ DeleteEvent,
361
+ ])
362
+
363
+ export type WebhookEvent = ResolveSchema<typeof EventSchema>
364
+ ```
365
+
366
+ When validating, `tex.discriminatedUnion` matches the discriminator property against variant schemas. If the discriminator is missing or does not match any registered variant, ExisJS returns an explicit diagnostic error:
367
+
368
+ ```json filename="Discriminator Error Example"
369
+ {
370
+ "statusCode": 400,
371
+ "error": "Bad Request",
372
+ "message": "Validation Error: type: Invalid discriminator value 'archive' for field 'type'",
373
+ "details": [
374
+ {
375
+ "path": "type",
376
+ "message": "Invalid discriminator value 'archive' for field 'type'",
377
+ "expected": "one of matching variant discriminators",
378
+ "received": "'archive'",
379
+ "code": "INVALID_DISCRIMINATOR_VALUE"
380
+ }
381
+ ]
236
382
  }
237
383
  ```
238
384
 
385
+ ---
386
+
387
+ ## Schema Composition (`extend`, `merge`, `pick`, `omit`)
388
+
389
+ Derive and reuse schemas across endpoints using composition methods on `TexEngine`:
239
390
 
240
- ### Nested Sub-Documents & Array Schemas
391
+ ### 1. Extending Schemas (`.extend()`)
241
392
 
242
- ExisJS natively validates arbitrarily deep sub-documents and nested arrays off-heap with Rust acceleration:
393
+ Add or override fields on an existing schema:
243
394
 
244
395
  ```typescript filename="src/http/users/schema.ts"
245
- import { tex } from 'exisjs/validator'
396
+ const BaseUser = tex.object({
397
+ id: tex.uuid(),
398
+ name: tex.string({ min: 2 }),
399
+ email: tex.email(),
400
+ })
246
401
 
247
- export const RegisterUserSchema = tex.object({
248
- username: tex.string({ min: 3 }),
249
- profile: tex.object({
250
- bio: tex.string().nullish(), // allows undefined, null, or ""
251
- avatarUrl: tex.string({ optional: true }),
252
- socials: tex.object({
253
- twitter: tex.string({ optional: true }),
254
- github: tex.string({ optional: true }),
255
- }),
256
- tags: tex.array(tex.string({ min: 2 })),
257
- }),
258
- // You can use tex.object(...) OR a plain object schema shorthand!
259
- items: tex.array({
260
- sku: tex.string(),
261
- quantity: tex.number({ min: 1 }),
262
- }),
402
+ export const AdminUser = BaseUser.extend({
403
+ role: tex.enum(['admin', 'superadmin']),
404
+ permissions: tex.array(tex.string()),
263
405
  })
264
406
  ```
265
407
 
266
- #### First-Class Form & Multipart Support
267
- - **Single-Object Coercion**: When HTML forms or mobile clients submit a single item instead of an array (e.g. `{ items: { sku: "ABC", quantity: 1 } }`), the Rust engine automatically coerces it into a single-element array `[ { sku: "ABC", quantity: 1 } ]` rather than throwing `"Must be an array"`.
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.
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`.
408
+ ### 2. Merging Schemas (`.merge()`)
270
409
 
271
- ### Query String Array & Date Coercion
410
+ Combine two independent `TexEngine` schemas into a unified object definition:
272
411
 
273
- Query strings in HTTP cannot natively represent arrays or date instances. ExisJS solves this elegantly through `{ coerce: true }`:
412
+ ```typescript filename="src/http/posts/schema.ts"
413
+ const TimestampSchema = tex.object({
414
+ createdAt: tex.date({ coerce: true }),
415
+ updatedAt: tex.date({ optional: true, coerce: true }),
416
+ })
274
417
 
275
- #### Functional Paradigm
418
+ const PostContentSchema = tex.object({
419
+ title: tex.string({ min: 5 }),
420
+ body: tex.string(),
421
+ })
276
422
 
277
- ```typescript filename="src/http/events/route.ts"
423
+ export const FullPostSchema = PostContentSchema.merge(TimestampSchema)
424
+ ```
425
+
426
+ ### 3. Picking and Omitting Fields (`.pick()`, `.omit()`)
427
+
428
+ Construct derived views for public or partial operations:
429
+
430
+ ```typescript filename="src/http/users/schema.ts"
431
+ // Select only specific fields
432
+ export const PublicProfile = BaseUser.pick(['name', 'email'])
433
+
434
+ // Omit internal or sensitive fields
435
+ export const CreateUser = BaseUser.omit(['id'])
436
+ ```
437
+
438
+ ---
439
+
440
+ ## Explicit Type Coercion (`tex.coerce`)
441
+
442
+ For query strings, path parameters, and headers where HTTP transports deliver values as strings, use the `tex.coerce` namespace to coerce inputs into numbers, booleans, dates, or strings:
443
+
444
+ ```typescript filename="src/http/products/route.ts"
278
445
  import { controller, route } from 'exisjs/router'
279
446
  import { tex } from 'exisjs/validator'
280
447
 
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 })
448
+ const ProductFilterQuery = tex.object({
449
+ page: tex.coerce.number({ min: 1, default: 1 }),
450
+ limit: tex.coerce.number({ min: 1, max: 100, default: 20 }),
451
+ inStock: tex.coerce.boolean({ optional: true }),
452
+ createdAfter: tex.coerce.date({ optional: true }),
453
+ category: tex.coerce.string({ optional: true }),
292
454
  })
293
455
 
294
456
  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
- })
457
+ list: route.get('/', {
458
+ query: ProductFilterQuery,
459
+ async handle({ query, res }) {
460
+ // query.page is number (3)
461
+ // query.inStock is boolean (true)
462
+ // query.createdAfter is Date instance
463
+ res.json({ query })
464
+ },
465
+ }),
304
466
  })
305
467
  ```
306
468
 
307
- #### Class-Based (OOP) Paradigm
469
+ ---
308
470
 
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'
471
+ ## Data Transformations (`transform`)
313
472
 
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 }),
473
+ Apply post-validation data transformations at both the individual field level and the complete schema level:
474
+
475
+ ### Field-Level Transformations (`TexType.transform`)
476
+
477
+ ```typescript filename="src/http/auth/schema.ts"
478
+ export const LoginSchema = tex.object({
479
+ email: tex.string().transform((val) => val.toLowerCase().trim()),
480
+ apiKey: tex.string().transform((key) => Buffer.from(key, 'base64').toString('utf-8')),
319
481
  })
482
+ ```
320
483
 
321
- type FilterDto = ResolveSchema<typeof FilterSchema>
484
+ ### Schema-Level Transformations (`TexEngine.transform`)
322
485
 
323
- @Controller('/events')
324
- export default class EventsController {
325
- @Get('/', { query: FilterSchema })
326
- listEvents(@Query() query: FilterDto) {
327
- return { filter: query }
328
- }
329
- }
486
+ ```typescript filename="src/http/users/schema.ts"
487
+ export const UserDtoSchema = tex.object({
488
+ firstName: tex.string(),
489
+ lastName: tex.string(),
490
+ birthYear: tex.number(),
491
+ }).transform((user) => ({
492
+ fullName: `${user.firstName} ${user.lastName}`,
493
+ age: new Date().getFullYear() - user.birthYear,
494
+ }))
330
495
  ```
331
496
 
332
497
  ---
333
498
 
334
- ## Pagination Schemas (`tex.pagination`)
499
+ ## Field Documentation Metadata (`describe`, `example`)
335
500
 
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:
501
+ Attach human-readable descriptions and example values to schema fields. These are automatically extracted by the ExisJS Swagger / OpenAPI 3.1 generator:
337
502
 
338
- #### Functional Paradigm
503
+ ```typescript filename="src/http/orders/schema.ts"
504
+ export const CreateOrderSchema = tex.object({
505
+ itemSku: tex.string()
506
+ .describe('Stock keeping unit identifier')
507
+ .example('SKU-9872-BLU'),
508
+ quantity: tex.number({ min: 1 })
509
+ .describe('Number of units ordered')
510
+ .example(2),
511
+ })
512
+ ```
513
+
514
+ Use `tex.pagination()` to standardize query pagination parameters (`page`, `limit`, `sort`, `order`). Pair it with `getPaginationSkip()` and `paginate()` from `exisjs/validator`.
339
515
 
340
516
  ```typescript filename="src/http/items/route.ts"
341
517
  import { controller, route } from 'exisjs/router'
342
518
  import { tex, paginate, getPaginationSkip } from 'exisjs/validator'
343
519
 
344
- const ItemQuerySchema = tex.pagination({
345
- defaultLimit: 20, // Default items per page if omitted
346
- maxLimit: 100, // Hard cap to prevent memory exhaustion
520
+ const PaginationQuery = tex.pagination({
521
+ defaultLimit: 20,
522
+ maxLimit: 100,
347
523
  })
348
524
 
349
525
  export default controller({
350
526
  list: route.get('/', {
351
- query: ItemQuerySchema,
352
- async handle({ query }) {
527
+ query: PaginationQuery,
528
+ async handle({ query, res }) {
353
529
  const { skip, limit, page } = getPaginationSkip(query)
354
- const items = await fetchItems({ skip, limit, sort: query.sort, order: query.order })
355
- const total = await countItems()
356
-
357
- return paginate(items, total, { page, limit })
358
- }
359
- })
530
+ const [items, total] = await Promise.all([
531
+ db.items.findMany({ skip, take: limit, orderBy: { [query.sort || 'createdAt']: query.order || 'desc' } }),
532
+ db.items.count(),
533
+ ])
534
+
535
+ res.json(paginate(items, total, { page, limit }))
536
+ },
537
+ }),
360
538
  })
361
539
  ```
362
540
 
363
- #### Class-Based (OOP) Paradigm
541
+ ### Paginated Response Structure
364
542
 
365
- ```typescript filename="src/http/items/route.ts"
366
- import { Controller, Get, Query } from 'exisjs/decorators'
367
- import { tex, paginate, getPaginationSkip } from 'exisjs/validator'
543
+ ```json
544
+ {
545
+ "data": [
546
+ { "id": "1", "name": "Item 1" }
547
+ ],
548
+ "pagination": {
549
+ "page": 1,
550
+ "limit": 20,
551
+ "total": 45,
552
+ "totalPages": 3,
553
+ "hasNextPage": true,
554
+ "hasPrevPage": false,
555
+ "hasNext": true,
556
+ "hasPrev": false
557
+ }
558
+ }
559
+ ```
368
560
 
369
- const ItemQuerySchema = tex.pagination({
370
- defaultLimit: 20,
371
- maxLimit: 100,
372
- })
561
+ ---
562
+
563
+ ## Environment Variable Validation (`tex.env`)
373
564
 
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()
565
+ Use `tex.env()` in `src/config/env.ts` to validate environment variables during application startup. It automatically enables type coercion for numbers, booleans, dates, and arrays.
381
566
 
382
- return paginate(items, total, { page, limit })
383
- }
384
- }
567
+ ```typescript filename="src/config/env.ts"
568
+ import { tex } from 'exisjs/validator'
569
+
570
+ export const env = tex.env({
571
+ PORT: tex.number({ default: 3000 }),
572
+ NODE_ENV: tex.enum(['development', 'production', 'test'], { default: 'development' }),
573
+ DATABASE_URL: tex.string(),
574
+ JWT_SECRET: tex.string({ min: 32 }),
575
+ ALLOWED_ORIGINS: tex.array(tex.string()),
576
+ ENABLE_METRICS: tex.boolean({ default: false }),
577
+ }).parse(process.env)
385
578
  ```
386
579
 
580
+ If any required variable is missing or invalid, `tex.env().parse()` throws during initialization and logs the error descriptors before the server accepts traffic.
581
+
387
582
  ---
388
583
 
389
- ## Error Handling & Developer Diagnostics
584
+ ## Error Response Format
390
585
 
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:
586
+ When incoming requests fail validation, ExisJS returns an HTTP `400 Bad Request` with structured error details:
392
587
 
393
- ```json filename="HTTP 400 Bad Request Response"
588
+ ```json filename="HTTP 400 Bad Request"
394
589
  {
395
590
  "statusCode": 400,
396
591
  "error": "Bad Request",
397
- "message": "Validation Error: username: Field must be at least 4 characters, profile.tags: Expected array of strings",
592
+ "message": "Validation Error: username: Field must be at least 4 characters, email: Invalid email address",
398
593
  "errors": {
399
594
  "username": "Field must be at least 4 characters",
400
- "profile.tags": "Expected array of strings"
595
+ "email": "Invalid email address"
401
596
  },
402
597
  "details": [
403
598
  {
@@ -408,65 +603,25 @@ If a request fails validation, ExisJS intercepts the rejection and returns a sta
408
603
  "code": "VALUE_TOO_SMALL"
409
604
  },
410
605
  {
411
- "path": "profile.tags",
412
- "message": "Expected array of strings",
413
- "expected": "Array of string",
414
- "received": "number",
415
- "code": "INVALID_TYPE"
606
+ "path": "email",
607
+ "message": "Invalid email address",
608
+ "expected": "valid email format",
609
+ "received": "invalid-email",
610
+ "code": "INVALID_EMAIL"
416
611
  }
417
612
  ]
418
613
  }
419
614
  ```
420
615
 
421
- ### Dev Mode Terminal Diagnostics
422
-
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:
424
-
425
- ```text
426
- [VALIDATION FAILED] POST /api/v1/visits (part: body)
427
- ┌─────────────┬───────────────────────────┬──────────────────┐
428
- │ Field │ Received │ Expected │
429
- ├─────────────┼───────────────────────────┼──────────────────┤
430
- │ phone │ "123" │ min: 7, max: 20 │
431
- │ idProofs │ "[object Object]" │ Array of IDProof │
432
- └─────────────┴───────────────────────────┴──────────────────┘
433
- ```
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.
616
+ In development mode (`NODE_ENV=development`), ExisJS also outputs a tabular summary of the failed validation constraints to the terminal.
458
617
 
459
618
  ---
460
619
 
461
- ## Third-Party Validator Interoperability (Zod, Valibot, ArkType)
620
+ ## Third-Party Schema Interoperability
462
621
 
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.
622
+ ExisJS supports third-party schema libraries (such as **Zod**, **Valibot**, and **ArkType**) via duck typing. Any schema exposing a `.parse()` or `.safeParse()` method can be assigned directly to route configurations or decorators.
464
623
 
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
624
+ ### Using Zod in Functional Routes
470
625
 
471
626
  ```typescript filename="src/http/users/route.ts"
472
627
  import { controller, route } from 'exisjs/router'
@@ -475,20 +630,20 @@ import { z } from 'zod'
475
630
  const CreateUserSchema = z.object({
476
631
  name: z.string().min(3),
477
632
  email: z.string().email(),
478
- role: z.enum(['admin', 'user']),
633
+ role: z.enum(['admin', 'member']),
479
634
  })
480
635
 
481
636
  export default controller({
482
637
  create: route.post('/', {
483
- body: CreateUserSchema, // 👈 Directly pass your Zod schema!
638
+ body: CreateUserSchema,
484
639
  async handle({ body, res }) {
485
- return res.status(201).json({ success: true, user: body })
486
- }
487
- })
640
+ res.status(201).json({ success: true, user: body })
641
+ },
642
+ }),
488
643
  })
489
644
  ```
490
645
 
491
- #### Class-Based (OOP) Paradigm
646
+ ### Using Zod in OOP Controllers
492
647
 
493
648
  ```typescript filename="src/http/users/route.ts"
494
649
  import { Controller, Post, Body, HttpCode } from 'exisjs/decorators'
@@ -497,7 +652,7 @@ import { z } from 'zod'
497
652
  const CreateUserSchema = z.object({
498
653
  name: z.string().min(3),
499
654
  email: z.string().email(),
500
- role: z.enum(['admin', 'user']),
655
+ role: z.enum(['admin', 'member']),
501
656
  })
502
657
 
503
658
  type CreateUserDto = z.infer<typeof CreateUserSchema>
@@ -512,21 +667,4 @@ export default class UsersController {
512
667
  }
513
668
  ```
514
669
 
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
-
670
+ ExisJS normalizes error outputs from third-party schema libraries into the standard `400 Bad Request` JSON error envelope.