@camcima/nestjs-rfc9457 0.0.2

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 (33) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +753 -0
  3. package/dist/index.d.ts +8 -0
  4. package/dist/index.js +19 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/problem-details.factory.d.ts +18 -0
  7. package/dist/problem-details.factory.js +242 -0
  8. package/dist/problem-details.factory.js.map +1 -0
  9. package/dist/problem-type.decorator.d.ts +3 -0
  10. package/dist/problem-type.decorator.js +11 -0
  11. package/dist/problem-type.decorator.js.map +1 -0
  12. package/dist/rfc9457.constants.d.ts +3 -0
  13. package/dist/rfc9457.constants.js +7 -0
  14. package/dist/rfc9457.constants.js.map +1 -0
  15. package/dist/rfc9457.exception-filter.d.ts +11 -0
  16. package/dist/rfc9457.exception-filter.js +65 -0
  17. package/dist/rfc9457.exception-filter.js.map +1 -0
  18. package/dist/rfc9457.interfaces.d.ts +37 -0
  19. package/dist/rfc9457.interfaces.js +3 -0
  20. package/dist/rfc9457.interfaces.js.map +1 -0
  21. package/dist/rfc9457.module.d.ts +7 -0
  22. package/dist/rfc9457.module.js +93 -0
  23. package/dist/rfc9457.module.js.map +1 -0
  24. package/dist/utils/slug.d.ts +1 -0
  25. package/dist/utils/slug.js +13 -0
  26. package/dist/utils/slug.js.map +1 -0
  27. package/dist/validation/rfc9457-validation-pipe-exception.factory.d.ts +2 -0
  28. package/dist/validation/rfc9457-validation-pipe-exception.factory.js +10 -0
  29. package/dist/validation/rfc9457-validation-pipe-exception.factory.js.map +1 -0
  30. package/dist/validation/rfc9457-validation.exception.d.ts +5 -0
  31. package/dist/validation/rfc9457-validation.exception.js +12 -0
  32. package/dist/validation/rfc9457-validation.exception.js.map +1 -0
  33. package/package.json +79 -0
