nest-problem-details-filter
v1.8.0
Published
A NestJS exception filter to convert JSON responses to RFC 9457 (formerly RFC 7807)-compliant Problem Details for HTTP APIs. Standardizes HTTP error responses and sets Content-Type to application/problem+json.
Maintainers
Readme
NestHttpProblemDetails (RFC 9457 / RFC 7807)
Make NestJS return RFC 9457 (formerly RFC 7807)-compliant Problem Details for HTTP APIs. Drop in one filter — no code changes — and every error in your app starts speaking application/problem+json.
Keywords: RFC 9457, RFC 7807, Problem Details, HTTP API errors, NestJS, application/problem+json.
Quick start
npm i nest-problem-details-filter// main.ts
import { HttpAdapterHost, NestFactory } from '@nestjs/core';
import { HttpExceptionFilter } from 'nest-problem-details-filter';
import { AppModule } from './app/app.module';
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new HttpExceptionFilter(app.get(HttpAdapterHost)));Every HttpException your app throws now serializes to application/problem+json:
- HTTP/1.1 404 Not Found
- Content-Type: application/json
- { "statusCode": 404, "message": "Dragon not found" }
+ HTTP/1.1 404 Not Found
+ Content-Type: application/problem+json
+ { "type": "not-found", "title": "Dragon not found", "status": 404 }See Usage for module setup, Retry-After, validation errors, and Swagger.
Features
- RFC 9457 / RFC 7807 Compliant - Standardized Problem Details for HTTP APIs
Retry-Afterheader support - Per RFC 9110 §10.2.3, opt-in viaProblemDetailsExceptionor anyHttpExceptionsubclass exposingretryAfter- Swagger / OpenAPI decorator (optional) -
@ApiProblemResponse()vianest-problem-details-filter/swaggersubpath auto-documentsapplication/problem+jsonwithout forcing@nestjs/swaggeron users who don't need it - Docs / runtime alignment - Shared resolvers guarantee OpenAPI examples match the wire format (status-to-type map, title fallbacks, base-URI resolution)
- Flexible validation error handling - Three approaches from zero-config to full RFC 9457 JSON Pointer compliance (see Validation errors)
- Zero runtime dependencies - Core filter has no runtime dependencies
Table of contents:
- NestHttpProblemDetails (RFC 9457 / RFC 7807)
Usage
Install the library with:
# npm
npm i nest-problem-details-filter
# or, pnpm
pnpm i nest-problem-details-filterThen check NestJS documentation on how to bind exception filters.
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)));
...
}Note that the app.get(HttpAdapterHost) argument is needed because the HttpExceptionFilter works for any kind of NestJS HTTP adapter!
HttpExceptionFilter accepts a base URI for 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.
When you use Nest's built-in exceptions (e.g. NotFoundException, BadRequestException) or throw a plain HttpException without a custom ProblemDetailsException, the filter maps the exception's built-in message directly into detail. This means stack traces, database error strings, or other sensitive messages can leak to the client without any extra effort on your part.
For example, throwing new InternalServerErrorException('Database connection timeout') will produce:
{
"type": "internal-server-error",
"title": "Internal Server Error",
"status": 500,
"detail": "Database connection timeout"
}The suppressDetail option accepts either a boolean or a callback:
import { HttpExceptionFilter, SuppressDetail } from 'nest-problem-details-filter';
// Always suppress detail on every response:
app.useGlobalFilters(new HttpExceptionFilter(app.get(HttpAdapterHost), '', undefined, true));
// Or suppress selectively with a callback:
app.useGlobalFilters(new HttpExceptionFilter(app.get(HttpAdapterHost), '', undefined, ({ status }) => status >= 500));With the callback above, detail is stripped from all 5xx responses while remaining visible on 4xx responses (where it is typically safe and useful, e.g. validation messages). See docs/usage.md for the full API including the SUPPRESS_DETAIL_KEY DI token for module usage.
As a module
The library ships as a dynamic module. Use register() (or registerAsync()) to configure it, and HTTP_EXCEPTION_FILTER_KEY to bind it to APP_FILTER:
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 {}For config-driven setups, registerAsync() resolves options from a factory:
NestProblemDetailsModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
baseUri: config.get('PROBLEMS_BASE_URI'),
}),
});Importing NestProblemDetailsModule directly (without calling register()) still works and uses sensible defaults. The legacy pattern of overriding BASE_PROBLEMS_URI_KEY, HTTP_ERRORS_MAP_KEY and SUPPRESS_DETAIL_KEY providers manually is also still supported.
See:
Throwing exceptions
To produce a Problem Details response, throw either:
ProblemDetailsException— accepts a flat RFC 9457 payload directly (recommended for new code), or- a native
HttpException(NotFoundException,ForbiddenException, custom subclasses, ...) — the filter recognizes the standard Nest payload shape.
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.',
balance: 30,
});type is optional; when omitted, the filter resolves it from its status-to-type map (or falls back to about:blank, per RFC 9457 §4.2.1).
Retry-After header
Pass retryAfter as a number (delta-seconds), Date (absolute), or pre-formatted string on any retriable error response. The filter sets the Retry-After header per RFC 9110 §10.2.3 and strips the value from the JSON body. Common cases are 429 Too Many Requests (rate limiting) and 503 Service Unavailable (maintenance / backpressure), but the library imposes no status restriction.
throw new ProblemDetailsException({
type: 'rate-limit-exceeded',
title: 'Too Many Requests',
status: 429,
detail: 'Quota exceeded.',
retryAfter: 3600, // → "Retry-After: 3600"
});The filter reads retryAfter from any HttpException instance (duck-typed), so you can extend Nest's built-in exceptions instead:
import { ServiceUnavailableException } from '@nestjs/common';
import { RetryAfterValue } from 'nest-problem-details-filter';
class MaintenanceException extends ServiceUnavailableException {
constructor(public readonly retryAfter: RetryAfterValue) {
super('Maintenance window in progress.');
}
}
throw new MaintenanceException(300); // → "Retry-After: 300"See docs/usage.md for the full set of examples (including the native HttpException form and Retry-After details).
Swagger / OpenAPI
If you use @nestjs/swagger, import @ApiProblemResponse from the nest-problem-details-filter/swagger subpath to document application/problem+json responses:
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. It auto-generates:
- The canonical
ProblemDetailsschema undercontent['application/problem+json'] - A response example with
type,title, andstatus - The
Retry-Afterheader schema whenretryAfteris provided
If your filter is configured with a BASE_PROBLEMS_URI, pass the same value as baseUri so the OpenAPI docs match the runtime wire format:
@ApiProblemResponse({ status: 404, type: 'not-found', baseUri: 'https://api.example.com/problems' })
// OpenAPI example type → "https://api.example.com/problems/not-found"Likewise, if you override the default status-to-type map via HTTP_ERRORS_MAP_KEY, pass the same map as httpErrors:
@ApiProblemResponse({ status: 404, httpErrors: { 404: 'missing-resource' } })
// OpenAPI example type → "missing-resource" (not the built-in default)By default the decorator inlines the schema in every response so it works out of the box. If you want a named ProblemDetails entry 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);See docs/usage.md for the full decorator API (custom schemas, explicit examples, headers, etc.).
Preview: copy
tests/fixtures/swagger-document.jsonand paste it into editor.swagger.io to see how the decorator renders in Swagger UI.
Example response
# curl -i http://localhost:3333/api/dragons/99?title=true&details=true
HTTP/1.1 404 Not Found
Content-Type: application/problem+json; charset=utf-8
Content-Length: 109
...
{
"type": "not-found",
"title": "Dragon not found",
"status": 404,
"detail": "Could not find any dragon with ID: 99"
}OpenAPI schema
Full JSON Schema and OpenAPI 3.0 definitions are available in docs/openapi.md.
components:
schemas:
ProblemDetails:
type: object
description: >
Problem Details object as defined by RFC 9457 (formerly RFC 7807).
Returned with media type `application/problem+json`.
required:
- type
- title
- status
properties:
type:
type: string
format: uri-reference
maxLength: 1024
default: 'about:blank'
description: >
A URI reference that identifies the problem type. Per RFC 9457
this is a URI-reference (may be relative). Defaults to
"about:blank" when not provided.
example: 'about:blank'
title:
type: string
maxLength: 1024
description: >
A short, human-readable summary of the problem type. It should
not change from occurrence to occurrence of the problem, except
for purposes of localization.
example: 'Not Found'
status:
type: integer
format: int32
minimum: 100
maximum: 599
description: >
The HTTP status code generated by the origin server for this
occurrence of the problem.
example: 404
detail:
type: string
maxLength: 4096
description: >
A human-readable explanation specific to this occurrence of the
problem.
example: 'Could not find any dragon with ID: 99'
instance:
type: string
format: uri-reference
maxLength: 1024
description: >
A URI reference that identifies the specific occurrence of the
problem.
example: '/dragons/99'
additionalProperties: trueDocumentation
Check the docs/ folder for usage examples and the OpenAPI schema.
Validation errors
The filter ships three flexible approaches for surfacing class-validator validation errors — all using the errors RFC 9457 extension member (not detail, per §3.1.4).
Peer dependency: the helpers below require
class-validator(already a NestJS validation standard). Install it alongside the filter:npm install class-validator
Approach 1 — Zero config
Just register ValidationPipe + HttpExceptionFilter. The filter detects the string[] message Nest emits and moves it to errors automatically (why not using details as array?):
{
"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"]
}Approach 2 — Field-map via BadRequestException
Use mapClassValidatorErrors() in exceptionFactory for per-field grouping with dotted-path nesting:
import { mapClassValidatorErrors } from 'nest-problem-details-filter/class-validator-mappers';
new ValidationPipe({
exceptionFactory: (e) => new BadRequestException({ message: 'Validation failed', errors: mapClassValidatorErrors(e) }),
});{
"type": "bad-request",
"title": "Validation failed",
"status": 400,
"errors": {
"email": ["must be an email"],
"address.street": ["should not be empty"]
}
}Approach 3 — ProblemDetailsException (field-map or RFC pointer array)
Use toValidationProblemDetails() for a one-liner that returns a ProblemDetailsException directly:
import { toValidationProblemDetails } from 'nest-problem-details-filter/class-validator-mappers';
// Field-map (default)
new ValidationPipe({ exceptionFactory: (e) => toValidationProblemDetails(e) });
// RFC 9457 JSON Pointer array
new ValidationPipe({ exceptionFactory: (e) => toValidationProblemDetails(e, { usePointers: true }) });Pointer format output:
{
"type": "validation-error",
"title": "Validation Failed",
"status": 400,
"errors": [
{ "detail": "must be an email", "pointer": "#/email" },
{ "detail": "should not be empty", "pointer": "#/address/street" }
]
}See docs/usage.md for the full walkthrough including nested objects, custom validators, and all options.
Development
Tests
The library includes reusable integration tests that run against real NestJS applications backed by Express and Fastify to verify that the filter works correctly with each HTTP adapter.
# Run all tests (unit + integration)
npm test
# Watch mode
npm run test:watch
# With coverage
npm run test:covMock app
A local NestJS server is available for manual testing and exploring the Swagger / OpenAPI output. It reuses the same controllers as the integration tests, decorated with @ApiProblemResponse so the generated spec includes application/problem+json response examples.
# Start the mock app (reuses the same controllers as the integration tests)
npm run start:mock- API endpoints:
http://localhost:3000/api/test/... - Swagger UI:
http://localhost:3000/api
The mock app automatically restarts when you edit tests/mock-main.ts or tests/test-app.module.ts.
Lint & format
npm run lint # ESLint check (no auto-fix; fails on issues)
npm run lint:fix # ESLint with --fix
npm run format # Prettier (src/**/*.ts)Build
npm run build # Compiles src/ → dist/ via nest buildResources
- IETF RFC 9457: Problem Details for HTTP APIs (obsoletes RFC 7807)
- IETF RFC 7807: Problem Details for HTTP APIs (obsoleted)
- Zalando RESTful API:
- And of course, Nest's awesome community:
Contributing
We welcome contributions! Please see our CONTRIBUTING.md for guidelines on how to contribute to this project.
Security
For security-related issues, please review our SECURITY.md for responsible disclosure guidelines.
License
This project is licensed under the MIT License - see the LICENSE file for details
