@venizia/ignis-docs 0.2.0 → 0.2.1-0

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 (142) hide show
  1. package/README.md +14 -14
  2. package/content/best-practices/api-usage-examples.md +39 -19
  3. package/content/best-practices/architectural-patterns.md +22 -11
  4. package/content/best-practices/architecture-decisions.md +29 -16
  5. package/content/best-practices/code-style-standards/advanced-patterns.md +43 -31
  6. package/content/best-practices/code-style-standards/constants-configuration.md +19 -4
  7. package/content/best-practices/code-style-standards/control-flow.md +33 -1
  8. package/content/best-practices/code-style-standards/documentation.md +28 -8
  9. package/content/best-practices/code-style-standards/function-patterns.md +15 -4
  10. package/content/best-practices/code-style-standards/index.md +7 -2
  11. package/content/best-practices/code-style-standards/naming-conventions.md +26 -2
  12. package/content/best-practices/code-style-standards/route-definitions.md +9 -5
  13. package/content/best-practices/code-style-standards/tooling.md +14 -1
  14. package/content/best-practices/code-style-standards/type-safety.md +39 -6
  15. package/content/best-practices/common-pitfalls.md +59 -35
  16. package/content/best-practices/contribution-workflow.md +6 -2
  17. package/content/best-practices/data-modeling.md +78 -62
  18. package/content/best-practices/deployment-strategies.md +56 -19
  19. package/content/best-practices/error-handling.md +182 -93
  20. package/content/best-practices/index.md +6 -0
  21. package/content/best-practices/performance-optimization.md +26 -13
  22. package/content/best-practices/security-guidelines.md +35 -9
  23. package/content/best-practices/testing-strategies.md +7 -2
  24. package/content/best-practices/troubleshooting-tips.md +27 -17
  25. package/content/extensions/components/api-reference.md +107 -322
  26. package/content/extensions/components/authentication/api.md +454 -603
  27. package/content/extensions/components/authentication/errors.md +121 -498
  28. package/content/extensions/components/authentication/index.md +88 -801
  29. package/content/extensions/components/authentication/usage.md +207 -956
  30. package/content/extensions/components/authorization/api.md +736 -656
  31. package/content/extensions/components/authorization/errors.md +168 -206
  32. package/content/extensions/components/authorization/index.md +82 -797
  33. package/content/extensions/components/authorization/usage.md +194 -527
  34. package/content/extensions/components/health-check.md +71 -243
  35. package/content/extensions/components/mail/api.md +504 -287
  36. package/content/extensions/components/mail/errors.md +73 -61
  37. package/content/extensions/components/mail/index.md +96 -467
  38. package/content/extensions/components/mail/usage.md +130 -172
  39. package/content/extensions/components/request-tracker.md +66 -173
  40. package/content/extensions/components/socket-io/api.md +195 -13
  41. package/content/extensions/components/socket-io/errors.md +3 -3
  42. package/content/extensions/components/socket-io/index.md +50 -337
  43. package/content/extensions/components/socket-io/usage.md +143 -26
  44. package/content/extensions/components/static-asset/api.md +410 -142
  45. package/content/extensions/components/static-asset/errors.md +110 -53
  46. package/content/extensions/components/static-asset/index.md +79 -608
  47. package/content/extensions/components/static-asset/usage.md +180 -300
  48. package/content/extensions/components/websocket/api.md +275 -399
  49. package/content/extensions/components/websocket/errors.md +47 -56
  50. package/content/extensions/components/websocket/index.md +74 -407
  51. package/content/extensions/components/websocket/usage.md +110 -341
  52. package/content/extensions/helpers/cron/index.md +51 -160
  53. package/content/extensions/helpers/crypto/index.md +62 -483
  54. package/content/extensions/helpers/crypto/reference.md +456 -0
  55. package/content/extensions/helpers/env/index.md +60 -178
  56. package/content/extensions/helpers/error/index.md +221 -207
  57. package/content/extensions/helpers/inversion/index.md +65 -556
  58. package/content/extensions/helpers/inversion/reference.md +522 -0
  59. package/content/extensions/helpers/kafka/admin.md +20 -1
  60. package/content/extensions/helpers/kafka/compile-binary.md +41 -35
  61. package/content/extensions/helpers/kafka/consumer.md +54 -20
  62. package/content/extensions/helpers/kafka/examples.md +21 -16
  63. package/content/extensions/helpers/kafka/index.md +80 -610
  64. package/content/extensions/helpers/kafka/producer.md +134 -5
  65. package/content/extensions/helpers/kafka/schema-registry.md +45 -70
  66. package/content/extensions/helpers/logger/hf-logger.md +193 -0
  67. package/content/extensions/helpers/logger/index.md +64 -563
  68. package/content/extensions/helpers/logger/pino.md +85 -0
  69. package/content/extensions/helpers/logger/reference.md +746 -0
  70. package/content/extensions/helpers/network/api.md +241 -195
  71. package/content/extensions/helpers/network/index.md +72 -530
  72. package/content/extensions/helpers/queue/index.md +72 -900
  73. package/content/extensions/helpers/queue/reference.md +467 -0
  74. package/content/extensions/helpers/redis/index.md +73 -645
  75. package/content/extensions/helpers/redis/reference.md +727 -0
  76. package/content/extensions/helpers/secrets/index.md +66 -0
  77. package/content/extensions/helpers/socket-io/api.md +305 -203
  78. package/content/extensions/helpers/socket-io/index.md +66 -432
  79. package/content/extensions/helpers/storage/api.md +564 -462
  80. package/content/extensions/helpers/storage/index.md +77 -573
  81. package/content/extensions/helpers/types/index.md +66 -499
  82. package/content/extensions/helpers/types/reference.md +650 -0
  83. package/content/extensions/helpers/uid/index.md +58 -227
  84. package/content/extensions/helpers/websocket/api.md +329 -216
  85. package/content/extensions/helpers/websocket/index.md +65 -503
  86. package/content/extensions/helpers/worker-thread/index.md +58 -396
  87. package/content/extensions/helpers/worker-thread/reference.md +428 -0
  88. package/content/guides/core-concepts/persistent/models.md +1 -1
  89. package/content/guides/core-concepts/persistent/search-typesense.md +3 -3
  90. package/content/guides/core-concepts/persistent/transactions.md +1 -1
  91. package/content/guides/core-concepts/secrets-vault.md +177 -0
  92. package/content/guides/core-concepts/services.md +1 -1
  93. package/content/guides/migrations/redis-helpers-migration.md +1 -1
  94. package/content/guides/migrations/unified-connectors-migration.md +2 -2
  95. package/content/guides/tutorials/ecommerce-api.md +3 -8
  96. package/content/references/base/application.md +1 -1
  97. package/content/references/base/connectors.md +79 -136
  98. package/content/references/base/datasources-reference.md +599 -0
  99. package/content/references/base/datasources.md +84 -444
  100. package/content/references/base/dependency-injection.md +17 -39
  101. package/content/references/base/filter-system/application-usage.md +69 -121
  102. package/content/references/base/filter-system/array-operators.md +12 -0
  103. package/content/references/base/filter-system/comparison-operators.md +12 -0
  104. package/content/references/base/filter-system/default-filter.md +136 -348
  105. package/content/references/base/filter-system/fields-order-pagination.md +38 -16
  106. package/content/references/base/filter-system/index.md +106 -257
  107. package/content/references/base/filter-system/json-filtering.md +12 -2
  108. package/content/references/base/filter-system/list-operators.md +16 -2
  109. package/content/references/base/filter-system/logical-operators.md +13 -0
  110. package/content/references/base/filter-system/null-operators.md +13 -0
  111. package/content/references/base/filter-system/pattern-matching.md +12 -0
  112. package/content/references/base/filter-system/quick-reference.md +11 -2
  113. package/content/references/base/filter-system/range-operators.md +12 -0
  114. package/content/references/base/filter-system/tips.md +70 -133
  115. package/content/references/base/filter-system/use-cases.md +156 -233
  116. package/content/references/base/middlewares.md +35 -21
  117. package/content/references/base/models-reference.md +886 -0
  118. package/content/references/base/models.md +80 -1452
  119. package/content/references/base/repositories/advanced.md +156 -192
  120. package/content/references/base/repositories/index.md +77 -650
  121. package/content/references/base/repositories/mixins.md +22 -18
  122. package/content/references/base/repositories/relations.md +123 -171
  123. package/content/references/base/repositories/soft-deletable.md +58 -56
  124. package/content/references/base/secrets.md +263 -0
  125. package/content/references/base/services.md +2 -2
  126. package/content/references/configuration/environment-variables.md +48 -4
  127. package/content/references/configuration/index.md +49 -31
  128. package/content/references/quick-reference.md +3 -16
  129. package/content/references/utilities/crypto.md +35 -76
  130. package/content/references/utilities/date.md +33 -73
  131. package/content/references/utilities/index.md +1 -1
  132. package/content/references/utilities/jsx-reference.md +298 -0
  133. package/content/references/utilities/jsx.md +82 -525
  134. package/content/references/utilities/module.md +29 -62
  135. package/content/references/utilities/parse.md +34 -64
  136. package/content/references/utilities/performance.md +33 -58
  137. package/content/references/utilities/promise.md +28 -62
  138. package/content/references/utilities/request.md +57 -218
  139. package/content/references/utilities/schema.md +43 -137
  140. package/content/references/utilities/statuses-reference.md +361 -0
  141. package/content/references/utilities/statuses.md +63 -667
  142. package/package.json +8 -8
