exisjs 0.7.12 → 0.7.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/README.md +195 -39
  2. package/dist/cli/commands/run.d.ts.map +1 -1
  3. package/dist/cli/commands/run.js +3 -1
  4. package/dist/cli/commands/run.js.map +1 -1
  5. package/dist/config/types.d.ts +2 -2
  6. package/dist/config/types.d.ts.map +1 -1
  7. package/dist/decorators/params.d.ts +21 -0
  8. package/dist/decorators/params.d.ts.map +1 -1
  9. package/dist/decorators/params.js +21 -1
  10. package/dist/decorators/params.js.map +1 -1
  11. package/dist/di/container.d.ts +15 -6
  12. package/dist/di/container.d.ts.map +1 -1
  13. package/dist/di/container.js +26 -3
  14. package/dist/di/container.js.map +1 -1
  15. package/dist/di/index.d.ts +1 -1
  16. package/dist/di/index.d.ts.map +1 -1
  17. package/dist/di/index.js.map +1 -1
  18. package/dist/di/inject.d.ts +2 -1
  19. package/dist/di/inject.d.ts.map +1 -1
  20. package/dist/di/inject.js +2 -1
  21. package/dist/di/inject.js.map +1 -1
  22. package/dist/docs/ARCHITECTURE.md +130 -78
  23. package/dist/docs/BENCHMARKS.md +4 -4
  24. package/dist/docs/content/README.md +43 -44
  25. package/dist/docs/content/boundaries.mdx +138 -61
  26. package/dist/docs/content/cli/commands.mdx +151 -128
  27. package/dist/docs/content/cli/overview.mdx +61 -47
  28. package/dist/docs/content/cli/usage.mdx +107 -30
  29. package/dist/docs/content/config.mdx +145 -158
  30. package/dist/docs/content/controllers.mdx +193 -464
  31. package/dist/docs/content/cron.mdx +183 -69
  32. package/dist/docs/content/decorators.mdx +449 -154
  33. package/dist/docs/content/errors.mdx +202 -305
  34. package/dist/docs/content/exisjs-mcp.mdx +59 -49
  35. package/dist/docs/content/installation.mdx +243 -157
  36. package/dist/docs/content/introduction.mdx +220 -48
  37. package/dist/docs/content/middleware.mdx +171 -497
  38. package/dist/docs/content/modules.mdx +141 -83
  39. package/dist/docs/content/providers.mdx +216 -180
  40. package/dist/docs/content/requests.mdx +168 -488
  41. package/dist/docs/content/responses.mdx +200 -533
  42. package/dist/docs/content/routing.mdx +235 -598
  43. package/dist/docs/content/security.mdx +233 -57
  44. package/dist/docs/content/structure.mdx +151 -240
  45. package/dist/docs/content/swagger.mdx +192 -91
  46. package/dist/docs/content/testing.mdx +205 -74
  47. package/dist/docs/content/validation.mdx +479 -341
  48. package/dist/docs/reference/README.md +34 -2
  49. package/dist/docs/reference/config.mdx +277 -75
  50. package/dist/docs/reference/context.mdx +197 -106
  51. package/dist/docs/reference/cron.mdx +266 -152
  52. package/dist/docs/reference/database.mdx +135 -53
  53. package/dist/docs/reference/decorators.mdx +234 -116
  54. package/dist/docs/reference/di.mdx +146 -112
  55. package/dist/docs/reference/error.mdx +155 -178
  56. package/dist/docs/reference/exis.mdx +141 -70
  57. package/dist/docs/reference/logger.mdx +110 -126
  58. package/dist/docs/reference/middleware.mdx +235 -158
  59. package/dist/docs/reference/plugin.mdx +136 -64
  60. package/dist/docs/reference/router.mdx +275 -293
  61. package/dist/docs/reference/sanitize.mdx +91 -87
  62. package/dist/docs/reference/swagger.mdx +280 -100
  63. package/dist/docs/reference/testing.mdx +223 -89
  64. package/dist/docs/reference/validator.mdx +194 -206
  65. package/dist/lib/start-server.js +22 -8
  66. package/dist/lib/start-server.js.map +1 -1
  67. package/dist/module/module.d.ts +6 -3
  68. package/dist/module/module.d.ts.map +1 -1
  69. package/dist/module/module.js +17 -3
  70. package/dist/module/module.js.map +1 -1
  71. package/dist/router/controller-registrar.d.ts.map +1 -1
  72. package/dist/router/controller-registrar.js +26 -4
  73. package/dist/router/controller-registrar.js.map +1 -1
  74. package/dist/router/scanner/fs-scanner.d.ts.map +1 -1
  75. package/dist/router/scanner/fs-scanner.js +22 -0
  76. package/dist/router/scanner/fs-scanner.js.map +1 -1
  77. package/dist/router/types.d.ts +6 -4
  78. package/dist/router/types.d.ts.map +1 -1
  79. package/dist/server/app.d.ts +3 -1
  80. package/dist/server/app.d.ts.map +1 -1
  81. package/dist/server/app.js +24 -8
  82. package/dist/server/app.js.map +1 -1
  83. package/dist/server/bootstrapper.d.ts.map +1 -1
  84. package/dist/server/bootstrapper.js +19 -0
  85. package/dist/server/bootstrapper.js.map +1 -1
  86. package/dist/server/request.d.ts +2 -0
  87. package/dist/server/request.d.ts.map +1 -1
  88. package/dist/server/request.js +11 -0
  89. package/dist/server/request.js.map +1 -1
  90. package/dist/server/response.d.ts +10 -5
  91. package/dist/server/response.d.ts.map +1 -1
  92. package/dist/server/response.js +65 -18
  93. package/dist/server/response.js.map +1 -1
  94. package/dist/swagger/generator.d.ts.map +1 -1
  95. package/dist/swagger/generator.js +130 -10
  96. package/dist/swagger/generator.js.map +1 -1
  97. package/dist/telemetry/index.d.ts +12 -0
  98. package/dist/telemetry/index.d.ts.map +1 -0
  99. package/dist/telemetry/index.js +121 -0
  100. package/dist/telemetry/index.js.map +1 -0
  101. package/dist/testing/client.d.ts +1 -0
  102. package/dist/testing/client.d.ts.map +1 -1
  103. package/dist/testing/client.js +4 -1
  104. package/dist/testing/client.js.map +1 -1
  105. package/dist/testing/index.d.ts.map +1 -1
  106. package/dist/testing/index.js +11 -4
  107. package/dist/testing/index.js.map +1 -1
  108. package/dist/validator/builder.d.ts +26 -1
  109. package/dist/validator/builder.d.ts.map +1 -1
  110. package/dist/validator/builder.js +31 -0
  111. package/dist/validator/builder.js.map +1 -1
  112. package/dist/validator/engine.d.ts +23 -0
  113. package/dist/validator/engine.d.ts.map +1 -1
  114. package/dist/validator/engine.js +228 -31
  115. package/dist/validator/engine.js.map +1 -1
  116. package/dist/validator/index.d.ts +8 -4
  117. package/dist/validator/index.d.ts.map +1 -1
  118. package/dist/validator/index.js +4 -1
  119. package/dist/validator/index.js.map +1 -1
  120. package/dist/validator/tex-types.d.ts +19 -4
  121. package/dist/validator/tex-types.d.ts.map +1 -1
  122. package/dist/validator/tex-types.js +102 -0
  123. package/dist/validator/tex-types.js.map +1 -1
  124. package/dist/validator/tex.d.ts +1 -1
  125. package/dist/validator/tex.d.ts.map +1 -1
  126. package/dist/validator/tex.js +3 -1
  127. package/dist/validator/tex.js.map +1 -1
  128. package/dist/validator/types.d.ts +1 -0
  129. package/dist/validator/types.d.ts.map +1 -1
  130. package/package.json +46 -14
  131. package/dist/docs/content/dataloader.mdx +0 -170
  132. package/dist/docs/reference/dataloader.mdx +0 -91
