Make NestJS return RFC 9457 (formerly RFC 7807)-compliant Problem Details for HTTP APIs.
Keywords: RFC 9457, RFC 7807, Problem Details, HTTP API errors, NestJS, application/problem+json.
- 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
- NestHttpProblemDetails (RFC 9457 / RFC 7807)
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.
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"
}The library can be imported as a module, and then can use HTTP_EXCEPTION_FILTER_KEY to set APP_FILTER
import { APP_FILTER } from '@nestjs/core';
import {
NestProblemDetailsModule,
HTTP_EXCEPTION_FILTER_KEY,
} from 'nest-problem-details-filter';
@Module({
imports: [NestProblemDetailsModule],
...
providers: [
{
provide: APP_FILTER,
useExisting: HTTP_EXCEPTION_FILTER_KEY,
},
...
],
})See:
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).
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).
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.
# 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"
}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: trueCheck the docs/ folder for usage examples and the OpenAPI schema.
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
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"
]
}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"]
}
}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.
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:covA 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.
npm run lint # ESLint with auto-fix
npm run format # Prettier (src/**/*.ts)npm run build # Compiles src/ → dist/ via nest build- 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:
We welcome contributions! Please see our CONTRIBUTING.md for guidelines on how to contribute to this project.
For security-related issues, please review our SECURITY.md for responsible disclosure guidelines.
This project is licensed under the MIT License - see the LICENSE file for details