@@ -24,24 +24,67 @@ throw getError({
24
24
  message: 'User not found',
25
25
  });
26
26
 
27
- // Error with details
27
+ // Error with context - any key the framework does not model lands in `extra`
28
28
  throw getError({
29
29
  statusCode: HTTP.ResultCodes.RS_4.BadRequest,
30
30
  message: 'Invalid request',
31
- details: {
32
- field: 'email',
33
- reason: 'Must be a valid email address',
34
- },
31
+ details: { field: 'email', reason: 'Must be a valid email address' },
32
+ });
33
+ // -> error.extra.details
34
+
35
+ // The same thing, explicit. Prefer this when the context could be mistaken for a field.
36
+ throw getError({
37
+ statusCode: HTTP.ResultCodes.RS_4.BadRequest,
38
+ message: 'Invalid request',
39
+ extra: { details: { field: 'email' } },
35
40
  });
36
41
 
37
- // Error with context (for logging)
42
+ // Wrapping a lower-level failure - `cause` reaches the native Error.cause
38
43
  throw getError({
39
44
  statusCode: HTTP.ResultCodes.RS_5.InternalServerError,
40
45
  message: '[UserService][create] Database connection failed',
41
- details: { userId: requestedId },
46
+ cause: error,
47
+ extra: { userId: requestedId },
42
48
  });