@@ -1,35 +1,88 @@
1
1
  ---
2
- nextUrl: "/docs/websockets"
3
- nextTitle: "WebSockets"
2
+ title: "Error Handling and Exceptions"
3
+ description: "Standardize HTTP errors, exception filters, validation diagnostics, and uncaught exception handling in ExisJS."
4
+ nextUrl: "/docs/cron"
5
+ nextTitle: "Cron & Background Tasks"
4
6
  ---
5
7
 
6
8
  # Error Handling
7
9
 
8
- ExisJS implements a comprehensive error boundary and observability system. The framework is designed to reduce the need to manually construct HTTP error responses.
10
+ ExisJS provides an integrated exception architecture designed to standardize HTTP error responses, handle validation failures, and safely catch uncaught runtime exceptions across both functional and class-based applications.
9
11
 
10
- ## The `HttpError` System
12
+ ---
13
+
14
+ ## The `HttpError` Hierarchy
15
+
16
+ All HTTP-related exceptions in ExisJS inherit from the base `HttpError` class. When an `HttpError` is thrown within a route handler or middleware, the router intercepts it and formats a consistent JSON error response with appropriate HTTP status codes and headers.
17
+
18
+ ### Base `HttpError` Class
19
+
20
+ ```typescript
21
+ export class HttpError extends Error {
22
+ public readonly statusCode: number
23
+ public readonly code: string
24
+ public readonly isOperational: boolean
25
+ public readonly details?: unknown
26
+
27
+ constructor(
28
+ message: string,
29
+ statusCode = 500,
30
+ code = 'INTERNAL_ERROR',
31
+ details?: unknown
32
+ )
33
+ }
34
+ ```
35
+
36
+ - **`statusCode`**: The HTTP status code returned to the client (e.g., `404`, `401`, `500`).
37
+ - **`code`**: A machine-readable uppercase string identifying the error category (e.g., `'NOT_FOUND'`, `'BAD_REQUEST'`).
38
+ - **`isOperational`**: A boolean flag (`true` for `HttpError`) indicating expected operational errors versus programmer bugs or unhandled runtime crashes.
39
+ - **`details`**: Optional structured context or validation metadata attached to the error.
40
+
41
+ ---
42
+
43
+ ## Standard Error Classes & Exception Aliases
44
+
45
+ ExisJS exports dedicated error classes alongside standard `*Exception` aliases for compatibility with both idiomatic TypeScript and NestJS-style OOP architectures.
46
+
47
+ | HTTP Status | Error Class | Exception Alias | Default Error Code | Static Factory Method |
48
+ | :--- | :--- | :--- | :--- | :--- |
49
+ | **`400`** | `BadRequestError` | `BadRequestException` | `BAD_REQUEST` | `HttpError.badRequest(msg, details?)` |
50
+ | **`401`** | `UnauthorizedError` | `UnauthorizedException` | `UNAUTHORIZED` | `HttpError.unauthorized(msg?)` |
51
+ | **`403`** | `ForbiddenError` | `ForbiddenException` | `FORBIDDEN` | `HttpError.forbidden(msg?)` |
52
+ | **`404`** | `NotFoundError` | `NotFoundException` | `NOT_FOUND` | `HttpError.notFound(resource?)` |
53
+ | **`409`** | `ConflictError` | `ConflictException` | `CONFLICT` | `HttpError.conflict(msg)` |
54
+ | **`413`** | `PayloadTooLargeError` | `PayloadTooLargeException` | `PAYLOAD_TOO_LARGE` | `HttpError.payloadTooLarge(msg?, details?)` |
55
+ | **`422`** | `UnprocessableError` | `UnprocessableException` | `UNPROCESSABLE_ENTITY` | `HttpError.unprocessable(msg, details?)` |
56
+ | **`429`** | `RateLimitError` | `RateLimitException` | `RATE_LIMITED` | `HttpError.tooManyRequests(msg?)` |
57
+ | **`500`** | `InternalError` | `InternalException` | `INTERNAL_ERROR` | `HttpError.internal(msg?)` |
58
+ | **`503`** | `ServiceUnavailableError` | `ServiceUnavailableException` | `SERVICE_UNAVAILABLE` | `HttpError.serviceUnavailable(msg?)` |
59
+ | **`504`** | `GatewayTimeoutError` | `GatewayTimeoutException` | `GATEWAY_TIMEOUT` | `HttpError.gatewayTimeout(msg?)` |
60
+
61
+ ---
11
62
 
