Usage Documentation
May 12, 2026 · View on GitHub
- Usage Documentation
As a global filter
In main.ts add app.useGlobalFilters(new HttpExceptionFilter(app.get(HttpAdapterHost))) as the following:
import { NestFactory, HttpAdapterHost } from '@nestjs/core';
import { HttpExceptionFilter } from 'nest-problem-details-filter';
import { AppModule } from './app/app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new HttpExceptionFilter(app.get(HttpAdapterHost)));
// ...
}
HttpExceptionFilter accepts a base URI if you want to return absolute URIs for your problem types, e.g:
app.useGlobalFilters(new HttpExceptionFilter(app.get(HttpAdapterHost), 'https://example.org'));
Will return:
{
"type": "https://example.org/not-found",
"title": "Dragon not found",
"status": 404,
"detail": "Could not find any dragon with ID: 99"
}
Suppressing detail in production
Recommended for production deployments. The
detailfield can expose internal error messages to clients. Use thesuppressDetailoption to omit it — either always, or based on custom logic.
Pass true as the fourth constructor argument to suppress detail on every response:
import { HttpExceptionFilter } from 'nest-problem-details-filter';
app.useGlobalFilters(new HttpExceptionFilter(app.get(HttpAdapterHost), '', undefined, true));
Or pass a callback for conditional suppression. When the callback returns true, detail is omitted:
import { HttpExceptionFilter, SuppressDetail } from 'nest-problem-details-filter';
const suppress: SuppressDetail = ({ status }) => status >= 500;
app.useGlobalFilters(new HttpExceptionFilter(app.get(HttpAdapterHost), '', undefined, suppress));
The callback receives a SuppressDetailContext with three fields:
| Field | Type | Description |
|---|---|---|
status | number | HTTP status code of the response |
type | string | Resolved problem type URI |
exception | HttpException | The original caught exception |
This allows fine-grained control:
// Hide detail for all 5xx errors
({ status }) => status >= 500
// Hide detail only for a specific problem type
({ type }) => type === 'about:blank'
// Keep detail visible for known safe exceptions, hide for everything else 5xx
({ status, exception }) =>
status >= 500 && !(exception instanceof MyKnownSafeException)
When using the module, pass suppressDetail to register():
import { NestProblemDetailsModule, HTTP_EXCEPTION_FILTER_KEY } from 'nest-problem-details-filter';
@Module({
imports: [
NestProblemDetailsModule.register({
suppressDetail: ({ status }) => status >= 500, // or: true
}),
],
providers: [
{
provide: APP_FILTER,
useExisting: HTTP_EXCEPTION_FILTER_KEY,
},
],
})
export class AppModule {}
Legacy: if you import
NestProblemDetailsModulestatically, you can still override theSUPPRESS_DETAIL_KEYprovider directly:{ provide: SUPPRESS_DETAIL_KEY, useValue: true }
When detail is suppressed, the response omits the field entirely:
{
"type": "internal-server-error",
"title": "Something went wrong",
"status": 500
}
As a module
The library ships as a dynamic module. The recommended pattern is NestProblemDetailsModule.register():
import { APP_FILTER } from '@nestjs/core';
import { NestProblemDetailsModule, HTTP_EXCEPTION_FILTER_KEY } from 'nest-problem-details-filter';
@Module({
imports: [
NestProblemDetailsModule.register({
baseUri: 'https://api.example.org/problems',
httpErrorsMap: { 418: 'teapot-error' },
suppressDetail: ({ status }) => status >= 500,
}),
],
providers: [
{
provide: APP_FILTER,
useExisting: HTTP_EXCEPTION_FILTER_KEY,
},
],
})
export class AppModule {}
register() accepts:
| Option | Type | Default | Description |
|---|---|---|---|
baseUri | string | '' | Base URI prepended to every problem type. |
httpErrorsMap | Record<number, string> | DEFAULT_HTTP_ERRORS | Status-to-type slug overrides; merged on top of the defaults. |
suppressDetail | boolean | (ctx) => boolean | undefined | See Suppressing detail in production. |
registerAsync()
For options sourced from a ConfigService or another injectable:
NestProblemDetailsModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
baseUri: config.get('PROBLEMS_BASE_URI'),
suppressDetail: config.get('NODE_ENV') === 'production',
}),
});
Static (zero-config) usage
Importing NestProblemDetailsModule directly without calling register() still works and uses sensible defaults (empty baseUri, DEFAULT_HTTP_ERRORS, no suppressDetail):
@Module({
imports: [NestProblemDetailsModule],
providers: [{ provide: APP_FILTER, useExisting: HTTP_EXCEPTION_FILTER_KEY }],
})
export class AppModule {}
The legacy pattern of overriding BASE_PROBLEMS_URI_KEY, HTTP_ERRORS_MAP_KEY and SUPPRESS_DETAIL_KEY providers manually is also still supported for advanced use cases.
See:
Throwing exceptions
You can produce a Problem Details response in two ways.
Recommended: ProblemDetailsException
A dedicated exception that accepts a flat RFC 9457 payload directly — no nesting required. Custom extension members (e.g. balance, accounts) are forwarded as-is.
import { ProblemDetailsException } from 'nest-problem-details-filter';
throw new ProblemDetailsException({
type: 'out-of-credit',
title: 'You do not have enough credit.',
status: 403,
detail: 'Your balance is 30, but that costs 50.',
instance: '/account/12345/msgs/abc',
balance: 30,
accounts: ['/account/12345', '/account/67890'],
});
Produces:
HTTP/1.1 403 Forbidden
Content-Type: application/problem+json; charset=utf-8
{
"type": "out-of-credit",
"title": "You do not have enough credit.",
"status": 403,
"detail": "Your balance is 30, but that costs 50.",
"instance": "/account/12345/msgs/abc",
"balance": 30,
"accounts": ["/account/12345", "/account/67890"]
}
Optional type
type is the only optional field on ProblemDetailsInput. When omitted, the filter resolves it from its status-to-type map (or falls back to about:blank, per RFC 9457 §4.2.1).
throw new ProblemDetailsException({
status: 404,
title: 'Dragon not found',
});
// → { "type": "not-found", "title": "Dragon not found", "status": 404 }
Alternative: native HttpException
The filter also recognizes the nested { message, error } shape produced by NestJS built-ins (NotFoundException, ForbiddenException, etc.) and any custom HttpException:
throw new HttpException(
{
message: 'You do not have enough credit.',
error: {
type: 'out-of-credit',
detail: 'Your balance is 30, but that costs 50.',
balance: 30,
},
},
403,
);
Both forms produce identical responses; prefer ProblemDetailsException for new code.
Retry-After header
RFC 9457 §4 permits problem types to specify the Retry-After response header (defined in RFC 9110 §10.2.3). Common cases are 503 Service Unavailable (maintenance / backpressure) and 429 Too Many Requests (rate limiting, per RFC 6585). The library imposes no status restriction — set retryAfter on any error response where indicating a retry delay is appropriate.
ProblemDetailsException accepts an optional retryAfter field. The filter sets the Retry-After HTTP header and strips the value from the JSON body — Retry-After is header semantics, not problem-detail body semantics.
Three input forms are accepted:
| Type | Meaning | On-wire format |
|---|---|---|
number | Non-negative delta-seconds | Integer string; fractional values rounded up |
Date | Absolute retry instant | IMF-fixdate via Date.toUTCString() |
string | Caller-formatted, passed through unchanged | Whatever you supply (must be non-blank) |
// Rate limiting — delta-seconds:
throw new ProblemDetailsException({
type: 'rate-limit-exceeded',
title: 'Too Many Requests',
status: 429,
detail: 'Quota exceeded.',
retryAfter: 3600,
});
Produces:
HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json; charset=utf-8
Retry-After: 3600
{
"type": "rate-limit-exceeded",
"title": "Too Many Requests",
"status": 429,
"detail": "Quota exceeded."
}
// Scheduled maintenance — absolute date:
throw new ProblemDetailsException({
type: 'service-maintenance',
title: 'Service Unavailable',
status: 503,
detail: 'Maintenance window in progress.',
retryAfter: new Date('2026-04-30T06:00:00Z'),
});
// → Retry-After: Thu, 30 Apr 2026 06:00:00 GMT
Invalid values (negative seconds, non-finite numbers, invalid Date) are silently dropped — no header is emitted rather than a malformed one.
Note:
retryAfterlives on the exception instance, not the JSON body. The filter reads it viaexception.retryAfter, so any customHttpExceptionsubclass that exposes the same field will also produce the header.
With Nest's native exceptions
The duck-typed read means you can extend any built-in Nest exception (ServiceUnavailableException, HttpException, etc.) and expose retryAfter as an instance property — no need to switch to ProblemDetailsException:
import { ServiceUnavailableException } from '@nestjs/common';
import { RetryAfterValue } from 'nest-problem-details-filter';
export class MaintenanceException extends ServiceUnavailableException {
constructor(
public readonly retryAfter: RetryAfterValue,
message = 'Maintenance window in progress.',
) {
super(message);
}
}
// Usage:
throw new MaintenanceException(300); // → Retry-After: 300
throw new MaintenanceException(new Date('2026-04-30T06:00:00Z'));
The filter sets Retry-After from exception.retryAfter regardless of which HttpException subclass produced the exception.
Swagger / OpenAPI
If you use @nestjs/swagger, import @ApiProblemResponse from the nest-problem-details-filter/swagger subpath to document application/problem+json responses on your endpoints:
import { ApiProblemResponse } from 'nest-problem-details-filter/swagger';
@Controller('dragons')
export class DragonsController {
@Get(':id')
@ApiProblemResponse({ status: 404, type: 'not-found', title: 'Dragon not found' })
@ApiProblemResponse({ status: 429, type: 'rate-limit-exceeded', retryAfter: 3600 })
findOne(@Param('id') id: string) { ... }
}
The decorator is stackable: apply once per status code you want documented. It auto-generates:
- The canonical
ProblemDetailsschema undercontent['application/problem+json'] - A response example with
type,title, andstatus - The
Retry-Afterheader schema whenretryAfteris provided
Registering a named ProblemDetails model
By default the decorator inlines the schema in every response so it works out of the box. If you want a named ProblemDetails entry to appear in Swagger UI's Schemas section, call addProblemDetailsSchema() after creating the document:
import { addProblemDetailsSchema } from 'nest-problem-details-filter/swagger';
const document = SwaggerModule.createDocument(app, builder);
addProblemDetailsSchema(document);
This registers ProblemDetails under components.schemas so Swagger UI shows it as a reusable model.
Aligning with BASE_PROBLEMS_URI
If your filter is configured with a BASE_PROBLEMS_URI, pass the same value as baseUri so the documented example matches the wire format:
@ApiProblemResponse({
status: 404,
type: 'not-found',
baseUri: 'https://api.example.com/problems',
})
// OpenAPI example type → "https://api.example.com/problems/not-found"
Absolute references (about:blank, https://…) are passed through untouched per RFC 9457 §4.2.1.
Aligning with HTTP_ERRORS_MAP_KEY
If you override the built-in status-to-type map via HTTP_ERRORS_MAP_KEY, pass the same map as httpErrors so the documented example uses your custom defaults instead of the built-ins:
@ApiProblemResponse({
status: 404,
httpErrors: { 404: 'missing-resource' },
})
// OpenAPI example type → "missing-resource"
Preview:
tests/fixtures/swagger-document.jsonis a real OpenAPI 3.0 JSON generated by the integration test — copy it into editor.swagger.io to see how the decorator renders.
Example responses
Default Nest NotFoundException handler
curl -i http://localhost:3333/some-wrong-path
Response:
HTTP/1.1 404 Not Found
Content-Type: application/problem+json; charset=utf-8
{
"type": "not-found",
"title": "Cannot GET /some-wrong-path",
"status": 404,
"detail": "Not Found"
}
Throwing a HttpException with no parameters
Code:
throw new NotFoundException();
Response:
HTTP/1.1 404 Not Found
Content-Type: application/problem+json; charset=utf-8
{
"type": "not-found",
"title": "Not Found",
"status": 404
}
Throwing a HttpException with a title
Code:
throw new NotFoundException('Dragon not found');
Response:
HTTP/1.1 404 Not Found
Content-Type: application/problem+json; charset=utf-8
{
"type": "not-found",
"title": "Dragon not found",
"status": 404,
"detail": "Not Found"
}
Throwing a HttpException with a title and description
Code:
throw new NotFoundException('Dragon not found', `Could not find any dragon with ID: ${id}`);
Response:
HTTP/1.1 404 Not Found
Content-Type: application/problem+json; charset=utf-8
{
"type": "not-found",
"title": "Dragon not found",
"status": 404,
"detail": "Could not find any dragon with ID: 99"
}
Validation error handling
The library supports three approaches for surfacing class-validator errors. Choose the one that fits your use case.
Peer dependency: these helpers require
class-validator(already a NestJS validation standard). Install it alongside the filter:npm install class-validator
Why
errorsand notdetail? RFC 9457 §3.1.4 states: "Consumers SHOULD NOT parse thedetailmember for information; extensions are more suitable and less error-prone ways to obtain such information." All three approaches use theerrorsextension member, notdetail.
Why not
invalid-params?invalid-paramsappeared only in an RFC 7807 example and was not standardised in RFC 9457. Avoid it.
Approach 1 — Zero config (default ValidationPipe)
Register HttpExceptionFilter as usual. When Nest's default ValidationPipe rejects a request it throws BadRequestException with message: string[]. The filter detects this and moves the array into the errors extension — no extra configuration needed.
// main.ts
app.useGlobalPipes(new ValidationPipe());
app.useGlobalFilters(new HttpExceptionFilter(app.get(HttpAdapterHost)));
{
"type": "bad-request",
"title": "Bad Request",
"status": 400,
"detail": "Bad Request",
"errors": ["username must be longer than or equal to 3 characters", "email must be an email", "address.street should not be empty"]
}
Messages are flat (no field grouping) because Nest's default pipe does not expose field keys in its output.
Approach 2 — BadRequestException with exceptionFactory
Use mapClassValidatorErrors() inside exceptionFactory to group messages by field name, then throw Nest's built-in BadRequestException. The filter picks up the errors object and passes it through.
import { mapClassValidatorErrors } from 'nest-problem-details-filter/class-validator-mappers';
app.useGlobalPipes(
new ValidationPipe({
exceptionFactory: (validationErrors) =>
new BadRequestException({
message: 'Validation failed',
errors: mapClassValidatorErrors(validationErrors),
}),
}),
);
{
"type": "bad-request",
"title": "Validation failed",
"status": 400,
"errors": {
"username": ["must be longer than or equal to 3 characters"],
"email": ["must be an email"],
"address.street": ["should not be empty"],
"address.city": ["should not be empty"]
}
}
Nested objects are flattened with dotted-path keys (e.g. address.street). Custom validator messages appear under the correct field key just like built-in constraints.
Approach 3A — ProblemDetailsException with field-map (shorthand)
Use toValidationProblemDetails() for the shortest possible factory. It wraps mapClassValidatorErrors() and builds a fully-formed ProblemDetailsException in one call.
import { toValidationProblemDetails } from 'nest-problem-details-filter/class-validator-mappers';
app.useGlobalPipes(
new ValidationPipe({
exceptionFactory: (e) => toValidationProblemDetails(e),
}),
);
{
"type": "validation-error",
"title": "Validation Failed",
"status": 400,
"errors": {
"username": ["must be longer than or equal to 3 characters"],
"address.city": ["should not be empty"]
}
}
You can override the defaults:
exceptionFactory: (e) =>
toValidationProblemDetails(e, {
status: 422,
title: 'Unprocessable Entity',
type: 'unprocessable-entity',
});
Approach 3B — ProblemDetailsException with RFC 9457 JSON Pointer array
Pass { usePointers: true } to toValidationProblemDetails() to get strict RFC 9457 compliance. Each violation becomes a { detail, pointer } object where pointer is a JSON Pointer (#/field or #/nested/field).
import { toValidationProblemDetails } from 'nest-problem-details-filter/class-validator-mappers';
app.useGlobalPipes(
new ValidationPipe({
exceptionFactory: (e) => toValidationProblemDetails(e, { usePointers: true }),
}),
);
{
"type": "validation-error",
"title": "Validation Failed",
"status": 400,
"errors": [
{ "detail": "must be longer than or equal to 3 characters", "pointer": "#/username" },
{ "detail": "must be an email", "pointer": "#/email" },
{ "detail": "should not be empty", "pointer": "#/address/street" }
]
}
You can also build the pointer array manually via mapToPointerErrors() if you need custom filtering or sorting:
import { mapToPointerErrors, ProblemDetailsException } from 'nest-problem-details-filter/class-validator-mappers';
exceptionFactory: (validationErrors) =>
new ProblemDetailsException({
status: 400,
title: 'Validation Failed',
type: 'validation-error',
errors: mapToPointerErrors(validationErrors),
});
Exported helpers summary
| Export | Returns | Use case |
|---|---|---|
mapClassValidatorErrors(errors) | Record<string, string[]> | Build a field-map to pass to BadRequestException or ProblemDetailsException |
mapToPointerErrors(errors) | PointerError[] | Build an RFC 9457 pointer array manually |
toValidationProblemDetails(errors, options?) | ProblemDetailsException | One-liner exceptionFactory for approaches 3A and 3B |