43
49
  ```
44
50
 
51
+ > [!NOTE]
52
+ > Any key `getError` does not model rides into `extra` - that is how a throw site attaches context the framework knows nothing about. The trade is that a **misspelling** goes the same way: `getError({ message, statuscode: 503 })` compiles, `statusCode` stays `400`, and `503` sits in `extra.statuscode`. The framework cannot tell your context from your typo.
53
+
54
+ ### Catalogue a domain failure, raise the rest free-form
55
+
56
+ The form above is right for a failure that carries no i18n code: an invariant, a misconfiguration, a seed guard. Nobody translates `'[UserService][create] Database connection failed'`.
57
+
58
+ A **domain** failure - one a client localizes and branches on - belongs in a catalog instead. Retyping its code and status at each throw is how two call sites end up raising `category.create.duplicate_name` and `category.duplicate_name` for the same thing:
59
+
60
+ ```typescript
61
+ import { ErrorScopes, getError, HTTP } from '@venizia/ignis-helpers';
62
+ import type { TErrorDefinition, TRegisterErrors } from '@venizia/ignis-helpers';
63
+
64
+ export const UserErrors = {
65
+ CREATE_DUPLICATE_EMAIL: {
66
+ message: {
67
+ text: 'An account with %{email} already exists.',
68
+ code: 'server.core.user.create.duplicate_email',
69
+ },
70
+ statusCode: HTTP.ResultCodes.RS_4.Conflict,
71
+ category: ErrorScopes.VALIDATION,
72
+ description: 'Sign-up rejected because the email is already registered.',
73
+ },
74
+ } as const satisfies Record<string, TErrorDefinition>;
75
+
76
+ declare module '@venizia/ignis-helpers' {
77
+ interface IErrorKeyRegistry extends TRegisterErrors<typeof UserErrors> {}
78
+ }
79
+
80
+ // At every throw site:
81
+ throw getError({ error: UserErrors.CREATE_DUPLICATE_EMAIL, messageArgs: { email } });
82
+ ```
83
+
84
+ Pass the definition as `error` - **never spread it**. `getError({ ...UserErrors.CREATE_DUPLICATE_EMAIL, messageArgs: { email } })` reads naturally and is wrong: spreading skips the `error:` field entirely, so `category` and `description` - fields only modeled inside a definition - fall through into `extra` instead of staying structured. `statusCode` and `message` happen to still resolve correctly only because their shapes collide with what `getError` expects standalone; nothing catches the rest for you.
85
+
86
+ See the [Error helper reference](/extensions/helpers/error/) for the full surface.
87
+
45
88
  ## 2. HTTP Status Code Reference
46
89
 
47
90
  Use the correct status code for each error type:
@@ -90,9 +133,9 @@ export class UserService extends BaseService {
90
133
  try {
91
134
  await this.emailService.sendWelcome(data.email);
92
135
  } catch (error) {
93
- // Log but don't fail user creation
136
+ // Log but don't fail user creation - never a silent catch
94
137
  this.logger.error('[createUser] Failed to send welcome email | email: %s | error: %s',
95
- data.email, error.message);
138
+ data.email, error);
96
139
  }
97
140
 
98
141
  // create returns { count, data }
@@ -158,9 +201,18 @@ Database errors in SQLSTATE classes `22` (data exception), `23` (integrity const
158
201
  ```json
159
202
  {
160
203
  "message": "Unique constraint violation\nDetail: Key (email)=(test@example.com) already exists.\nTable: User\nConstraint: UQ_User_email",
161
- "messageCode": "core.system_error",
162
204
  "statusCode": 400,
163
- "requestId": "abc123"
205
+ "normalized": {
206
+ "text": "Unique constraint violation\nDetail: Key (email)=(test@example.com) already exists.\nTable: User\nConstraint: UQ_User_email",
207
+ "code": "core.system_error",
208
+ "args": {}
209
+ },
210
+ "requestId": "abc123",
211
+ "details": {
212
+ "url": "http://localhost:3000/users",
213
+ "path": "/users",
214
+ "stack": "Error: Unique constraint violation\n at ..."
215
+ }
164
216
  }
165
217
  ```
166
218
 
@@ -168,18 +220,24 @@ Database errors in SQLSTATE classes `22` (data exception), `23` (integrity const
168
220
  In production the message is the **base message only** - `Detail:` (which echoes row values like emails), `Table:`, and `Constraint:` are stripped, and `details.stack`/`details.cause` are omitted. Unexpected (non-client) database errors and connection failures return a generic `"Internal Server Error"`, so SQL, schema names, and connection host/port never leak. Use `requestId` + server logs to diagnose.
169
221
 
170
222
  ```json
