A handler’s result becomes { success: true, data, error: null }, and a 204 sends no body. An error becomes { success: false, error: { code, message, data } }:
Error
Status
What the client sees
ApiException
its status, 400 to 599
its code, message, and data
Another Exception, such as a jg package’s
from resolveStatus, else 500
its code, message, and data
Nest’s HttpException
its status
HTTP_EXCEPTION, with its message
A client error Express’s middleware built for clients (expose: true), such as a body over the limit
its 4xx statusCode
HTTP_EXCEPTION, with its message
Anything else
from resolveStatus, else 500
INTERNAL_ERROR and a generic message, never the error’s own message
Throw an ApiException for failures the client should understand:
ts
import { ApiException } from 'jg-nestjs';throw new ApiException('The project was not found.', { code: 'PROJECT_NOT_FOUND', status: 404, severity: 'expected',});
Logging and reporting
The filter logs, through Nest’s Logger, an error whose throw site set shouldLog or severity: 'critical', and any error that is none of an Exception, an HttpException, and a client error Express’s middleware built for clients, with the method, the redacted URL, and the status. A report option sends errors to an error tracker, except those with severity: 'expected', the statuses in ignoredStatuses (401 and 429 by default), and the codes in ignoredCodes (CORS_ORIGIN_NOT_ALLOWED, MAINTENANCE_MODE, and IP_BLOCKED by default). Provide the options as API_EXCEPTION_FILTER_OPTIONS; redactUrl removes secrets from URLs before either sees them.
Validating input
SchemaValidationPipe validates each parameter that declares a Standard Schema, such as a Zod schema in @Body({ schema }). Invalid input gets a 400 with VALIDATION_FAILED and up to 32 { path, message } issues as its data, and a valid parameter becomes the schema’s output, without __proto__ keys.
It allows the listed origins exactly, their www. variants, and requests without an Origin header, which servers and command-line clients send, unless allowOriginless is false. Any other origin gets a 403 with CORS_ORIGIN_NOT_ALLOWED, through the filter, and isn’t reported.
Maintenance mode and IP blocklists
MaintenanceModeGuard refuses requests with a 503, MAINTENANCE_MODE, while getState says maintenance is on, with the state’s message as the error’s data. IpBlocklistGuard refuses requests from blocked addresses with a 403, IP_BLOCKED, without echoing the address. Each reads its options from its own token, and the app fails to start without them:
IpBlocklistGuard checks Express’s request.ip, which follows configureHttpApp’s trustProxyHops, or what a getIp option returns, after normalizeIp: trimmed, lowercased, and ::ffff:203.0.113.7 as 203.0.113.7. Normalize addresses the same way when adding them to the list. A request without an address goes through. Both guards fail closed: an error from getState or isBlocked refuses the request, which the filter answers with a 500 and reports. Other contexts, such as microservices, pass.
Run the cheap refusals first, so a flood or a blocked client never reaches authentication: the rate limiter, then IpBlocklistGuard, then MaintenanceModeGuard, then authentication. Guards in one @UseGuards(ThrottlerGuard, IpBlocklistGuard, MaintenanceModeGuard, AuthGuard) run in that order, and so do APP_GUARD providers, in the order they’re listed. A route that must stay up during maintenance, such as a health check, leaves MaintenanceModeGuard out.
Bootstrapping an app
configureHttpApp sets up an Express-based app before it listens, always in this order: JSON and URL-encoded body parsers with bodyLimit, the security middleware, the trusted proxy hops, the request log, versioning, and shutdown hooks on SIGINT and SIGTERM:
ts
import { NestFactory } from '@nestjs/core';import type { NestExpressApplication } from '@nestjs/platform-express';import { raw } from 'express';import helmet from 'helmet';import { configureHttpApp } from 'jg-nestjs';const app = await NestFactory.create<NestExpressApplication>(AppModule);// a webhook that checks a signature needs the exact bytes, so its parser comes firstapp.use('/billing/webhooks', raw({ type: 'application/json', limit: '1mb' }));configureHttpApp(app, { bodyLimit: '1mb', securityMiddleware: helmet(), trustProxyHops: 1 });await app.listen(3000);
The request log writes METHOD url status Nms through Nest’s Logger once each response finishes, at error for a 5xx, warn for a 4xx, and log otherwise, with the client’s address, user agent, referer, and response size as structured fields. Its redactUrl option cleans the URL and the referer first, and createRequestLoggingMiddleware gives the same middleware on its own. The prefix and the levels Nest prints belong to Nest’s own logger: NestFactory.create(AppModule, { logger: new ConsoleLogger({ prefix: 'API', logLevels: ['fatal', 'error', 'warn', 'log'] }) }).
Errors
Code
When
VALIDATION_FAILED
A parameter didn’t match its schema
HTTP_EXCEPTION
A handler, guard, or Nest itself threw an HttpException, such as a 404, or Express’s middleware refused the request with a client error, such as a body over the limit
INTERNAL_ERROR
An error the filter doesn’t recognize, such as a failed database call, reached it
CORS_ORIGIN_NOT_ALLOWED
A browser called from an origin the API doesn’t allow
MAINTENANCE_MODE
MaintenanceModeGuard refused a request while maintenance is on, with a 503
IP_BLOCKED
IpBlocklistGuard refused a request from a blocked address, with a 403
ERROR_MESSAGES holds each code’s message.
Generated from TSDoc
API reference
Every export of [email protected], 25 in all, with its signature, its errors, and an example.
Configures an Express-based Nest app before it listens, in this order: JSON and URL-encoded body parsers with bodyLimit, the security middleware, the trusted proxy hops, the request log, versioning, and shutdown hooks. A route that needs the raw body, such as a webhook that checks a signature, registers its own parser with app.use before this call, and the JSON parser then leaves its body alone.
Creates the CORS options for app.enableCors, allowing only the listed origins, matched exactly, and by default their www. variants and requests without an Origin header. Any other origin is refused with a 403 ApiException, code CORS_ORIGIN_NOT_ALLOWED, which ApiExceptionFilter answers and doesn't report. An origin that isn't a URL throws when the options are created.
Creates a middleware that logs each request once its response finishes, through Nest's Logger: METHOD url status Nms, at error for a 5xx, warn for a 4xx, and log otherwise, with the client's address, user agent, referer, and response size as structured fields when they're known. The URL and the referer go through redactUrl first. The prefix and the levels Nest prints are set on Nest's own ConsoleLogger.
Normalizes an IP address the way IpBlocklistGuard checks it: trimmed, lowercased, and an IPv4 address mapped into IPv6, as ::ffff:203.0.113.7, as plain IPv4. Normalize addresses the same way before adding them to the blocklist.
An Exception that sets the HTTP status of the response ApiExceptionFilter sends: its code, message, and data go to the client in the jg-api-response envelope, so they must be safe to show. The status is an integer from 400 to 599, and 500 when it's missing or anything else.
Members
codeproperty
string
dataproperty
IJsonValue | undefined
severityproperty
"expected" | "critical"
shouldLogproperty
boolean
toJSONmethod
toJSON(): IExceptionJson
Converts the exception to a plain object, which JSON.stringify calls.
toStringmethod
toString(): string
Describes the exception in one line, with its code.
statusproperty
number
the response's status, from 400 to 599
constructorconstructor
new ApiException(message: string, options: IApiExceptionOptions)
Creates an API exception.
Example
import { ApiException } from 'jg-nestjs';throw new ApiException('The project was not found.', { code: 'PROJECT_NOT_FOUND', status: 404, data: { id: 42 }, severity: 'expected',});
Answers every error an HTTP request throws, from a handler, guard, pipe, or middleware, with the jg-api-response envelope. An ApiException sets the status; an Exception sends its code, message, and data; an HttpException sends HTTP_EXCEPTION with its own message, as does a client error Express's middleware built for clients, such as a body over the limit, with its own 4xx; and anything else gets a 500 with INTERNAL_ERROR and a generic message, while the error itself goes only to the logs and the reporter. It logs an error that its throw site marked shouldLog or critical, or that is none of those anticipated kinds, and reports every error except an expected one or one whose status or code is ignored. Errors outside HTTP are rethrown.
Members
constructorconstructor
new ApiExceptionFilter(__adapterHost: HttpAdapterHost, options?: IApiExceptionFilterOptions)
Creates the filter. Nest injects its arguments.
catchmethod
catch(error: unknown, host: ArgumentsHost): void
Answers an error with the envelope, after logging and reporting it.
Example
import { Module } from '@nestjs/common';import { APP_FILTER } from '@nestjs/core';import { API_EXCEPTION_FILTER_OPTIONS, ApiExceptionFilter, type IApiExceptionFilterOptions } from 'jg-nestjs';const filterOptions: IApiExceptionFilterOptions = { redactUrl: (url) => url.replace(/token=[^&]+/, 'token=[redacted]'),};@Module({ providers: [ { provide: API_EXCEPTION_FILTER_OPTIONS, useValue: filterOptions }, { provide: APP_FILTER, useClass: ApiExceptionFilter }, ],})export class AppModule {}
Wraps what each HTTP handler returns in the jg-api-response envelope, { success: true, data, error: null }, which ApiExceptionFilter's error envelope matches. Other contexts, such as microservices, pass through untouched. A handler that answers 204 sends no body, envelope included.
Refuses HTTP requests from blocked IP addresses with a 403, IP_BLOCKED, which carries no data and which the filter doesn't report. It reads the client's address with getIp, Express's request.ip by default, normalizes it with normalizeIp, and lets a request without one through. It fails closed: an error from isBlocked refuses the request. It never echoes the address or changes the request. Other contexts, such as microservices, pass. Its options come from IP_BLOCKLIST_GUARD_OPTIONS, and the app fails to start without them.
Members
constructorconstructor
new IpBlocklistGuard(__options: IIpBlocklistGuardOptions)
Refuses HTTP requests with a 503 while the API is down for maintenance, reading the state on every request, so turning it on or off needs no restart. The error, MAINTENANCE_MODE, carries the state's message as its data when there is one, and the filter doesn't report it. It fails closed: an error reading the state refuses the request. Other contexts, such as microservices, pass. Its options come from MAINTENANCE_MODE_GUARD_OPTIONS, and the app fails to start without them.
Members
constructorconstructor
new MaintenanceModeGuard(__options: IMaintenanceModeGuardOptions)
Validates each parameter that declares a Standard Schema, such as a Zod schema in @Body({ schema }), and answers invalid input with a 400 ApiException: code VALIDATION_FAILED, with up to 32 { path, message } issues as its data. A valid parameter becomes the schema's output, and keys such as __proto__ are stripped before validation.
The message each error answers with. An HTTP_EXCEPTION answers with the HttpException's or the exposed client error's own message instead, and its entry describes it. INTERNAL_ERROR's is all a client sees of an unexpected error.
sends an error to an error tracker. It runs without delaying the response, and a throw or a rejection is logged
resolveStatusproperty
(error: unknown) => number | undefined
gives the status of an error that is none of an ApiException, an HttpException, and an exposed client error, such as one from a library. A value that isn't an integer from 400 to 599 is ignored
ignoredStatusesproperty
readonly number[]
the statuses that are never reported. Defaults to 401 and 429
ignoredCodesproperty
readonly string[]
the error codes that are never reported. Defaults to CORS_ORIGIN_NOT_ALLOWED, MAINTENANCE_MODE, and IP_BLOCKED
redactUrlproperty
(url: string) => string
removes secrets from a URL before it's logged or reported, such as a token in the query. Defaults to leaving it as it is
the most a JSON or URL-encoded body may hold, such as '1mb', or a number of bytes. A larger body is refused with a 413, which ApiExceptionFilter answers as HTTP_EXCEPTION
resolves to whether the normalized address is blocked, such as with jg-redis's blocklist. An error it throws refuses the request, which the filter answers with a 500 and reports
reads the current state on every request, such as from an environment flag or a stored record. An error it throws refuses the request, which the filter answers with a 500 and reports