12
- You can throw an `HttpError` anywhere in your application, and the ExisJS router will automatically intercept it, log it, and format it into a standardized JSON response for the client.
63
+ ## Throwing Errors in Handlers
64
+
65
+ You can throw errors directly in controllers, route definitions, or services using either static factory methods, direct instantiation of error classes, or exception aliases.
13
66
 
14
67
  ### Functional Paradigm
15
68
 
16
69
  ```typescript filename="src/http/users/route.ts"
17
70
  import { controller, route } from 'exisjs/router'
18
- import { HttpError } from 'exisjs/error'
71
+ import { HttpError, NotFoundError, ForbiddenException } from 'exisjs/error'
19
72
 
20
73
  export default controller({
21
74
  getUser: route.get('/:id', {
22
75
  async handle({ params }) {
23
- const user = db.findUser(params.id)
76
+ const user = await db.users.findById(params.id)
24
77
 
25
78
  if (!user) {
26
- // This stops execution and returns a 404
27
- throw HttpError.notFound('User')
79
+ // Option 1: Static factory helper
80
+ throw HttpError.notFound('User')
28
81
  }
29
82
 
30
- if (user.isBanned) {
31
- // This returns a 403 Forbidden
32
- throw HttpError.forbidden('User is banned from the platform')
83
+ if (user.isSuspended) {
84
+ // Option 2: Direct exception alias
85
+ throw new ForbiddenException('User account has been suspended')
33
86
  }
34
87
 
35
88
  return { user }
@@ -42,22 +95,20 @@ export default controller({
42
95
 
43
96
  ```typescript filename="src/http/users/route.ts"
44
97
  import { Controller, Get, Param } from 'exisjs/decorators'
45
- import { HttpError } from 'exisjs/error'
98
+ import { NotFoundException, ForbiddenException } from 'exisjs/error'
46
99
 
