https://github.com/fcmam5/nest-problem-details
A Nest.js HTTP exceptions filter returning RFC 9457 / RFC-7807 responses
https://github.com/fcmam5/nest-problem-details
api-rest hacktoberfest nest nestjs nodejs problem-details restful-api rfc-7807 rfc-9457
Last synced: about 1 month ago
JSON representation
A Nest.js HTTP exceptions filter returning RFC 9457 / RFC-7807 responses
- Host: GitHub
- URL: https://github.com/fcmam5/nest-problem-details
- Owner: Fcmam5
- License: mit
- Created: 2021-05-27T18:53:00.000Z (about 5 years ago)
- Default Branch: develop
- Last Pushed: 2026-05-11T22:30:56.000Z (about 1 month ago)
- Last Synced: 2026-05-11T22:31:08.178Z (about 1 month ago)
- Topics: api-rest, hacktoberfest, nest, nestjs, nodejs, problem-details, restful-api, rfc-7807, rfc-9457
- Language: TypeScript
- Homepage: https://www.npmjs.com/package/nest-problem-details-filter
- Size: 543 KB
- Stars: 46
- Watchers: 2
- Forks: 1
- Open Issues: 3
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Contributing: CONTRIBUTING.md
- Funding: .github/FUNDING.yml
- License: LICENSE
- Code of conduct: CODE_OF_CONDUCT.md
- Security: SECURITY.md
Awesome Lists containing this project
README
# NestHttpProblemDetails (RFC 9457 / RFC 7807)
[](https://www.npmjs.com/package/nest-problem-details-filter)
[](https://opensource.org/licenses/MIT)
[](https://github.com/Fcmam5/nest-problem-details/actions/workflows/main.yml)  [](https://coveralls.io/github/Fcmam5/nest-problem-details?branch=develop)
Make NestJS return [RFC 9457](https://datatracker.ietf.org/doc/html/rfc9457) (formerly [RFC 7807](https://datatracker.ietf.org/doc/html/rfc7807))-compliant **Problem Details for HTTP APIs**.
> Keywords: RFC 9457, RFC 7807, Problem Details, HTTP API errors, NestJS, application/problem+json.
## Features
- **RFC 9457 / RFC 7807 Compliant** - Standardized Problem Details for HTTP APIs
- **`Retry-After` header support** - Per RFC 9110 §10.2.3, opt-in via `ProblemDetailsException` or any `HttpException` subclass exposing `retryAfter`
- **Swagger / OpenAPI decorator (optional)** - `@ApiProblemResponse()` via `nest-problem-details-filter/swagger` subpath auto-documents `application/problem+json` without forcing `@nestjs/swagger` on 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](#validation-errors))
- **Zero runtime dependencies** - Core filter has no runtime dependencies
## Table of contents:
- [NestHttpProblemDetails (RFC 9457 / RFC 7807)](#nesthttpproblemdetails-rfc-9457--rfc-7807)
- [Features](#features)
- [Usage](#usage)
- [As a global filter](#as-a-global-filter)
- [Suppressing `detail` in production](#suppressing-detail-in-production)
- [As a module](#as-a-module)
- [Throwing exceptions](#throwing-exceptions)
- [`Retry-After` header](#retry-after-header)
- [Swagger / OpenAPI](#swagger--openapi)
- [Example response](#example-response)
- [OpenAPI schema](#openapi-schema)
- [Documentation](#documentation)
- [Validation errors](#validation-errors)
- [Approach 1 — Zero config](#approach-1--zero-config)
- [Approach 2 — Field-map via `BadRequestException`](#approach-2--field-map-via-badrequestexception)
- [Approach 3 — `ProblemDetailsException` (field-map or RFC pointer array)](#approach-3--problemdetailsexception-field-map-or-rfc-pointer-array)
- [Development](#development)
- [Tests](#tests)
- [Mock app](#mock-app)
- [Lint \& format](#lint--format)
- [Build](#build)
- [Resources](#resources)
- [Contributing](#contributing)
- [Security](#security)
- [License](#license)
## Usage
Install the library with:
```bash
# npm
npm i nest-problem-details-filter
# or, pnpm
pnpm i nest-problem-details-filter
```
Then check [NestJS documentation](https://docs.nestjs.com/exception-filters#binding-filters) on how to bind exception filters.
### As a global filter
In `main.ts` add `app.useGlobalFilters(new HttpExceptionFilter(app.get(HttpAdapterHost)))` as the following
```ts
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](https://docs.nestjs.com/faq/http-adapter)!
`HttpExceptionFilter` accepts a base URI for if you want to return absolute URIs for your problem types, e.g:
```ts
app.useGlobalFilters(new HttpExceptionFilter(app.get(HttpAdapterHost), 'https://example.org'));
```
Will return:
```json
{
"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.
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:
```json
{
"type": "internal-server-error",
"title": "Internal Server Error",
"status": 500,
"detail": "Database connection timeout"
}
```
The `suppressDetail` option accepts either a boolean or a callback:
```ts
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`](./docs/usage.md#suppressing-detail-in-production) for the full API including the `SUPPRESS_DETAIL_KEY` DI token for module usage.
### As a module
The library can be imported as a module, and then can use `HTTP_EXCEPTION_FILTER_KEY` to set `APP_FILTER`
```typescript
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:
- [Custom providers: Alias providers (`useExisting`)](https://docs.nestjs.com/fundamentals/custom-providers#alias-providers-useexisting)
- [Using `APP_FILTER` token](https://docs.nestjs.com/exception-filters#binding-filters)
### 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.
```ts
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](https://datatracker.ietf.org/doc/html/rfc9110#section-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.
```ts
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:
```ts
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`](./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:
```ts
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 `ProblemDetails` schema under `content['application/problem+json']`
- A response example with `type`, `title`, and `status`
- The `Retry-After` header schema when `retryAfter` is 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:
```ts
@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`:
```ts
@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:
```ts
import { addProblemDetailsSchema } from 'nest-problem-details-filter/swagger';
const document = SwaggerModule.createDocument(app, builder);
addProblemDetailsSchema(document);
```
See [`docs/usage.md`](./docs/usage.md) for the full decorator API (custom schemas, explicit `examples`, `headers`, etc.).
> **Preview**: copy [`tests/fixtures/swagger-document.json`](./tests/fixtures/swagger-document.json) and paste it into [editor.swagger.io](https://editor.swagger.io) to see how the decorator renders in Swagger UI.
### Example response
```bash
# 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`](./docs/openapi.md).
```yaml
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: true
```
### Documentation
Check the [`docs/`](./docs/) folder for usage examples and the [OpenAPI schema](./docs/openapi.md).
### 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:
> ```bash
> 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?](./docs/usage.md#validation-error-handling)):
```json
{
"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:
```ts
import { mapClassValidatorErrors } from 'nest-problem-details-filter/class-validator-mappers';
new ValidationPipe({
exceptionFactory: (e) =>
new BadRequestException({ message: 'Validation failed', errors: mapClassValidatorErrors(e) }),
})
```
```json
{
"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:
```ts
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:
```json
{
"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`](./docs/usage.md#validation-error-handling) 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.
```bash
# Run all tests (unit + integration)
npm test
# Watch mode
npm run test:watch
# With coverage
npm run test:cov
```
### Mock 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.
```bash
# 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
```bash
npm run lint # ESLint with auto-fix
npm run format # Prettier (src/**/*.ts)
```
### Build
```bash
npm run build # Compiles src/ → dist/ via nest build
```
## Resources
- [IETF RFC 9457: Problem Details for HTTP APIs](https://datatracker.ietf.org/doc/html/rfc9457) (obsoletes RFC 7807)
- [IETF RFC 7807: Problem Details for HTTP APIs (obsoleted)](https://datatracker.ietf.org/doc/html/rfc7807)
- [Zalando RESTful API:](https://opensource.zalando.com/restful-api-guidelines/#176)
- And of course, Nest's awesome community:
- [Exception filters](https://docs.nestjs.com/exception-filters#exception-filters-1)
- [@kamilmysliwiec's comment](https://github.com/nestjs/nest/issues/2953#issuecomment-531678153)
## Contributing
We welcome contributions! Please see our [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines on how to contribute to this project.
## Security
For security-related issues, please review our [SECURITY.md](./SECURITY.md) for responsible disclosure guidelines.
## License
This project is licensed under the MIT License - see the [LICENSE](./LICENSE) file for details