171
- { "message": "Unique constraint violation", "messageCode": "core.system_error", "statusCode": 400, "requestId": "abc123" }
223
+ {
224
+ "message": "Unique constraint violation",
225
+ "statusCode": 400,
226
+ "normalized": { "text": "Unique constraint violation", "code": "core.system_error", "args": {} },
227
+ "requestId": "abc123",
228
+ "details": { "url": "http://localhost:3000/users", "path": "/users" }
229
+ }
172
230
  ```
173
231
  :::
174
232
 
175
233
  You don't need to wrap repository calls in try-catch for constraint errors. If you need custom error messages, you can still handle them explicitly:
176
234
 
177
235
  ```typescript
178
- import { DefaultCRUDRepository } from '@venizia/ignis';
236
+ import { DefaultRelationalRepository, type TCount } from '@venizia/ignis';
179
237
  import { getError, HTTP } from '@venizia/ignis-helpers';
180
238
 
181
- export class UserRepository extends DefaultCRUDRepository<typeof User.schema> {
182
- async createWithCustomError(data: TCreateUser): Promise<TCreateResult<TUser>> {
239
+ export class UserRepository extends DefaultRelationalRepository<typeof User.schema> {
240
+ async createWithCustomError(data: TCreateUser): Promise<TCount & { data: TUser }> {
183
241
  try {
184
242
  return await this.create({ data });
185
243
  } catch (error) {
@@ -198,41 +256,43 @@ export class UserRepository extends DefaultCRUDRepository<typeof User.schema> {
198
256
 
199
257
  ## 4. Global Error Handler
200
258
 
201
- IGNIS includes a built-in error handler. Customize behavior in your application:
259
+ IGNIS wires a built-in handler by default - `AppErrorMiddleware` from `@venizia/ignis` is a class, registered as `new AppErrorMiddleware({ logger, rootKey }).value()`, and `value()` returns the Hono `ErrorHandler`. You rarely need to replace it; when you do, keep the same response contract:
202
260
 
203
261
  ```typescript
204
262
  import { BaseApplication } from '@venizia/ignis';
205
- import { ApplicationError, MessageCode } from '@venizia/ignis-helpers';
263
+ import { isApplicationError, MessageCode } from '@venizia/ignis-helpers';
206
264
 
207
265
  export class Application extends BaseApplication {
208
266
  override setupMiddlewares(): void {
209
267
  super.setupMiddlewares();
210
268
 
211
- // Custom error handler (optional)
212
- this.server.onError((error, c) => {
269
+ // Custom error handler (optional) - the default AppErrorMiddleware already does this.
270
+ // `this.server` is `{ hono, runtime, instance }` - reach the Hono app via getServer().
271
+ this.getServer().onError((error, c) => {
213
272
  const requestId = c.get('requestId') ?? 'unknown';
214
273
 
215
274
  // Log all errors
216
- this.logger.error('[%s] Error | %s', requestId, error.message);
275
+ this.logger.error('[%s] Error | %s', requestId, error);
217
276
 
218
- // Handle known application errors
219
- if (error instanceof ApplicationError) {
277
+ // Handle known application errors - isApplicationError(), never `instanceof
278
+ // ApplicationError` (unreliable across package boundaries)
279
+ if (isApplicationError(error)) {
220
280
  return c.json({
221
281
  statusCode: error.statusCode,
222
282
  message: error.message,
223
- messageCode: error.messageCode, // already lower-cased, never undefined
224
- details: error.details,
283
+ normalized: error.normalized, // { text, code, args } - never undefined
284
+ extra: error.extra,
225
285
  requestId,
226
- }, error.statusCode as StatusCode);
286
+ }, error.statusCode as Parameters<typeof c.json>[1]);
227
287
  }
228
288
 
229
- // Handle Zod validation errors
289
+ // Handle Zod validation errors - Zod v4 exposes `issues`, not `errors`
230
290
  if (error.name === 'ZodError') {
231
291
  return c.json({
232
292
  statusCode: 422,
233
293
  message: 'Validation failed',
234
- messageCode: MessageCode.DEFAULT,
235
- details: { cause: error.errors },
294
+ normalized: { text: 'Validation failed', code: MessageCode.DEFAULT, args: {} },
295
+ details: { cause: error.issues },
236
296
  requestId,
237
297
  }, 422);
238
298
  }
@@ -241,7 +301,7 @@ export class Application extends BaseApplication {
241
301
  return c.json({
242
302
  statusCode: 500,
243
303
  message: 'Internal server error',
244
- messageCode: MessageCode.DEFAULT,
304
+ normalized: { text: 'Internal server error', code: MessageCode.DEFAULT, args: {} },
245
305
  requestId,
246
306
  }, 500);
247
307
  });
@@ -257,51 +317,61 @@ All errors should follow a consistent format:
257
317
  interface ErrorResponse {
258
318
  statusCode: number;
259
319
  message: string;
260
- messageCode?: string; // stable, localizable code (validation: from params.code or the raw Zod code)
320
+ normalized: {
321
+ text: string;
322
+ code: string; // stable, localizable code (validation: from params.code or the raw Zod code)
323
+ args: Record<string, unknown>;
324
+ };
261
325
  requestId: string;
262
326
  extra?: Record<string, unknown>; // structured context attached via getError(...)
263
- details?: {
264
- cause?: Array<{
265
- path: string;
266
- message: string;
267
- code: string;
268
- }>;
327
+ details: {
328
+ url: string;
329
+ path: string;
330
+ stack?: string; // non-production only
331
+ cause?: unknown; // non-production only, or the Zod issue list for 422s
269
332
  [key: string]: unknown;
270
333
  };
271
334
  }
272
335
  ```
273
336
 
337
+ There is no top-level `messageCode` - read `normalized.code`. `extra` never mirrors `messageArgs`; the resolved interpolation values live at `normalized.args`.
338
+
274
339
  **Example Responses:**
275
340
 
276
341
  ```json
277
342
  // 400 Bad Request
278
343
  {
279
- "statusCode": 400,
280
344
  "message": "Invalid request body",
281
- "messageCode": "core.system_error",
282
- "requestId": "abc123"
345
+ "statusCode": 400,
346
+ "normalized": { "text": "Invalid request body", "code": "core.system_error", "args": {} },
347
+ "requestId": "abc123",
348
+ "details": { "url": "http://localhost:3000/users", "path": "/users" }
283
349
  }
284
350
 
285
351
  // 404 Not Found
286
352
  // Extra keys passed to getError(...) (e.g. `details`) surface under `extra`;
287
353
  // the top-level `details` object is reserved for middleware context (url, path, stack, cause).
288
354
  {
289
- "statusCode": 404,
290
355
  "message": "User not found",
291
- "messageCode": "core.system_error",
356
+ "statusCode": 404,
357
+ "normalized": { "text": "User not found", "code": "core.system_error", "args": {} },
292
358
  "requestId": "abc123",
293
- "extra": { "details": { "id": "user-uuid" } }
359
+ "extra": { "details": { "id": "user-uuid" } },
360
+ "details": { "url": "http://localhost:3000/users/user-uuid", "path": "/users/:id" }
294
361
  }
295
362
 
296
363
  // 422 Validation Error
297
- // `message`/`messageCode` come from the first failing issue - its `params.code` if the schema set
298
- // one, otherwise the raw Zod code (e.g. `invalid_type`, `too_small`). The full list stays in `details.cause`.
364
+ // `message`/`normalized.code` come from the first failing issue - its `params.code` if the schema
365
+ // set one, otherwise the raw Zod code (e.g. `invalid_type`, `too_small`). `normalized.args` is
366
+ // always empty for a Zod issue; the full list of issues stays in `details.cause`.
299
367
  {
300
- "statusCode": 422,
301
368
  "message": "Invalid email format",
302
- "messageCode": "user.email.invalid",
369
+ "statusCode": 422,
370
+ "normalized": { "text": "Invalid email format", "code": "user.email.invalid", "args": {} },
303
371
  "requestId": "abc123",
304
372
  "details": {
373
+ "url": "http://localhost:3000/users",
374
+ "path": "/users",
305
375
  "cause": [
306
376
  {
307
377
  "path": "email",
@@ -314,10 +384,11 @@ interface ErrorResponse {
314
384
 
315
385
  // 500 Internal Error (production)
316
386
  {
317
- "statusCode": 500,
318
387
  "message": "Internal server error",
319
- "messageCode": "core.system_error",
320
- "requestId": "abc123"
388
+ "statusCode": 500,
389
+ "normalized": { "text": "Internal server error", "code": "core.system_error", "args": {} },
390
+ "requestId": "abc123",
391
+ "details": { "url": "http://localhost:3000/orders", "path": "/orders" }
321
392
  }
322
393
  ```
323
394
 
@@ -325,27 +396,27 @@ interface ErrorResponse {
325
396
 
326
397
  ### `%s`, Never `%j`, for an `Error`
327
398
 
328
- `message` and `stack` are non-enumerable properties on a native `Error` (and on `ApplicationError`, which extends it). `%j` serializes via `JSON.stringify`, which only visits enumerable own properties - so `logger.error('... | error: %j', error)` silently drops both `message` and `stack`, logging little more than `{}`. Always use `%s` to log an `Error` instance; reserve `%j`/`%o` for plain data objects.
399
+ `message` and `stack` are non-enumerable properties on a native `Error` (and on `ApplicationError`, which extends it). `%j` serializes via `JSON.stringify`, which only visits enumerable own properties - so `logger.error('... | error: %j', error)` silently drops both. Always use `%s` to log an `Error` instance; reserve `%j`/`%o` for plain data objects.
329
400
 
330
401
  ```typescript
331
402
  // ✅ Good - %s prints message + stack
332
403
  this.logger.error('[createOrder] Failed | error: %s', error);
333
404
 
334
- // ❌ Bad - %j drops message and stack (non-enumerable)
405
+ // ❌ Bad - %j drops the stack
335
406
  this.logger.error('[createOrder] Failed | error: %j', error);
336
407
  ```
337
408
 
409
+ > [!NOTE]
410
+ > An `ApplicationError` logged with `%j` does show its message text, because the text rides inside the enumerable `normalized.text`. That is incidental, not a reprieve: the **stack** is still gone, which is the reason the rule exists. A plain `Error` under `%j` still logs little more than `{}`.
411
+
338
412
  ### What to Log
339
413
 
340
414
  ```typescript
341
- // ✅ Good - Context for debugging
415
+ // ✅ Good - Context for debugging. `%s` on the error itself carries message + stack
342
416
  this.logger.error('[createOrder] Failed | userId: %s | orderId: %s | error: %s',
343
- userId, orderId, error.message);
344
-
345
- // ✅ Good - Include stack trace for unexpected errors
346
- this.logger.error('[createOrder] Unexpected error | %s', error.stack);
417
+ userId, orderId, error);
347
418
 
348
- // ❌ Bad - No context
419
+ // ❌ Bad - No context, and `.message` throws the stack away
349
420
  this.logger.error(error.message);
350
421
 
351
422
  // ❌ Bad - Sensitive data
@@ -354,12 +425,15 @@ this.logger.error('Login failed for user | password: %s', password);
354
425
 
355
426
  ### Log Levels
356
427
 
428
+ `ILogger` has exactly five levels, each a direct method. `alert`, `http`, `verbose` and `silly` do not exist.
429
+
357
430
  | Level | Use For |
358
431
  |-------|---------|
432
+ | `emerg` | The process cannot continue - unrecoverable |
359
433
  | `error` | Exceptions that need attention |
360
434
  | `warn` | Recoverable issues, deprecation warnings |
361
435
  | `info` | Important business events |
362
- | `debug` | Detailed debugging information |
436
+ | `debug` | Detailed debugging information (gated on `DEBUG`) |
363
437
 
364
438
  ```typescript
365
439
  // Error - requires attention
@@ -381,30 +455,29 @@ this.logger.debug('[query] Executing | sql: %s | params: %j', sql, params);
381
455
 
382
456
  ```typescript
383
457
  // ✅ Good - Errors propagate naturally with async/await
384
- async function processOrder(orderId: string) {
458
+ const processOrder = async (orderId: string) => {
385
459
  const order = await orderRepository.findById({ id: orderId }); // Throws if fails
386
- const payment = await paymentService.charge(order); // Throws if fails
387
- return payment;
388
- }
460
+ return paymentService.charge(order); // Throws if fails
461
+ };
389
462
 
390
463
  // ✅ Good - Explicit catch when you need to handle
391
- async function processOrderWithFallback(orderId: string) {
464
+ const processOrderWithFallback = async (order: TOrder) => {
392
465
  try {
393
466
  return await paymentService.charge(order);
394
467
  } catch (error) {
395
- this.logger.warn('[processOrder] Primary payment failed, trying backup');
396
- return await backupPaymentService.charge(order);
468
+ this.logger.warn('[processOrder] Primary payment failed, trying backup | error: %s', error);
469
+ return backupPaymentService.charge(order);
397
470
  }
398
- }
471
+ };
399
472
 
400
473
  // ❌ Bad - Swallowing errors
401
- async function processOrder(orderId: string) {
474
+ const processOrderSilently = async () => {
402
475
  try {
403
476
  await dangerousOperation();
404
477
  } catch (error) {
405
478
  // Error is swallowed - no one knows it happened!
406
479
  }
407
- }
480
+ };
408
481
  ```
409
482
 
410
483
  ### Fire-and-Forget with Error Handling
@@ -412,7 +485,7 @@ async function processOrder(orderId: string) {
412
485
  ```typescript
413
486
  // ✅ Good - Log errors from fire-and-forget operations
414
487
  this.sendNotification(userId).catch(error => {
415
- this.logger.error('[notify] Failed | userId: %s | error: %s', userId, error.message);
488
+ this.logger.error('[notify] Failed | userId: %s | error: %s', userId, error);
416
489
  });
417
490
 
418
491
  // ✅ Good - Use void to indicate intentional fire-and-forget
@@ -425,35 +498,44 @@ this.sendNotification(userId); // If this rejects, crash!
425
498
  ## 8. Transaction Error Handling
426
499
 
427
500
  ```typescript
428
- async function transferFunds(from: string, to: string, amount: number) {
429
- const tx = await accountRepository.beginTransaction();
501
+ import { getError, HTTP } from '@venizia/ignis-helpers';
502
+
503
+ const transferFunds = async (opts: { from: string; to: string; amount: number }) => {
504
+ const { from, to, amount } = opts;
505
+ const transaction = await accountRepository.beginTransaction();
430
506
 
431
507
  try {
432
- await accountRepository.debit({ id: from, amount, options: { transaction: tx } });
433
- await accountRepository.credit({ id: to, amount, options: { transaction: tx } });
508
+ await accountRepository.debit({ id: from, amount, options: { transaction } });
509
+ await accountRepository.credit({ id: to, amount, options: { transaction } });
434
510
 
435
- await tx.commit();
511
+ await transaction.commit();
436
512
  return { success: true };
437
513
  } catch (error) {
438
- await tx.rollback();
514
+ await transaction.rollback();
439
515
 
440
516
  // Re-throw with context
441
517
  throw getError({
442
518
  statusCode: HTTP.ResultCodes.RS_5.InternalServerError,
443
519
  message: '[transferFunds] Transaction failed',
444
- details: { from, to, amount, originalError: error.message },
520
+ cause: error,
521
+ extra: { from, to, amount },
445
522
  });
446
523
  }
447
- }
524
+ };
448
525
  ```
449
526
 
527
+ > [!NOTE]
528
+ > `rollback()` throws on failure, so it belongs in the `catch` exactly as above - never after a
529
+ > `commit()` you already awaited outside one. A rollback that follows a **failed** commit is a
530
+ > deliberate no-op: the transaction is already torn down, so this canonical shape stays safe.
531
+
450
532
  ## 9. Client-Side Error Handling
451
533
 
452
534
  Guide for API consumers:
453
535
 
454
536
  ```typescript
455
537
  // TypeScript client example
456
- async function createUser(data: CreateUserRequest): Promise<User> {
538
+ const createUser = async (data: TCreateUserRequest): Promise<TUser> => {
457
539
  const response = await fetch('/api/users', {
458
540
  method: 'POST',
459
541
  headers: { 'Content-Type': 'application/json' },
@@ -463,31 +545,38 @@ async function createUser(data: CreateUserRequest): Promise<User> {
463
545
  if (!response.ok) {
464
546
  const error = await response.json();
465
547
 
548
+ // Branch on `normalized.code` - it is always present. There is no top-level `messageCode`.
466
549
  switch (response.status) {
467
- case 400:
468
- throw new ValidationError(error.message, error.details);
469
- case 401:
470
- // Redirect to login
550
+ case 400: {
551
+ // Context passed to getError(...) arrives under `extra`, not `details`
552
+ throw new ValidationError(error.message, error.extra);
553
+ }
554
+ case 401: {
471
555
  window.location.href = '/login';
472
556
  throw new AuthError('Please log in');
473
- case 404:
557
+ }
558
+ case 404: {
474
559
  throw new NotFoundError(error.message);
475
- case 422:
476
- // Handle field-level errors
477
- const fieldErrors = error.details?.cause?.reduce((acc, e) => {
478
- acc[e.path] = e.message;
479
- return acc;
560
+ }
561
+ case 422: {
562
+ // Field-level issues live in details.cause
563
+ const fieldErrors = error.details?.cause?.reduce((accumulator, issue) => {
564
+ accumulator[issue.path] = issue.message;
565
+ return accumulator;
480
566
  }, {});
481
- throw new ValidationError('Validation failed', fieldErrors);
482
- case 429:
567
+ throw new ValidationError(error.normalized.code, fieldErrors);
568
+ }
569
+ case 429: {
483
570
  throw new RateLimitError('Too many requests. Try again later.');
484
- default:
571
+ }
572
+ default: {
485
573
  throw new ApiError(error.message || 'Something went wrong');
574
+ }
486
575
  }
487
576
  }
488
577
 
489
578
  return response.json();
490
- }
579
+ };
491
580
  ```
492
581
 
493
582
  ## Error Handling Checklist
@@ -495,7 +584,7 @@ async function createUser(data: CreateUserRequest): Promise<User> {
495
584
  | Category | Check |
496
585
  |----------|-------|
497
586
  | **Services** | Business rule violations throw appropriate errors |
498
- | **Repositories** | Database errors are caught and wrapped |
587
+ | **Repositories** | Constraint errors left to the global handler; only custom messages wrapped |
499
588
  | **Controllers** | Errors propagate to global handler |
500
589
  | **Async** | All promises have error handling |
501
590
  | **Transactions** | Always rollback on error |
@@ -102,6 +102,9 @@ Controller → Service → Repository → DataSource
102
102
  // ✅ Error Handling
103
103
  throw getError({ statusCode: 404, message: 'User not found' });
104
104
 
105
+ // ✅ Scoped Logging (ILogger - never names a provider)
106
+ this.logger.for('createUser').info('User created | id: %s', user.id);
107
+
105
108
  // ✅ Input Validation
106
109
  request: { body: jsonContent({ schema: z.object({ email: z.string().email() }) }) }
107
110
  ```
@@ -120,6 +123,9 @@ async getUser(c: Context) {
120
123
  // ❌ Catching all errors silently
121
124
  try { await riskyOperation(); } catch (e) { /* swallowed */ }
122
125
 
126
+ // ❌ Raw Error - loses statusCode, messageCode and the normalized response shape
127
+ throw new Error('User not found'); // Use getError()
128
+
123
129
  // ❌ Using `any` type
124
130
  const data: any = await fetchData(); // Use proper types!
125
131
  ```
@@ -9,6 +9,7 @@ Identify bottlenecks before optimizing:
9
9
  ```typescript
10
10
  import { executeWithPerformanceMeasure } from '@venizia/ignis-helpers';
11
11
 
12
+ // `logger` is optional and typed `ILogger` - it falls back to `console` when omitted.
12
13
  await executeWithPerformanceMeasure({
13
14
  logger: this.logger,
14
15
  scope: 'DataProcessing',
@@ -148,13 +149,22 @@ if (!cached) {
148
149
 
149
150
  | Setting | Value | Why |
150
151
  |---------|-------|-----|
151
- | `NODE_ENV` | `production` | Enables library optimizations |
152
- | Process Manager | PM2, systemd, Docker | Auto-restart, cluster mode |
153
- | Cluster Mode | CPU cores | Utilize all CPUs |
152
+ | `NODE_ENV` | `production` | Enables library optimizations; also gates error-detail leakage (unset is treated as production) |
153
+ | `APP_ENV_LOGGER_LEVEL` | `info` or `warn` | Keep hot-path logging off the critical path |
154
+ | Process Manager | systemd, Docker, Kubernetes | Auto-restart, supervision |
155
+ | Horizontal Scaling | One process per CPU core | Utilize all CPUs |
156
+
157
+ Bun has no PM2-style cluster mode, and `Bun.serve` is started without `reusePort` - two processes cannot share a port. Scale out with one process per port behind a reverse proxy, or with container replicas:
158
+
159
+ ```bash
160
+ # Each replica binds its own port; nginx / a load balancer fans out across them
161
+ APP_ENV_SERVER_PORT=3000 NODE_ENV=production bun run dist/index.js &
162
+ APP_ENV_SERVER_PORT=3001 NODE_ENV=production bun run dist/index.js &
163
+ ```
154
164
 
155
- **PM2 Cluster Mode:**
156
165
  ```bash
157
- pm2 start dist/index.js -i max # Use all CPU cores
166
+ # Or let the orchestrator do it
167
+ docker compose up -d --scale app=4
158
168
  ```
159
169
 
160
170
  ## 6. Transaction Support
@@ -208,12 +218,12 @@ Connection pooling significantly improves performance by reusing database connec
208
218
  ```typescript
209
219
  import { Pool } from 'pg';
210
220
  import { datasource } from '@venizia/ignis';
211
- import { BasePostgresDataSource } from '@venizia/ignis/postgres';
221
+ import { BaseRelationalDataSource } from '@venizia/ignis/postgres';
212
222
  import { NodePostgresDriver } from '@venizia/ignis/postgres/node-postgres';
213
223
 
214
224
  // IDataSourceConfigs: your settings interface (host/port/user/password/database)
215
225
  @datasource({ driver: NodePostgresDriver })
216
- export class PostgresDataSource extends BasePostgresDataSource<IDataSourceConfigs> {
226
+ export class PostgresDataSource extends BaseRelationalDataSource<IDataSourceConfigs> {
217
227
  override configure(): void {
218
228
  // Keep the pool on `this.client` - NodePostgresDriver above wires the driver and Drizzle
219
229
  // connector from it lazily, on first getConnector()/beginTransaction()
@@ -385,9 +395,9 @@ const logger = HfLogger.get('OrderEngine');
385
395
  const MSG_ORDER_SENT = HfLogger.encodeMessage('Order sent');
386
396
  const MSG_ORDER_FILLED = HfLogger.encodeMessage('Order filled');
387
397
 
388
- // Start background flusher
398
+ // Start background flusher (writes to stdout by default; pass { filePath } or { sink })
389
399
  const flusher = new HfLogFlusher();
390
- flusher.start(100); // Flush every 100ms
400
+ flusher.start(100); // Flush interval in ms - 100 is also the default
391
401
 
392
402
  // In hot path (~100-300ns, zero allocation):
393
403
  logger.log('info', MSG_ORDER_SENT);
@@ -395,11 +405,14 @@ logger.log('info', MSG_ORDER_FILLED);
395
405
  ```
396
406
 
397
407
  **Key points:**
408
+ - `log(level, bytes)` is the zero-allocation path. Passing a string, or any `...args`, falls back to formatting
409
+ - Levels are the same five as `ILogger`: `debug`, `info`, `warn`, `error`, `emerg`
398
410
  - Pre-encode messages at initialization, not in hot path
399
- - Use background flushing to avoid I/O blocking
400
- - HfLogger uses a lock-free ring buffer (64K entries, 16MB)
411
+ - HfLogger uses a lock-free ring buffer (64K entries x 256 bytes = 16MB), allocated lazily on first use
412
+ - Scope truncates at 32 UTF-8 bytes, message at 213 - silently
413
+ - For the standard logger, narrow output with `APP_ENV_LOGGER_LEVEL` in production and keep debug lines behind the `DEBUG` gate (pre-computed at module load, near-zero cost when off)
401
414
 
402
- > **Deep Dive:** See [Logger Helper](../extensions/helpers/logger/) for complete HfLogger API.
415
+ > **Deep Dive:** See [HfLogger](../extensions/helpers/logger/hf-logger.md) for the complete API.
403
416
 
404
417
  ## Performance Checklist
405
418
 
@@ -414,7 +427,7 @@ logger.log('info', MSG_ORDER_FILLED);
414
427
  | **Memory** | Large datasets processed in batches | High |
415
428
  | **Caching** | Expensive queries cached | High |
416
429
  | **Workers** | CPU-intensive tasks offloaded | High |
417
- | **Logging** | HfLogger for hot paths (HFT) | High |
430
+ | **Logging** | HfLogger for hot paths (HFT); `APP_ENV_LOGGER_LEVEL` in production | High |
418
431
  | **Monitoring** | Performance logging enabled | Low |
419
432
 
420
433
  ## See Also