package/README.md ADDED
@@ -0,0 +1,753 @@
1
+ <div align="center">
2
+
3
+ <picture>
4
+ <img alt="nestjs-rfc9457" src="assets/logo.svg" width="580">
5
+ </picture>
6
+
7
+ <br>
8
+
9
+ [![CI](https://github.com/camcima/nestjs-rfc9457/actions/workflows/ci.yml/badge.svg)](https://github.com/camcima/nestjs-rfc9457/actions/workflows/ci.yml)
10
+ [![codecov](https://codecov.io/gh/camcima/nestjs-rfc9457/graph/badge.svg)](https://codecov.io/gh/camcima/nestjs-rfc9457)
11
+ [![npm version](https://img.shields.io/npm/v/@camcima/nestjs-rfc9457)](https://www.npmjs.com/package/@camcima/nestjs-rfc9457)
12
+ [![npm downloads](https://img.shields.io/npm/dm/@camcima/nestjs-rfc9457.svg)](https://www.npmjs.com/package/@camcima/nestjs-rfc9457)
13
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
14
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5-blue.svg)](https://www.typescriptlang.org/)
15
+ [![Node.js](https://img.shields.io/badge/Node.js-18%20%7C%2020%20%7C%2022-green.svg)](https://nodejs.org/)
16
+
17
+ </div>
18
+
19
+ NestJS library for [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) Problem Details HTTP error responses.
20
+
21
+ ## What is RFC 9457?
22
+
23
+ [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) (July 2023) defines a standard JSON format for HTTP API error responses, using the `application/problem+json` media type. It supersedes RFC 7807 and gives APIs a consistent, machine-readable way to communicate errors.
24
+
25
+ A Problem Details response looks like this:
26
+
27
+ ```json
28
+ {
29
+ "type": "https://api.example.com/problems/not-found",
30
+ "title": "Not Found",
31
+ "status": 404,
32
+ "detail": "User 42 not found",
33
+ "instance": "/api/users/42"
34
+ }
35
+ ```
36
+
37
+ The five standard members are:
38
+
39
+ | Member | Description |
40
+ | ---------- | ------------------------------------------------------ |
41
+ | `type` | URI identifying the problem type |
42
+ | `title` | Short human-readable summary of the problem type |
43
+ | `status` | HTTP status code (advisory) |
44
+ | `detail` | Human-readable explanation of this specific occurrence |
45
+ | `instance` | URI identifying this specific occurrence |
46
+
47
+ Extension members (arbitrary key-value pairs) are allowed for problem-type-specific data.
48
+
49
+ ---
50
+
51
+ ## Features
52
+
53
+ - Zero-config drop-in: import the module once in `AppModule` and all HTTP exceptions become RFC 9457 responses
54
+ - Automatic `ValidationPipe` integration — flat string-array errors work out of the box (Tier 1)
55
+ - Enhanced structured validation errors with `property`, `constraints`, and nested `children` (Tier 2)
56
+ - `@ProblemType()` class decorator for custom exception types with full prototype-chain inheritance
57
+ - Configurable `type` URI generation with `typeBaseUri` and automatic kebab-case slug derivation
58
+ - Four `instance` strategies: `'request-uri'`, `'uuid'`, `'none'`, or a custom callback
59
+ - Optional catch-all mode for non-`HttpException` throwables (produces 500 Problem Details)
60
+ - Custom `exceptionMapper` callback for full control over any exception
61
+ - `ProblemDetailsFactory` is injectable — use it directly in GraphQL, microservices, or custom filters
62
+ - Works with both Express and Fastify adapters
63
+ - Zero runtime dependencies; `class-validator` is an optional peer dependency
64
+
65
+ ---
66
+
67
+ ## Installation
68
+
69
+ ```bash
70
+ npm install @camcima/nestjs-rfc9457
71
+ ```
72
+
73
+ ```bash
74
+ yarn add @camcima/nestjs-rfc9457
75
+ ```
76
+
77
+ ```bash
78
+ pnpm add @camcima/nestjs-rfc9457
79
+ ```
80
+
81
+ ### Peer dependencies
82
+
83
+ | Package | Version | Required |
84
+ | ------------------ | ---------------------- | ------------------------------------ |
85
+ | `@nestjs/common` | `^10.0.0 \|\| ^11.0.0` | Yes |
86
+ | `@nestjs/core` | `^10.0.0 \|\| ^11.0.0` | Yes |
87
+ | `reflect-metadata` | `^0.1.13 \|\| ^0.2.0` | Yes |
88
+ | `class-validator` | `^0.14.0` | No (optional, for Tier 2 validation) |
89
+
90
+ ---
91
+
92
+ ## Quick Start
93
+
94
+ Import `Rfc9457Module` once in your root `AppModule`. Because the module is **global**, you do not need to import it in any other module — the exception filter applies everywhere in your application automatically.
95
+
96
+ ```typescript
97
+ // app.module.ts
98
+ import { Module } from '@nestjs/common';
99
+ import { Rfc9457Module } from '@camcima/nestjs-rfc9457';
100
+
101
+ @Module({
102
+ imports: [Rfc9457Module.forRoot()],
103
+ })
104
+ export class AppModule {}
105
+ ```
106
+
107
+ That is all the configuration you need. Every `HttpException` thrown anywhere in your application will now produce an RFC 9457 response.
108
+
109
+ ### Before and after
110
+
111
+ **Before** (standard NestJS `NotFoundException`):
112
+
113
+ ```json
114
+ {
115
+ "statusCode": 404,
116
+ "message": "User 42 not found",
117
+ "error": "Not Found"
118
+ }
119
+ ```
120
+
121
+ **After** (with `@camcima/nestjs-rfc9457`):
122
+
123
+ ```json
124
+ {
125
+ "type": "about:blank",
126
+ "title": "Not Found",
127
+ "status": 404,
128
+ "detail": "User 42 not found"
129
+ }
130
+ ```
131
+
132
+ The response `Content-Type` is set to `application/problem+json` as required by the RFC.
133
+
134
+ ---
135
+
136
+ ## Configuration
137
+
138
+ `Rfc9457Module.forRoot()` accepts an optional `Rfc9457ModuleOptions` object.
139
+
140
+ ```typescript
141
+ Rfc9457Module.forRoot({
142
+ typeBaseUri: 'https://api.example.com/problems',
143
+ instanceStrategy: 'request-uri',
144
+ catchAllExceptions: true,
145
+ exceptionMapper: (exception, request) => {
146
+ /* ... */
147
+ },
148
+ validationExceptionMapper: (messages, request) => {
149
+ /* ... */
150
+ },
151
+ });
152
+ ```
153
+
154
+ ### `typeBaseUri`
155
+
156
+ **Type**: `string` | **Default**: `undefined`
157
+
158
+ When set, the library generates `type` URIs by combining the base URI with a kebab-case slug derived from the HTTP status phrase. When omitted, `type` defaults to `"about:blank"` (per RFC 9457 §4.2).
159
+
160
+ ```typescript
161
+ Rfc9457Module.forRoot({
162
+ typeBaseUri: 'https://api.example.com/problems',
163
+ });
164
+ ```
165
+
166
+ A `NotFoundException` (404) becomes:
167
+
168
+ ```json
169
+ {
170
+ "type": "https://api.example.com/problems/not-found",
171
+ "title": "Not Found",
172
+ "status": 404
173
+ }
174
+ ```
175
+
176
+ Slug derivation uses the HTTP status phrase from Node's built-in `http.STATUS_CODES`:
177
+
178
+ - `"Not Found"` → `not-found`
179
+ - `"Internal Server Error"` → `internal-server-error`
180
+ - `"Unprocessable Entity"` → `unprocessable-entity`
181
+
182
+ ### `instanceStrategy`
183
+
184
+ **Type**: `'request-uri' | 'uuid' | 'none' | ((request, exception) => string | undefined)` | **Default**: `'none'`
185
+
186
+ Controls how the `instance` field is populated.
187
+
188
+ **`'none'`** — `instance` is omitted from the response (default):
189
+
190
+ ```typescript
191
+ Rfc9457Module.forRoot({ instanceStrategy: 'none' });
192
+ ```
193
+
194
+ **`'request-uri'`** — uses the request URL path:
195
+
196
+ ```typescript
197
+ Rfc9457Module.forRoot({ instanceStrategy: 'request-uri' });
198
+ // instance: "/api/users/42"
199
+ ```
200
+
201
+ **`'uuid'`** — generates a `urn:uuid:<v4>` per occurrence:
202
+
203
+ ```typescript
204
+ Rfc9457Module.forRoot({ instanceStrategy: 'uuid' });
205
+ // instance: "urn:uuid:a8098c1a-f86e-11da-bd1a-00112444be1e"
206
+ ```
207
+
208
+ **Custom callback** — full control, receives the request and the original exception:
209
+
210
+ ```typescript
211
+ Rfc9457Module.forRoot({
212
+ instanceStrategy: (request, exception) => {
213
+ return `https://errors.example.com/log?path=${request.url}`;
214
+ },
215
+ });
216
+ ```
217
+
218
+ Return `undefined` from a custom callback to omit `instance` for that occurrence.
219
+
220
+ The `request` parameter implements `Rfc9457Request`:
221
+
222
+ ```typescript
223
+ interface Rfc9457Request {
224
+ url: string;
225
+ method: string;
226
+ [key: string]: unknown;
227
+ }
228
+ ```
229
+
230
+ Both Express and Fastify request objects satisfy this interface.
231
+
232
+ ### `catchAllExceptions`
233
+
234
+ **Type**: `boolean` | **Default**: `false`
235
+
236
+ When `false` (default), exceptions that are not `HttpException` instances are passed to NestJS's default error handling via `super.catch()`. When `true`, any throwable — including plain `Error` objects and non-HTTP exceptions — is caught and produces a generic 500 Problem Details response. Internal error information is never exposed in the response body.
237
+
238
+ ```typescript
239
+ Rfc9457Module.forRoot({ catchAllExceptions: true });
240
+ ```
241
+
242
+ ### `exceptionMapper`
243
+
244
+ **Type**: `(exception: unknown, request: Rfc9457Request) => ProblemDetail | null`
245
+
246
+ A callback that runs first in the resolution chain. Return a `ProblemDetail` object to take full control of the response, or `null` to fall through to the next resolution step (`@ProblemType()` metadata, then validation handling, then default mapping).
247
+
248
+ ```typescript
249
+ Rfc9457Module.forRoot({
250
+ exceptionMapper: (exception, request) => {
251
+ if (exception instanceof DatabaseException) {
252
+ return {
253
+ type: 'https://api.example.com/problems/database-error',
254
+ title: 'Database Error',
255
+ status: 503,
256
+ detail: 'A temporary database error occurred',
257
+ };
258
+ }
259
+ return null; // fall through to default handling
260
+ },
261
+ });
262
+ ```
263
+
264
+ If the returned `ProblemDetail` omits `status`, the factory falls back to `exception.getStatus()` (if it is an `HttpException`) or `500`.
265
+
266
+ ### `validationExceptionMapper`
267
+
268
+ **Type**: `(messages: string[], request: Rfc9457Request) => ProblemDetail`
269
+
270
+ Overrides the default Tier 1 validation error response. Receives the flat string array from `BadRequestException.getResponse().message`. Only applies to Tier 1 (flat string) validation errors — Tier 2 structured errors from `Rfc9457ValidationException` bypass this callback.
271
+
272
+ ```typescript
273
+ Rfc9457Module.forRoot({
274
+ validationExceptionMapper: (messages, request) => ({
275
+ type: 'https://api.example.com/problems/validation-error',
276
+ title: 'Validation Error',
277
+ status: 400,
278
+ detail: 'One or more fields failed validation',
279
+ violations: messages,
280
+ }),
281
+ });
282
+ ```
283
+
284
+ ---
285
+
286
+ ## Async Configuration
287
+
288
+ Use `Rfc9457Module.forRootAsync()` to inject configuration from a service such as `ConfigService`.
289
+
290
+ ### `useFactory`
291
+
292
+ ```typescript
293
+ import { Module } from '@nestjs/common';
294
+ import { ConfigModule, ConfigService } from '@nestjs/config';
295
+ import { Rfc9457Module } from '@camcima/nestjs-rfc9457';
296
+
297
+ @Module({
298
+ imports: [
299
+ ConfigModule.forRoot(),
300
+ Rfc9457Module.forRootAsync({
301
+ imports: [ConfigModule],
302
+ inject: [ConfigService],
303
+ useFactory: (config: ConfigService) => ({
304
+ typeBaseUri: config.get<string>('PROBLEM_TYPE_BASE_URI'),
305
+ instanceStrategy: 'uuid',
306
+ catchAllExceptions: config.get<boolean>('CATCH_ALL_EXCEPTIONS', false),
307
+ }),
308
+ }),
309
+ ],
310
+ })
311
+ export class AppModule {}
312
+ ```
313
+
314
+ ### `useClass`
315
+
316
+ Implement the `Rfc9457OptionsFactory` interface:
317
+
318
+ ```typescript
319
+ import { Injectable } from '@nestjs/common';
320
+ import { Rfc9457OptionsFactory, Rfc9457ModuleOptions } from '@camcima/nestjs-rfc9457';
321
+
322
+ @Injectable()
323
+ export class Rfc9457ConfigService implements Rfc9457OptionsFactory {
324
+ createRfc9457Options(): Rfc9457ModuleOptions {
325
+ return {
326
+ typeBaseUri: 'https://api.example.com/problems',
327
+ instanceStrategy: 'uuid',
328
+ };
329
+ }
330
+ }
331
+ ```
332
+
333
+ ```typescript
334
+ Rfc9457Module.forRootAsync({
335
+ useClass: Rfc9457ConfigService,
336
+ });
337
+ ```
338
+
339
+ ### `useExisting`
340
+
341
+ Reuse an existing provider that implements `Rfc9457OptionsFactory`:
342
+
343
+ ```typescript
344
+ Rfc9457Module.forRootAsync({
345
+ imports: [SharedConfigModule],
346
+ useExisting: SharedConfigService,
347
+ });
348
+ ```
349
+
350
+ ---
351
+
352
+ ## Custom Exception Types
353
+
354
+ Use the `@ProblemType()` decorator to attach RFC 9457 problem type metadata to your exception classes. The decorator stores a **template** with type identity fields (`type`, `title`, `status`). Occurrence-specific fields (`detail`, `instance`) are always resolved at runtime by the factory from the exception message and the configured instance strategy.
355
+
356
+ ```typescript
357
+ import { HttpException } from '@nestjs/common';
358
+ import { ProblemType } from '@camcima/nestjs-rfc9457';
359
+
360
+ @ProblemType({
361
+ type: 'https://api.example.com/problems/insufficient-funds',
362
+ title: 'Insufficient Funds',
363
+ status: 422,
364
+ })
365
+ export class InsufficientFundsException extends HttpException {
366
+ constructor(
367
+ public readonly balance: number,
368
+ public readonly required: number,
369
+ ) {
370
+ super(`Balance ${balance} is less than required ${required}`, 422);
371
+ }
372
+ }
373
+ ```
374
+
375
+ When this exception is thrown, the response is:
376
+
377
+ ```json
378
+ {
379
+ "type": "https://api.example.com/problems/insufficient-funds",
380
+ "title": "Insufficient Funds",
381
+ "status": 422,
382
+ "detail": "Balance 50 is less than required 100"
383
+ }
384
+ ```
385
+
386
+ The decorator accepts a `ProblemTypeMetadata` object:
387
+
388
+ ```typescript
389
+ interface ProblemTypeMetadata {
390
+ type?: string; // URI for the problem type
391
+ title?: string; // Short human-readable summary
392
+ status?: number; // HTTP status code
393
+ }
394
+ ```
395
+
396
+ All three fields are optional. If `status` is omitted, the factory uses `exception.getStatus()` for `HttpException` subclasses or falls back to `500` in catch-all mode. If `type` is omitted and `typeBaseUri` is configured, the slug for the status code is used.
397
+
398
+ ### Inheritance
399
+
400
+ Metadata lookup walks the prototype chain, so child classes automatically inherit their parent's `@ProblemType()` metadata:
401
+
402
+ ```typescript
403
+ // Parent defines the problem type
404
+ @ProblemType({
405
+ type: 'https://api.example.com/problems/payment-error',
406
+ title: 'Payment Error',
407
+ status: 402,
408
+ })
409
+ export class PaymentException extends HttpException {
410
+ constructor(message: string) {
411
+ super(message, 402);
412
+ }
413
+ }
414
+
415
+ // Child inherits parent's @ProblemType() metadata
416
+ export class CardDeclinedException extends PaymentException {
417
+ constructor() {
418
+ super('Card was declined');
419
+ }
420
+ }
421
+ ```
422
+
423
+ A child class can **fully override** the parent's metadata by applying its own `@ProblemType()` decorator. There is no merging — the child's decorator replaces the parent's entirely.
424
+
425
+ ```typescript
426
+ @ProblemType({
427
+ type: 'https://api.example.com/problems/card-declined',
428
+ title: 'Card Declined',
429
+ status: 402,
430
+ })
431
+ export class CardDeclinedException extends PaymentException {
432
+ constructor() {
433
+ super('Card was declined');
434
+ }
435
+ }
436
+ ```
437
+
438
+ `@ProblemType()` can also decorate plain `Error` subclasses (not extending `HttpException`), but these are only handled by the factory when `catchAllExceptions: true` is set.
439
+
440
+ ---
441
+
442
+ ## Validation Integration
443
+
444
+ ### Tier 1 — Automatic (zero config)
445
+
446
+ When NestJS's `ValidationPipe` rejects a request, it throws a `BadRequestException` whose response contains a `message` array of strings. The library detects this automatically and produces a structured validation error response with no configuration required.
447
+
448
+ ```typescript
449
+ // main.ts — standard ValidationPipe setup, nothing extra needed
450
+ app.useGlobalPipes(new ValidationPipe());
451
+ ```
452
+
453
+ Response:
454
+
455
+ ```json
456
+ {
457
+ "type": "about:blank",
458
+ "title": "Bad Request",
459
+ "status": 400,
460
+ "detail": "Request validation failed",
461
+ "errors": ["email must be an email", "age must not be less than 0"]
462
+ }
463
+ ```
464
+
465
+ To customize the Tier 1 response, use the `validationExceptionMapper` option described in the [Configuration](#configuration) section.
466
+
467
+ ### Tier 2 — Enhanced structured errors (opt-in)
468
+
469
+ For rich, structured validation output with `property`, `constraints`, and nested `children` arrays, use the `createRfc9457ValidationPipeExceptionFactory` helper.
470
+
471
+ **Step 1** — Install `class-validator` if you have not already:
472
+
473
+ ```bash
474
+ npm install class-validator class-transformer
475
+ ```
476
+
477
+ **Step 2** — Use the factory as the `ValidationPipe` exception factory:
478
+
479
+ ```typescript
480
+ // main.ts
481
+ import { ValidationPipe } from '@nestjs/common';
482
+ import { createRfc9457ValidationPipeExceptionFactory } from '@camcima/nestjs-rfc9457';
483
+
484
+ app.useGlobalPipes(
485
+ new ValidationPipe({
486
+ exceptionFactory: createRfc9457ValidationPipeExceptionFactory(),
487
+ }),
488
+ );
489
+ ```
490
+
491
+ Response for a DTO with nested validation:
492
+
493
+ ```json
494
+ {
495
+ "type": "about:blank",
496
+ "title": "Bad Request",
497
+ "status": 400,
498
+ "detail": "Request validation failed",
499
+ "errors": [
500
+ {
501
+ "property": "email",
502
+ "constraints": {
503
+ "isEmail": "email must be an email"
504
+ }
505
+ },
506
+ {
507
+ "property": "address",
508
+ "children": [
509
+ {
510
+ "property": "zip",
511
+ "constraints": {
512
+ "isPostalCode": "zip must be a postal code"
513
+ }
514
+ }
515
+ ]
516
+ }
517
+ ]
518
+ }
519
+ ```
520
+
521
+ Nested validation errors are preserved as `children` arrays matching the `class-validator` `ValidationError` tree. They are **not** flattened to dotted paths (e.g., `"address.zip"`) — the original structure is preserved.
522
+
523
+ ---
524
+
525
+ ## Advanced Usage
526
+
527
+ ### Using `ProblemDetailsFactory` directly
528
+
529
+ `ProblemDetailsFactory` is an injectable service exported by `Rfc9457Module`. You can inject it into any provider to produce Problem Details responses in contexts outside the standard HTTP filter — for example, GraphQL error formatters or microservice exception handlers.
530
+
531
+ ```typescript
532
+ import { Injectable } from '@nestjs/common';
533
+ import { ProblemDetailsFactory, Rfc9457Request } from '@camcima/nestjs-rfc9457';
534
+
535
+ @Injectable()
536
+ export class GraphQLErrorFormatter {
537
+ constructor(private readonly problemDetailsFactory: ProblemDetailsFactory) {}
538
+
539
+ format(exception: unknown, context: { path: string; method: string }) {
540
+ const request: Rfc9457Request = {
541
+ url: context.path,
542
+ method: context.method,
543
+ };
544
+ const { status, body } = this.problemDetailsFactory.create(exception, request);
545
+ return { extensions: { problem: body, httpStatus: status } };
546
+ }
547
+ }
548
+ ```
549
+
550
+ The `create` method signature is:
551
+
552
+ ```typescript
553
+ create(exception: unknown, request: Rfc9457Request): { status: number; body: ProblemDetail }
554
+ ```
555
+
556
+ - `status` is the definitive HTTP status code to use for the transport layer.
557
+ - `body` is the RFC 9457 Problem Details object to serialize.
558
+
559
+ The factory applies the full resolution chain (mapper → decorator → validation → default → fallback) and all normalization rules (`type`, `instance`, `title`) regardless of how it is called.
560
+
561
+ ### Custom exception filter
562
+
563
+ You can build your own filter on top of `ProblemDetailsFactory` if you need to intercept specific exception types before the global filter sees them:
564
+
565
+ ```typescript
566
+ import { Catch, ArgumentsHost, HttpException } from '@nestjs/common';
567
+ import { BaseExceptionFilter } from '@nestjs/core';
568
+ import { ProblemDetailsFactory } from '@camcima/nestjs-rfc9457';
569
+
570
+ @Catch(MySpecialException)
571
+ export class MySpecialExceptionFilter extends BaseExceptionFilter {
572
+ constructor(private readonly factory: ProblemDetailsFactory) {
573
+ super();
574
+ }
575
+
576
+ catch(exception: MySpecialException, host: ArgumentsHost) {
577
+ const ctx = host.switchToHttp();
578
+ const request = ctx.getRequest();
579
+ const response = ctx.getResponse();
580
+
581
+ const { status, body } = this.factory.create(exception, request);
582
+ response.status(status).json(body);
583
+ }
584
+ }
585
+ ```
586
+
587
+ ---
588
+
589
+ ## API Reference
590
+
591
+ | Export | Kind | Description |
592
+ | --------------------------------------------- | ---------------- | --------------------------------------------------------------------------- |
593
+ | `Rfc9457Module` | Class | Dynamic module. Use `forRoot(options?)` or `forRootAsync(options)` |
594
+ | `ProblemDetailsFactory` | Injectable class | Core resolver; injectable for use outside the HTTP filter |
595
+ | `Rfc9457ExceptionFilter` | Injectable class | Global exception filter; registered automatically by the module |
596
+ | `ProblemType` | Decorator | Class decorator that attaches problem type metadata to exception classes |
597
+ | `ProblemDetail` | Interface | RFC 9457 response body shape with index signature for extension members |
598
+ | `ProblemTypeMetadata` | Interface | Decorator options (`type`, `title`, `status`) |
599
+ | `Rfc9457ModuleOptions` | Interface | Options accepted by `forRoot()` |
600
+ | `Rfc9457OptionsFactory` | Interface | Implement for `useClass` / `useExisting` async patterns |
601
+ | `Rfc9457AsyncModuleOptions` | Interface | Options accepted by `forRootAsync()` |
602
+ | `InstanceStrategy` | Type | Union type for `instanceStrategy` option |
603
+ | `Rfc9457Request` | Interface | Minimal request context compatible with Express and Fastify |
604
+ | `Rfc9457ValidationException` | Class | Exception wrapping structured `ValidationError[]`; thrown by Tier 2 factory |
605
+ | `createRfc9457ValidationPipeExceptionFactory` | Function | Returns an `exceptionFactory` for `ValidationPipe` to enable Tier 2 errors |
606
+ | `RFC9457_MODULE_OPTIONS` | Symbol | DI token for the module options |
607
+ | `PROBLEM_CONTENT_TYPE` | Constant | `'application/problem+json'` |
608
+
609
+ ---
610
+
611
+ ## Example Responses
612
+
613
+ ### Basic 404 (no `typeBaseUri`)
614
+
615
+ ```typescript
616
+ throw new NotFoundException('User 42 not found');
617
+ ```
618
+
619
+ ```json
620
+ {
621
+ "type": "about:blank",
622
+ "title": "Not Found",
623
+ "status": 404,
624
+ "detail": "User 42 not found"
625
+ }
626
+ ```
627
+
628
+ ### Basic 404 (with `typeBaseUri` and `instanceStrategy: 'request-uri'`)
629
+
630
+ ```typescript
631
+ Rfc9457Module.forRoot({
632
+ typeBaseUri: 'https://api.example.com/problems',
633
+ instanceStrategy: 'request-uri',
634
+ });
635
+
636
+ throw new NotFoundException('User 42 not found');
637
+ // request path: /api/users/42
638
+ ```
639
+
640
+ ```json
641
+ {
642
+ "type": "https://api.example.com/problems/not-found",
643
+ "title": "Not Found",
644
+ "status": 404,
645
+ "detail": "User 42 not found",
646
+ "instance": "/api/users/42"
647
+ }
648
+ ```
649
+
650
+ ### Validation error (Tier 2 structured)
651
+
652
+ ```json
653
+ {
654
+ "type": "about:blank",
655
+ "title": "Bad Request",
656
+ "status": 400,
657
+ "detail": "Request validation failed",
658
+ "errors": [
659
+ {
660
+ "property": "email",
661
+ "constraints": {
662
+ "isEmail": "email must be an email"
663
+ }
664
+ },
665
+ {
666
+ "property": "address",
667
+ "children": [
668
+ {
669
+ "property": "zip",
670
+ "constraints": {
671
+ "isPostalCode": "zip must be a postal code"
672
+ }
673
+ }
674
+ ]
675
+ }
676
+ ]
677
+ }
678
+ ```
679
+
680
+ ### Custom problem type with `@ProblemType()`
681
+
682
+ ```typescript
683
+ @ProblemType({
684
+ type: 'https://api.example.com/problems/insufficient-funds',
685
+ title: 'Insufficient Funds',
686
+ status: 422,
687
+ })
688
+ export class InsufficientFundsException extends HttpException {
689
+ /* ... */
690
+ }
691
+
692
+ throw new InsufficientFundsException(50, 100);
693
+ ```
694
+
695
+ ```json
696
+ {
697
+ "type": "https://api.example.com/problems/insufficient-funds",
698
+ "title": "Insufficient Funds",
699
+ "status": 422,
700
+ "detail": "Balance 50 is less than required 100"
701
+ }
702
+ ```
703
+
704
+ ### Catch-all 500 (with `catchAllExceptions: true`)
705
+
706
+ ```typescript
707
+ throw new Error('Connection refused');
708
+ ```
709
+
710
+ ```json
711
+ {
712
+ "type": "about:blank",
713
+ "title": "Internal Server Error",
714
+ "status": 500
715
+ }
716
+ ```
717
+
718
+ Internal error messages are never included in the response to avoid leaking sensitive information.
719
+
720
+ ---
721
+
722
+ ## Contributing
723
+
724
+ Contributions are welcome. Please open an issue before submitting a pull request for significant changes.
725
+
726
+ ```bash
727
+ # Clone the repository
728
+ git clone https://github.com/camcima/nestjs-rfc9457.git
729
+ cd nestjs-rfc9457
730
+
731
+ # Install dependencies
732
+ npm install
733
+
734
+ # Run unit tests
735
+ npm run test:unit
736
+
737
+ # Run e2e tests
738
+ npm run test:e2e
739
+
740
+ # Run all tests with coverage
741
+ npm run test:cov
742
+
743
+ # Build
744
+ npm run build
745
+ ```
746
+
747
+ This project uses [Conventional Commits](https://www.conventionalcommits.org/) enforced by commitlint, and [Lefthook](https://github.com/evilmartians/lefthook) for pre-commit hooks (lint + format on staged files).
748
+
749
+ ---
750
+
751
+ ## License
752
+
753
+ [MIT](./LICENSE)