nestjs-error-registry
v0.1.3
Published
Typed error catalog for NestJS — RFC 7807 responses, automatic Swagger integration, zero boilerplate
Maintainers
Readme
nestjs-error-registry
Typed error catalog for NestJS. Define all your HTTP errors once, throw them with full TypeScript inference, get RFC 7807 Problem Details responses automatically, and have Swagger @ApiResponse schemas generated via @Throws() — without touching a single @ApiResponse decorator.
The problem
// Scattered across your codebase — no contract, no types, no Swagger
throw new NotFoundException(`User with id ${id} not found`);
throw new ConflictException('Email already in use');No single source of truth. No TypeScript enforcement at the call site. Swagger docs require manual @ApiResponse maintenance. Error messages are strings with no structure.
The solution
// errors/user.errors.ts — one place, full contract
export const UserErrors = defineErrors({
NOT_FOUND: {
status: 404,
code: 'USR-404',
title: 'User Not Found',
message: (id: string) => `User with id ${id} not found`,
},
EMAIL_TAKEN: {
status: 409,
code: 'USR-409',
title: 'Email Already Taken',
message: 'Email already in use',
},
});// users.service.ts — TypeScript enforces the args
throw UserErrors.NOT_FOUND(userId); // (id: string) ✓
throw UserErrors.EMAIL_TAKEN(); // () ✓// users.controller.ts — Swagger wired automatically
@Get(':id')
@Throws(UserErrors.NOT_FOUND)
async getUser(@Param('id') id: string) {
return this.usersService.findById(id);
}// HTTP response — RFC 7807 Problem Details
{
"type": "https://example.com/errors/USR-404",
"title": "User Not Found",
"status": 404,
"detail": "User with id abc-123 not found",
"instance": "/users/abc-123",
"timestamp": "2026-06-07T20:00:00.000Z",
"code": "USR-404"
}Installation
npm install nestjs-error-registryPeer dependencies (already in your project):
npm install @nestjs/common @nestjs/core reflect-metadata
# optional — only needed for @Throws() Swagger integration:
npm install @nestjs/swaggerSetup
// app.module.ts
import { ErrorRegistryModule } from 'nestjs-error-registry';
@Module({
imports: [
ErrorRegistryModule.forRoot({
baseUrl: 'https://example.com',
}),
],
})
export class AppModule {}Defining errors
import { defineErrors } from 'nestjs-error-registry';
export const OrderErrors = defineErrors({
NOT_FOUND: {
status: 404,
code: 'ORD-404',
title: 'Order Not Found',
message: (id: string) => `Order ${id} not found`,
},
PAYMENT_FAILED: {
status: 402,
code: 'ORD-402',
title: 'Payment Failed',
message: (reason: string) => `Payment failed: ${reason}`,
},
DUPLICATE: {
status: 409,
code: 'ORD-409',
title: 'Duplicate Order',
message: 'An order with this reference already exists',
},
});Each definition has:
| Field | Required | Description |
|---|---|---|
| status | ✓ | HTTP status code |
| code | ✓ | Machine-readable error code (used in RFC 7807 type URI) |
| title | — | Short human-readable summary. Defaults to code |
| message | ✓ | Static string or factory function — TypeScript infers parameter types |
Throwing errors
// Factory message — TypeScript enforces (id: string)
throw OrderErrors.NOT_FOUND(orderId);
// Multi-arg factory
throw OrderErrors.PAYMENT_FAILED('insufficient funds');
// Static message — no args required
throw OrderErrors.DUPLICATE();The return type is never — TypeScript understands that execution stops here. Works with or without the explicit throw keyword.
@Throws() decorator
Declare which errors a route can throw. Automatically injects @ApiResponse into Swagger for each declared error with a full RFC 7807 schema.
@Controller('orders')
export class OrdersController {
@Post()
@Throws(OrderErrors.DUPLICATE, OrderErrors.PAYMENT_FAILED)
async create(@Body() dto: CreateOrderDto) {
return this.ordersService.create(dto);
}
@Get(':id')
@Throws(OrderErrors.NOT_FOUND)
async findOne(@Param('id') id: string) {
return this.ordersService.findById(id);
}
}Errors with the same HTTP status are merged into a single @ApiResponse with a code enum listing all possible error codes.
@nestjs/swagger is an optional peer dependency — if not installed, @Throws() sets the Reflect metadata but skips Swagger injection silently.
RFC 7807 response format
Every RegistryError is formatted as RFC 7807 Problem Details with Content-Type: application/problem+json:
{
"type": "https://example.com/errors/ORD-404",
"title": "Order Not Found",
"status": 404,
"detail": "Order ord-abc-123 not found",
"instance": "/orders/ord-abc-123",
"timestamp": "2026-06-07T20:00:00.000Z",
"code": "ORD-404"
}Module options
ErrorRegistryModule.forRoot({
// Prefix for the RFC 7807 'type' URI. Default: '' (relative: '/errors/ORD-404')
baseUrl: 'https://example.com',
// When true (default), non-RegistryError HttpExceptions are passed to the
// default NestJS handler. Set to false to handle everything here.
passthrough: true,
})Advanced: introspect declared errors at runtime
@Throws() stores ErrorMeta[] on the handler via Reflect.defineMetadata. Readable in guards, interceptors, or tooling:
import { THROWS_METADATA_KEY } from 'nestjs-error-registry';
const metas = Reflect.getMetadata(THROWS_METADATA_KEY, handler);
// [{ status: 404, code: 'ORD-404', title: 'Order Not Found', ... }]Using without ErrorRegistryModule
If you already have a global exception filter, use ErrorRegistryFilter directly:
import { ErrorRegistryFilter } from 'nestjs-error-registry';
app.useGlobalFilters(new ErrorRegistryFilter({ baseUrl: 'https://example.com' }));License
MIT
