Usage Documentation

May 12, 2026 · View on GitHub

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 detail field can expose internal error messages to clients. Use the suppressDetail option 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:

FieldTypeDescription
statusnumberHTTP status code of the response
typestringResolved problem type URI
exceptionHttpExceptionThe 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 NestProblemDetailsModule statically, you can still override the SUPPRESS_DETAIL_KEY provider 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:

OptionTypeDefaultDescription
baseUristring''Base URI prepended to every problem type.
httpErrorsMapRecord<number, string>DEFAULT_HTTP_ERRORSStatus-to-type slug overrides; merged on top of the defaults.
suppressDetailboolean | (ctx) => booleanundefinedSee 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.

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 bodyRetry-After is header semantics, not problem-detail body semantics.

Three input forms are accepted:

TypeMeaningOn-wire format
numberNon-negative delta-secondsInteger string; fractional values rounded up
DateAbsolute retry instantIMF-fixdate via Date.toUTCString()
stringCaller-formatted, passed through unchangedWhatever 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: retryAfter lives on the exception instance, not the JSON body. The filter reads it via exception.retryAfter, so any custom HttpException subclass 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 ProblemDetails schema under content['application/problem+json']
  • A response example with type, title, and status
  • The Retry-After header schema when retryAfter is 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.json is 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 errors and not detail? RFC 9457 §3.1.4 states: "Consumers SHOULD NOT parse the detail member for information; extensions are more suitable and less error-prone ways to obtain such information." All three approaches use the errors extension member, not detail.

Why not invalid-params? invalid-params appeared 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

ExportReturnsUse 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?)ProblemDetailsExceptionOne-liner exceptionFactory for approaches 3A and 3B