47
- @Controller()
100
+ @Controller('/users')
48
101
  export default class UsersController {
49
102
  @Get('/:id')
50
103
  async getUser(@Param('id') id: string) {
51
- const user = db.findUser(id)
104
+ const user = await db.users.findById(id)
52
105
 
53
106
  if (!user) {
54
- // This stops execution and returns a 404
55
- throw HttpError.notFound('User')
107
+ throw new NotFoundException('User')
56
108
  }
57
109
 
58
- if (user.isBanned) {
59
- // This returns a 403 Forbidden
60
- throw HttpError.forbidden('User is banned from the platform')
110
+ if (user.isSuspended) {
111
+ throw new ForbiddenException('User account has been suspended')
61
112
  }
62
113
 
63
114
  return { user }
@@ -65,141 +116,109 @@ export default class UsersController {
65
116
  }
66
117
  ```
67
118
 
68
- ### Common Error Factories
69
-
70
- The `HttpError` class provides static factory methods for standard HTTP status codes:
71
-
72
- - `HttpError.badRequest('Message')` - 400
73
- - `HttpError.unauthorized('Message')` - 401
74
- - `HttpError.forbidden('Message')` - 403
75
- - `HttpError.notFound('Resource')` - 404
76
- - `HttpError.conflict('Message')` - 409
77
- - `HttpError.payloadTooLarge('Message')` - 413
78
- - `HttpError.unprocessable('Message', details)` - 422
79
- - `HttpError.rateLimit('Message')` - 429
80
- - `HttpError.internal('Message')` - 500
81
-
82
- ### Direct Error Classes & Exceptions
83
-
84
- If you prefer a class-based approach, ExisJS exports every standard error as its own class. We also provide `Exception` aliases for every error class. You can import and throw them directly:
85
-
86
- ```typescript filename="example.ts"
87
- import {
88
- NotFoundException,
89
- UnauthorizedException,
90
- BadRequestError,
91
- PayloadTooLargeError
92
- } from 'exisjs/error'
93
-
94
- export const uploadMedia = (req, res) => {
95
- if (req.headers['content-length'] > 10 * 1024 * 1024) {
96
- throw new PayloadTooLargeError('File exceeds 10MB limit')
97
- }
98
- }
99
- ```
119
+ ---
100
120
 
101
- ### The Standardized Error Response Envelope
121
+ ## Standard Error Response Format
102
122
 
103
- When an `HttpError` is thrown or a validation failure occurs, the client automatically receives a standardized JSON payload with status codes, error descriptions, and ISO timestamps:
123
+ When an `HttpError` is thrown, ExisJS returns a standardized JSON payload:
104
124
 
105
- ```json filename="HTTP Error Response Envelope"
125
+ ```json filename="HTTP 404 Response"
106
126
  {
107
127
  "success": false,
108
128
  "statusCode": 404,
109
129
  "error": "Not Found",
110
130
  "message": "User not found",
111
131
  "validationErrors": null,
112
- "timestamp": "2026-09-27T10:11:42.000Z"
132
+ "timestamp": "2026-10-02T12:45:00.000Z"
113
133
  }
114
134
  ```
115
135
 
116
- ### Validation Error Responses & Diagnostic Visibility
136
+ ### Validation Error Responses
117
137
 
118
- When schema validation fails (via `tex`, Zod, or Yup), ExisJS produces a typed `400 Bad Request` containing both a concise `validationErrors` (and `errors`) map for seamless frontend form field binding:
138
+ When schema validation fails (via `tex`, Zod, or Yup), ExisJS intercepts the validation failure and generates a structured `400 Bad Request` payload containing field-level error mappings:
119
139
 
120
- ```json filename="HTTP 400 Bad Request Response"
140
+ ```json filename="HTTP 400 Validation Error Response"
121
141
  {
122
142
  "success": false,
123
143
  "statusCode": 400,
124
144
  "error": "Bad Request",
125
- "message": "Validation Error: email: Invalid email format, age: Field must be >= 18",
145
+ "message": "Validation Error: email: Invalid email format, age: Minimum age is 18",
126
146
  "validationErrors": {
127
147
  "email": "Invalid email format",
128
- "age": "Field must be >= 18"
148
+ "age": "Minimum age is 18"
129
149
  },
130
150
  "errors": {
131
151
  "email": "Invalid email format",
132
- "age": "Field must be >= 18"
152
+ "age": "Minimum age is 18"
133
153
  },
134
- "timestamp": "2026-09-27T10:11:42.000Z"
154
+ "details": [
155
+ {
156
+ "field": "email",
157
+ "message": "Invalid email format",
158
+ "expected": "Valid email address",
159
+ "received": "invalid-email",
160
+ "code": "INVALID_EMAIL",
161
+ "httpPart": "body"
162
+ },
163
+ {
164
+ "field": "age",
165
+ "message": "Minimum age is 18",
166
+ "expected": ">= 18",
167
+ "received": "16",
168
+ "code": "MIN_VALUE",
169
+ "httpPart": "body"
170
+ }
171
+ ],
172
+ "timestamp": "2026-10-02T12:45:00.000Z"
135
173
  }
136
174
  ```
137
175
 
138
- #### Development Terminal Diagnostics
176
+ ### Development Terminal Diagnostics
139
177
 
140
- In development mode, ExisJS automatically outputs formatted tabular diagnostics to the terminal displaying the exact HTTP part, field, received value, and expected requirement:
178
+ In development mode (`NODE_ENV !== 'production'`), validation failures output a formatted diagnostic table to the console identifying the failed HTTP part (`body`, `query`, `params`, `headers`), field names, received values, and expected criteria:
141
179
 
142
180
  ```text
143
- [VALIDATION FAILED] POST /auth/register (part: body)
144
- ┌─────────────┬───────────────────────────┬──────────────────┐
145
- │ Field │ Received │ Expected │
146
- ├─────────────┼───────────────────────────┼──────────────────┤
147
- │ email │ "alice" │ Valid email │
148
- │ age │ "16" │ min: 18 │
149
- └─────────────┴───────────────────────────┴──────────────────┘
181
+ [Validation Failed] POST /auth/register (part: body)
182
+ ✖ email: Invalid email format
183
+ ✖ age: Minimum age is 18
184
+ ┌─────────────┬───────────────────────────┬──────────────────────────┐
185
+ │ Field │ Received │ Expected │
186
+ ├─────────────┼───────────────────────────┼──────────────────────────┤
187
+ │ email │ "invalid-email" │ Valid email address │
188
+ │ age │ "16" │ >= 18 │
189
+ └─────────────┴───────────────────────────┴──────────────────────────┘
150
190
  ```
151
191
 
152
- If you throw an unhandled exception (like a database connection failure or a `TypeError`), ExisJS catches it and securely returns a `500 Internal Server Error`, hiding the stack trace from the client in production mode while logging the full trace to your internal logger.
153
-
154
- ## Error Handling Decision Tree
155
-
156
- To keep your codebase clean and consistent, use this decision tree when deciding how to handle errors:
157
-
158
- 1. **Is it a known client error (e.g., missing parameter, forbidden)?**
159
- 👉 **Throw an `HttpError` directly.** Let the framework format the 4xx response automatically.
160
- 2. **Do you need to intercept an unexpected failure (e.g., Database Connection failure) globally?**
161
- 👉 **Use an Exception Filter (`catchError`) in `server.ts`.** Map the specific exception class to a 5xx response.
162
- 3. **Do you need to intercept errors only for a specific route group (e.g., Admin Panel)?**
163
- 👉 **Use Local Error Hooks (`onError`) in the gateway/controller.** Keep the logic isolated to that specific module.
164
- 4. **Is it standard 4-arg middleware from the Express ecosystem?**
165
- 👉 **Use `app.use(errorHandler)`** (where `errorHandler = (err, req, res, next) => {}`). ExisJS supports standard Connect/Express error middleware natively.
166
-
167
- ## Extended Error Handling
168
-
169
- ExisJS provides mechanisms for granular and centralized error handling.
170
-
171
- ### 1. Framework-Level Exception Handler (`src/http/error.ts`)
192
+ ---
172
193
 
173
- Instead of writing manual `try/catch` blocks in every route or wiring `app.onError` inside `server.ts`, ExisJS provides a dedicated file convention: **`src/http/error.ts`** (or `src/http/*.error.ts`).
194
+ ## Centralized Exception Handling (`src/http/error.ts`)
174
195
 
175
- When `src/http/error.ts` is present, ExisJS automatically discovers and mounts it during application boot as the global exception interceptor. Any uncaught exception, thrown `HttpError`, or validation failure anywhere in your application is routed to this file.
196
+ ExisJS supports an application-wide error handler convention located at `src/http/error.ts`. When this file is present, the framework routes all unhandled exceptions, validation errors, and uncaught rejections through this handler before sending the final HTTP response.
176
197
 
177
- #### Functional Paradigm
198
+ ### Functional Paradigm
178
199
 
179
200
  ```typescript filename="src/http/error.ts"
180
- import type { Request, Response } from 'exisjs'
181
- import { isDev } from 'exisjs/config'
201
+ import type { ExisRequest, ExisResponse } from 'exisjs'
182
202
 
183
203
  /**
184
- * Global Framework Exception Handler
185
- * Auto-mounted at startup to intercept all unhandled exceptions and validation errors.
204
+ * Global application error handler.
205
+ * Automatically mounted during server bootstrap.
186
206
  */
187
- export function onError(err: any, req: Request, res: Response) {
207
+ export function onError(err: any, req: ExisRequest, res: ExisResponse) {
188
208
  const status = err.statusCode || err.status || 500
189
- const isOperational = Boolean(err.isOperational || err.statusCode < 500)
209
+ const isOperational = Boolean(err.isOperational || status < 500)
190
210
 
191
- // 1. Log unexpected crashes in production to external APM (Sentry, Datadog, Axiom)
192
- if (!isOperational && !isDev) {
193
- // e.g. Sentry.captureException(err, { extra: { path: req.path, method: req.method } })
194
- req.log.error({ err }, '[Fatal] Unhandled Server Exception')
211
+ // Log non-operational server crashes to telemetry / APM
212
+ if (!isOperational && process.env.NODE_ENV === 'production') {
213
+ req.log.error({ err, path: req.path, method: req.method }, 'Unhandled server crash')
195
214
  }
196
215
 
197
- // 2. Format a unified, standard error envelope for client applications
198
216
  res.status(status).json({
199
217
  success: false,
218
+ statusCode: status,
200
219
  error: {
201
220
  code: err.code || (status === 500 ? 'INTERNAL_SERVER_ERROR' : 'ERROR'),
202
- message: err.message || 'Internal Server Error',
221
+ message: err.message || 'An unexpected error occurred',
203
222
  ...(err.details ? { details: err.details } : {}),
204
223
  ...(err.errors ? { errors: err.errors } : {}),
205
224
  path: req.path,
@@ -209,26 +228,26 @@ export function onError(err: any, req: Request, res: Response) {
209
228
  }
210
229
  ```
211
230
 
212
- #### Class-Based (OOP) Paradigm
231
+ ### Class-Based (OOP) Paradigm
213
232
 
214
233
  ```typescript filename="src/http/error.ts"
215
- import type { Request, Response } from 'exisjs'
216
- import { isDev } from 'exisjs/config'
234
+ import type { ExisRequest, ExisResponse } from 'exisjs'
217
235
 
218
236
  export default class GlobalErrorHandler {
219
- onError(err: any, req: Request, res: Response) {
237
+ onError(err: any, req: ExisRequest, res: ExisResponse) {
220
238
  const status = err.statusCode || err.status || 500
221
- const isOperational = Boolean(err.isOperational || err.statusCode < 500)
239
+ const isOperational = Boolean(err.isOperational || status < 500)
222
240
 
223
- if (!isOperational && !isDev) {
224
- req.log.error({ err }, '[Fatal] Unhandled Server Exception')
241
+ if (!isOperational && process.env.NODE_ENV === 'production') {
242
+ req.log.error({ err, path: req.path, method: req.method }, 'Unhandled server crash')
225
243
  }
226
244
 
227
245
  res.status(status).json({
228
246
  success: false,
247
+ statusCode: status,
229
248
  error: {
230
249
  code: err.code || (status === 500 ? 'INTERNAL_SERVER_ERROR' : 'ERROR'),
231
- message: err.message || 'Internal Server Error',
250
+ message: err.message || 'An unexpected error occurred',
232
251
  ...(err.details ? { details: err.details } : {}),
233
252
  path: req.path,
234
253
  timestamp: new Date().toISOString(),
@@ -238,249 +257,127 @@ export default class GlobalErrorHandler {
238
257
  }
239
258
  ```
240
259
 
241
- > [!TIP]
242
- > **Scaffold with CLI**: You can generate this file instantly using the ExisJS CLI:
243
- > ```bash
244
- > npx exis generate error
245
- > # or shorthand:
246
- > npx exis g error
247
- > ```
248
-
249
- #### What `src/http/error.ts` Intercepts:
250
- - **`HttpError` instances**: (e.g. `throw HttpError.notFound()`, `throw HttpError.unauthorized()`).
251
- - **Validation failures**: Zod, Tex, or Yup validation rejections automatically pass structured `err.errors` and `err.details`.
252
- - **Database / Network exceptions**: Postgres connection failures, timeout rejections, and third-party API errors.
253
- - **Syntax errors**: Malformed JSON bodies or bad request payloads.
254
-
260
+ ---
255
261
 
256
- ### 2. Exception Filters (`catchError`)
262
+ ## Exception Filters (`@Catch` & `catchError`)
257
263
 
258
- ExisJS allows you to create **Exception Filters** to globally intercept specific types of errors.
264
+ Exception filters allow you to bind error-handling logic to specific exception types, preventing repetitive `try/catch` boilerplate across service layers.
259
265
 
260
- Using the `catchError` middleware, you can map specific custom errors to custom responses globally:
266
+ ### Middleware-Based Exception Filter (`catchError`)
261
267
 
262
- ### Functional Paradigm
268
+ Use the `catchError` middleware to intercept specific error classes at the server or boundary level:
263
269
 
264
270
  ```typescript filename="src/http/server.ts"
265
271
  import { exis } from 'exisjs'
266
272
  import { catchError } from 'exisjs/middleware'
267
273
 
268
- export class DatabaseConnectionError extends Error {
269
- constructor(message: string) {
274
+ export class DatabaseTimeoutError extends Error {
275
+ constructor(message = 'Database query timed out') {
270
276
  super(message)
271
- this.name = 'DatabaseConnectionError'
277
+ this.name = 'DatabaseTimeoutError'
272
278
  }
273
279
  }
274
280
 
275
281
  export default exis({
276
282
  async onStart(app) {
277
- // Globally intercept DatabaseConnectionError
278
- app.use(catchError(DatabaseConnectionError, (err, req, res) => {
283
+ // Intercept DatabaseTimeoutError globally and map to a 503 response
284
+ app.use(catchError(DatabaseTimeoutError, (err, req, res) => {
279
285
  res.status(503).json({
280
286
  success: false,
281
- error: { code: 'DB_OFFLINE', message: err.message }
287
+ statusCode: 503,
288
+ error: {
289
+ code: 'DATABASE_UNAVAILABLE',
290
+ message: err.message,
291
+ },
282
292
  })
283
293
  }))
284
294
  }
285
295
  })
286
296
  ```
287
297
 
288
- ### Class-Based (OOP) Paradigm
298
+ ### OOP Exception Filters (`@Catch` & `ExceptionFilter`)
289
299
 
290
- ```typescript filename="src/http/server.ts"
291
- import { Server } from 'exisjs/decorators'
292
- import { catchError } from 'exisjs/middleware'
293
- import type { App } from 'exisjs'
300
+ In class-based applications, implement the `ExceptionFilter<T>` interface and bind it using `@UseFilters()` or `@Use()`:
294
301
 
295
- export class DatabaseConnectionError extends Error {
296
- constructor(message: string) {
297
- super(message)
298
- this.name = 'DatabaseConnectionError'
299
- }
300
- }
301
-
302
- @Server()
303
- export default class RootServer {
304
- async onStart(app: App) {
305
- // Globally intercept DatabaseConnectionError
306
- app.use(catchError(DatabaseConnectionError, (err, req, res) => {
307
- res.status(503).json({
308
- success: false,
309
- error: { code: 'DB_OFFLINE', message: err.message }
310
- })
311
- }))
312
- }
313
- }
314
- ```
315
-
316
- This prevents you from having to write `try/catch` blocks in every single route just to handle standard database failures.
317
-
318
- ### 2. Local Error Hooks (`onError`)
319
-
320
- If you want to catch errors only within a specific module or route grouping, you can use the `onError` hook directly in your controller definition.
321
-
322
- When a controller throws an error, the local `onError` hook will intercept it before it reaches the global error handler.
323
-
324
- ### Functional Paradigm
325
-
326
- ```typescript filename="src/http/users/route.ts"
327
- import { controller, route } from 'exisjs/router'
328
-
329
- export default controller({
330
- // This local hook catches errors thrown by the routes in this controller
331
- onError: (err, req, res) => {
332
- console.log('Users Controller Error Handler Caught:', err.message)
333
- res.status(400).json({ success: false, localError: err.message })
334
- },
335
-
336
- crash: route.get('/crash', async () => {
337
- // This error will be caught by the local onError hook
338
- throw new Error("Something went wrong in the users module")
339
- })
340
- })
341
- ```
342
-
343
- ### Class-Based (OOP) Paradigm
344
-
345
- You can use function handlers or structured class-based `ExceptionFilter` implementations with `@Catch()`:
346
-
347
- ```typescript filename="src/http/filters/not-found.filter.ts"
302
+ ```typescript filename="src/http/filters/database.filter.ts"
348
303
  import { Catch, type ExceptionFilter, type ArgumentsHost } from 'exisjs/decorators'
349
- import { NotFoundException } from 'exisjs/error'
304
+ import { DatabaseTimeoutError } from '../errors/database-timeout.error'
350
305
 
351
- @Catch(NotFoundException)
352
- export class NotFoundExceptionFilter implements ExceptionFilter<NotFoundException> {
353
- catch(exception: NotFoundException, host: ArgumentsHost) {
306
+ @Catch(DatabaseTimeoutError)
307
+ export class DatabaseExceptionFilter implements ExceptionFilter<DatabaseTimeoutError> {
308
+ catch(exception: DatabaseTimeoutError, host: ArgumentsHost) {
354
309
  const { res } = host
355
- res.status(404).json({
356
- statusCode: 404,
357
- error: 'Not Found',
358
- message: exception.message,
310
+
311
+ res.status(503).json({
312
+ success: false,
313
+ statusCode: 503,
314
+ error: {
315
+ code: 'DATABASE_TIMEOUT',
316
+ message: exception.message,
317
+ },
359
318
  timestamp: new Date().toISOString(),
360
319
  })
361
320
  }
362
321
  }
363
322
  ```
364
323
 
324
+ Attach the filter to a controller or specific route method:
325
+
365
326
  ```typescript filename="src/http/users/route.ts"
366
- import { Controller, Get, UseFilters, Use } from 'exisjs/decorators'
367
- import { NotFoundException } from 'exisjs/error'
368
- import { NotFoundExceptionFilter } from '../filters/not-found.filter'
327
+ import { Controller, Get, UseFilters } from 'exisjs/decorators'
328
+ import { DatabaseExceptionFilter } from '../filters/database.filter'
369
329
 
370
330
  @Controller('/users')
371
- @Use(NotFoundExceptionFilter) // or @UseFilters(NotFoundExceptionFilter)
331
+ @UseFilters(DatabaseExceptionFilter)
372
332
  export default class UsersController {
373
- @Get('/:id')
374
- async getUser() {
375
- throw new NotFoundException('User')
333
+ @Get('/metrics')
334
+ async getMetrics() {
335
+ // If this throws DatabaseTimeoutError, DatabaseExceptionFilter handles it
336
+ return await db.queryMetrics()
376
337
  }
377
338
  }
378
339
  ```
379
340
 
380
- By combining Exception Filters and Local Error Hooks, ExisJS gives you complete control over your error boundaries.
381
-
382
- ## Observability (BYOM/BYOT)
383
-
384
- ExisJS ships with tracing and metric adapters. Instead of tying you to a specific telemetry ecosystem, ExisJS lets you **Bring Your Own Metrics** (BYOM) and **Bring Your Own Tracer** (BYOT).
341
+ ---
385
342
 
386
- ### Metrics
343
+ ## Local Controller Error Handlers (`onError`)
387
344
 
388
- You can use the `metrics` middleware to track request durations and counts.
345
+ For localized error boundaries that apply only to routes within a single controller, define an `onError` hook directly on the controller.
389
346
 
390
347
  ### Functional Paradigm
391
348
 
392
- ```typescript filename="src/http/boundary.ts"
393
- import { defineBoundary } from 'exisjs/router'
394
- import { metrics } from 'exisjs/observability'
395
-
396
- const myPrometheusAdapter = {
397
- onRequestStart: ({ method, path }) => {
398
- // A request just started. Start a timer.
399
- return ({ statusCode, durationMs }) => {
400
- // The request just finished. Push to Prometheus.
401
- console.log(`[Metrics] ${method} ${path} took ${durationMs}ms with status ${statusCode}`)
402
- }
403
- }
404
- }
405
-
406
- export const config = defineBoundary({
407
- middleware: [metrics(myPrometheusAdapter)]
408
- })
409
- ```
410
-
411
- ### Class-Based (OOP) Paradigm
412
-
413
- ```typescript filename="src/http/boundary.ts"
414
- import { Boundary } from 'exisjs/decorators'
415
- import { metrics } from 'exisjs/observability'
416
-
417
- const myPrometheusAdapter = {
418
- onRequestStart: ({ method, path }) => {
419
- return ({ statusCode, durationMs }) => {
420
- console.log(`[Metrics] ${method} ${path} took ${durationMs}ms with status ${statusCode}`)
421
- }
422
- }
423
- }
424
-
425
- @Boundary({
426
- middleware: [metrics(myPrometheusAdapter)]
427
- })
428
- export default class RootBoundary {}
429
- ```
430
-
431
- *Note: ExisJS normalizes dynamic paths (e.g., turning `/users/123` into `/users/:id`) to prevent metrics cardinality issues.*
432
-
433
- ### OpenTelemetry Tracing
434
-
435
- ExisJS seamlessly integrates with OpenTelemetry (OTel) via the `tracing` middleware.
436
-
437
- ### Functional Paradigm
349
+ ```typescript filename="src/http/billing/route.ts"
350
+ import { controller, route } from 'exisjs/router'
438
351
 
439
- ```typescript filename="src/http/boundary.ts"
440
- import { defineBoundary } from 'exisjs/router'
441
- import { tracing } from 'exisjs/observability'
442
-
443
- const myOtelAdapter = {
444
- startActiveSpan: (name, metadata, callback) => {
445
- // Create an OTel span
446
- const span = tracer.startSpan(name)
447
- // Run the callback and pass the span proxy
448
- callback({
449
- setAttribute: (k, v) => span.setAttribute(k, v),
450
- setStatus: (status) => span.setStatus(status),
451
- recordException: (e) => span.recordException(e),
452
- end: () => span.end()
352
+ export default controller({
353
+ // Intercepts all errors thrown by routes in this controller
354
+ onError(err, req, res) {
355
+ req.log.warn({ err }, 'Billing operation failed')
356
+ res.status(402).json({
357
+ success: false,
358
+ statusCode: 402,
359
+ error: {
360
+ code: 'PAYMENT_REQUIRED',
361
+ message: err.message,
362
+ },
453
363
  })
454
- }
455
- }
364
+ },
456
365
 
457
- export const config = defineBoundary({
458
- middleware: [tracing(myOtelAdapter)]
366
+ checkout: route.post('/checkout', {
367
+ async handle({ body }) {
368
+ throw new Error('Insufficient funds')
369
+ }
370
+ })
459
371
  })
460
372
  ```
461
373
 
462
- ### Class-Based (OOP) Paradigm
374
+ ---
463
375
 
464
- ```typescript filename="src/http/boundary.ts"
465
- import { Boundary } from 'exisjs/decorators'
466
- import { tracing } from 'exisjs/observability'
467
-
468
- const myOtelAdapter = {
469
- startActiveSpan: (name, metadata, callback) => {
470
- const span = tracer.startSpan(name)
471
- callback({
472
- setAttribute: (k, v) => span.setAttribute(k, v),
473
- setStatus: (status) => span.setStatus(status),
474
- recordException: (e) => span.recordException(e),
475
- end: () => span.end()
476
- })
477
- }
478
- }
376
+ ## Unhandled Errors in Production vs Development
479
377
 
480
- @Boundary({
481
- middleware: [tracing(myOtelAdapter)]
482
- })
483
- export default class RootBoundary {}
484
- ```
378
+ ExisJS handles uncaught non-operational errors (such as unexpected `TypeError` or unhandled promise rejections) based on `NODE_ENV`:
485
379
 
486
- By providing these standard interfaces natively, your application metrics and traces are bound to the ExisJS router lifecycle.
380
+ - **Production (`NODE_ENV === 'production'`)**: Returns a generic `500 Internal Server Error` without leaking stack traces, internal variables, or system paths. The complete stack trace is written to the structured logger.
381
+ - **Development (`NODE_ENV !== 'production'`)**:
382
+ - For JSON clients (`Accept: application/json`): Returns the error message and complete stack trace in the JSON envelope.
383
+ - For browser clients (`Accept: text/html`): Renders an interactive diagnostic error overlay with visual code frames and file locations.