Stable HTTP response envelopes and typed error catalogs for TypeScript — with a NestJS module that works with zero config.
npm install js-outputThrow in your use cases. Shape responses once at the boundary.
Every Nest (and most Node HTTP) service ends up inventing the same three things:
- A consistent success/error JSON shape for clients
- Stable
errorIds you can throw from business code - A global exception filter + response interceptor
Envelope packages usually wrap responses. They rarely give you a catalog you can throw. This library does both.
import { Module } from '@nestjs/common';
import { JsOutputModule } from 'js-output/nest';
@Module({
imports: [JsOutputModule],
})
export class AppModule {}That registers:
- a global exception filter
- a response interceptor
- the
apipreset (product-shaped envelopes) - a built-in fallback for unknown errors (
Defaults.UNEXPECTED→APP-503-1)
No options required.
import { createErrors } from 'js-output';
export const Users = createErrors({
NOT_FOUND: {
status: 404,
errorId: 'USERS-404-1',
title: 'User not found',
message: 'No user exists for this id.',
},
} as const);import { Controller, Get, Param } from '@nestjs/common';
import { OkMessage } from 'js-output/nest';
import { Users } from './users.errors.js';
@Controller('users')
export class UsersController {
@Get(':id')
@OkMessage('User fetched successfully')
getOne(@Param('id') id: string) {
if (id !== '1') throw Users.NOT_FOUND;
return { id, name: 'Ada' };
}
}Success:
{
"statusCode": 200,
"message": "User fetched successfully",
"timestamp": "2026-08-03T12:00:00.000Z",
"data": { "id": "1", "name": "Ada" }
}Failure:
{
"statusCode": 404,
"errorId": "USERS-404-1",
"title": "User not found",
"message": "No user exists for this id.",
"timestamp": "2026-08-03T12:00:00.000Z"
}title is the short UI headline; message is the detail. Request path is off in production by default (path: 'auto') — see below.
forRoot accepts the same options as createApi:
JsOutputModule.forRoot({
service: 'billing-api',
fallback: Users.UNAVAILABLE, // catalog entry, not a magic string
debug: 'auto', // attach original error under `debug` outside production
path: 'auto', // echo request path outside production (default)
// path: true | false, // always / never
});Other Nest helpers (opt-in):
| Export | Use for |
|---|---|
@OkMessage(...) |
Per-route success copy |
validationError() |
ValidationPipe → catalog-shaped 400 |
DownstreamError / readDownstream |
Forward upstream error envelopes |
toHttp / AppHttpException |
Turn AppError into Nest HttpException |
Same core, no Nest:
import { createApi, createErrors } from 'js-output';
const api = createApi(); // api preset + Defaults.UNEXPECTED
const Orders = createErrors({
NOT_FOUND: {
status: 404,
errorId: 'ORDERS-404-1',
title: 'Order not found',
message: 'No order exists for this id.',
},
} as const);
app.get('/orders/:id', (req, res) => {
try {
const order = getOrder(req.params.id); // may throw Orders.NOT_FOUND
res.json(api.success(order, { path: req.path, message: 'Order fetched' }));
} catch (error) {
const body = api.failure(error, { path: req.path });
res.status(body.statusCode).json(body);
}
});Wire success / failure once at the HTTP boundary. Keep throwing in the rest of the app.
Prefer explicit fields — they are your public contract:
createErrors({
CONFLICT: {
status: 409,
errorId: 'ORDERS-409-1',
title: 'Conflict',
message: 'Order already exists.',
},
} as const);Helpers when you need them:
import { Defaults, errorsFromIds, withSeqIds } from 'js-output';
Defaults.UNEXPECTED; // APP-503-1 — used when something unexpected blows up
// Status lives in the id (ORDERS-ITEMS-404-1 → 404)
errorsFromIds({
NOT_FOUND: {
errorId: 'ORDERS-ITEMS-404-1',
title: 'Not found',
message: 'Missing item',
},
});
// Auto-seq ids: PREFIX-STATUS-N
withSeqIds('BILLING', {
INVALID: { status: 400, title: 'Invalid', message: 'Bad input' },
});| Preset | Behavior |
|---|---|
api (default) |
Product envelope; path: 'auto'; unknown errors → APP-503-1; downstream 500 → 503 |
detailed |
Rich fields without the opinionated fallback/remap policies |
minimal |
{ statusCode, message, data? } only |
createApi({ preset: 'minimal' });Client messages stay safe. Original errors are not thrown away:
- Nest logs unexpected throws (message + stack) via Nest’s Logger
onFailure(envelope, original)always receives the raw valuedebug: 'auto'(default) adds{ message, name, stack }on the failure body outside productionpath: 'auto'(default onapi) echoes the request pathname outside production (true/falseto force)
createApi({
debug: false, // never expose debug on the wire
onFailure(envelope, original) {
// your access log / metrics
},
});js-output
createApi, createErrors, errorsFromIds, withSeqIds, statusFromId, AppError, isAppError, Defaults, plus small Result helpers (ok / err / …).
js-output/nest
JsOutputModule, OutputFilter, OutputInterceptor, OkMessage, DownstreamError, readDownstream, validationError, toHttp, AppHttpException.
Result helpers are optional. Prefer throw.
npm run example:plain # core only
npm run example:nest # Nest on :3000 — /users/ok | /users/missing | /users/boomThis is 0.x. Envelope and catalog shapes may still evolve; see CHANGELOG.md.
After 1.0, field changes that clients depend on are semver-major.
MIT © Otavio Castejon
See CONTRIBUTING.md to develop locally.