@nestarc/safe-response
Standardized API response wrapper for NestJS — auto-wraps success/error responses, pagination metadata, and Swagger schema generation with a single module import.
Features
- Automatic response wrapping — all controller returns wrapped in
{ success, statusCode, data }structure - Error standardization — exceptions converted to
{ success: false, error: { code, message, details } } - Pagination metadata — offset (
page/limit/total) and cursor (nextCursor/hasMore) pagination with auto-calculated meta and HATEOAS links - Sort/Filter metadata —
@SortMeta()and@FilterMeta()decorators to include sorting and filtering info in responsemeta - Request ID tracking — opt-in
requestIdfield in all responses with incoming header reuse, auto-generation, and response header propagation - Response time — opt-in
meta.responseTime(ms) for performance monitoring - RFC 9457 Problem Details — opt-in standard error format with
application/problem+json - Swagger integration —
@ApiSafeResponse(Dto)for success schemas,@ApiSafeErrorResponse()/@ApiSafeErrorResponses()for error schemas — all with the wrapped envelope - Global error Swagger —
applyGlobalErrors()injects common error responses (401, 403, 500) into all OpenAPI operations - Frontend client types —
@nestarc/safe-response/clientprovides zero-dependency TypeScript types and type guards (isSuccess,isError,isPaginated,isProblemDetailsResponse,hasResponseTime,hasSort,hasFilters,isDeprecated,hasRateLimit) for frontend consumers - nestjs-i18n integration — automatic error/success message translation via adapter pattern
- API deprecation —
@Deprecated()decorator with RFC 9745/8594Deprecation/Sunsetheaders, Swaggerdeprecated: true, and responsemeta.deprecation - Rate limit metadata — opt-in
meta.rateLimitmirroring ofX-RateLimit-*response headers for frontend consumption - nestjs-cls integration — inject CLS store values (traceId, correlationId) into response
meta - class-validator support — validation errors parsed into
detailsarray with "Validation failed" message - Custom error codes — map exceptions to machine-readable codes via
errorCodeMapper - Composite decorators —
@SafeEndpoint(),@SafePaginatedEndpoint(),@SafeCursorPaginatedEndpoint()combine Swagger + runtime + error docs in a single decorator - Declarative error codes —
errorCodesoption for simple status-to-code mapping without writing a mapper function - Shape-mismatch warnings —
@Paginated(),@CursorPaginated(),@SortMeta(),@FilterMeta()warn when handler data doesn't match expected shape - Opt-out per route —
@RawResponse()skips wrapping for health checks, SSE, file downloads - Platform-agnostic — works with both Express and Fastify adapters out of the box
- Context-safe — automatically skips wrapping for non-HTTP contexts (RPC, WebSocket)
- Dynamic Module —
register()/registerAsync()with full DI support
Installation
npm install @nestarc/safe-responsePeer Dependencies
npm install @nestjs/common @nestjs/core @nestjs/swagger rxjs reflect-metadataQuick Start
import { Module } from '@nestjs/common';
import { SafeResponseModule } from '@nestarc/safe-response';
@Module({
imports: [SafeResponseModule.register()],
})
export class AppModule {}That's it. All routes now return standardized responses.
With Fastify
Works the same way — no extra configuration needed:
import { NestFactory } from '@nestjs/core';
import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';
import { AppModule } from './app.module';
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter(),
);
await app.listen(3000);Response Format
Success
{
"success": true,
"statusCode": 200,
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"data": { "id": 1, "name": "John" },
"timestamp": "2025-03-21T12:00:00.000Z",
"path": "/api/users/1"
}
requestIdis only present whenrequestIdoption is enabled. See Request ID.
Error
{
"success": false,
"statusCode": 400,
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"error": {
"code": "BAD_REQUEST",
"message": "Validation failed",
"details": ["email must be an email", "name should not be empty"]
},
"timestamp": "2025-03-21T12:00:00.000Z",
"path": "/api/users"
}Decorators
@ApiSafeResponse(Model)
Documents the Swagger data field with a specific DTO type.
@Get(':id')
@ApiSafeResponse(UserDto)
async findOne(@Param('id') id: string) {
return this.usersService.findOne(id);
}Options: isArray, statusCode, description
@ApiPaginatedSafeResponse(Model)
Generates paginated Swagger schema with meta.pagination.
@Get()
@Paginated({ maxLimit: 100 })
@ApiPaginatedSafeResponse(UserDto)
async findAll(@Query('page') page = 1, @Query('limit') limit = 20) {
const [items, total] = await this.usersService.findAndCount({
skip: (page - 1) * limit,
take: limit,
});
return { data: items, total, page, limit };
}Response:
{
"success": true,
"statusCode": 200,
"data": [{ "id": 1 }, { "id": 2 }],
"meta": {
"pagination": {
"type": "offset",
"page": 1,
"limit": 20,
"total": 100,
"totalPages": 5,
"hasNext": true,
"hasPrev": false
}
}
}@ApiCursorPaginatedSafeResponse(Model)
Generates cursor-paginated Swagger schema with meta.pagination.
@Get()
@CursorPaginated()
@ApiCursorPaginatedSafeResponse(UserDto)
async findAll(@Query('cursor') cursor?: string, @Query('limit') limit = 20) {
const { items, nextCursor, hasMore, totalCount } =
await this.usersService.findWithCursor({ cursor, limit });
return { data: items, nextCursor, hasMore, limit, totalCount };
}Response:
{
"success": true,
"statusCode": 200,
"data": [{ "id": 1 }, { "id": 2 }],
"meta": {
"pagination": {
"type": "cursor",
"nextCursor": "eyJpZCI6MTAwfQ==",
"previousCursor": null,
"hasMore": true,
"limit": 20,
"totalCount": 150
}
}
}The handler must return a CursorPaginatedResult<T>:
interface CursorPaginatedResult<T> {
data: T[];
nextCursor: string | null;
previousCursor?: string | null; // defaults to null
hasMore: boolean;
limit: number;
totalCount?: number; // optional
}@ApiSafeErrorResponse(status, options?)
Documents an error response in Swagger with the SafeErrorResponseDto envelope. Error codes are auto-resolved from DEFAULT_ERROR_CODE_MAP.
@Get(':id')
@ApiSafeResponse(UserDto)
@ApiSafeErrorResponse(404)
@ApiSafeErrorResponse(400, {
code: 'VALIDATION_ERROR',
message: 'Input validation failed',
details: ['email must be an email'],
})
async findOne(@Param('id') id: string) {
return this.usersService.findOne(id);
}Options: description, code, message, details
Note: This decorator generates build-time Swagger metadata only. If you use a custom
errorCodeMapperat runtime, the decorator cannot reflect those dynamic codes automatically — pass thecodeoption explicitly to match your runtime mapping.
The details field schema is automatically inferred from the example value:
- Array →
{ type: 'array', items: { type } }(item type inferred from first element: object, number, or string) object→{ type: 'object' }string→{ type: 'string' }
@ApiSafeErrorResponses(configs)
Documents multiple error responses at once. Accepts an array of status codes or config objects.
@Post()
@ApiSafeResponse(UserDto, { statusCode: 201 })
@ApiSafeErrorResponses([400, 401, 409])
async create(@Body() dto: CreateUserDto) {
return this.usersService.create(dto);
}
// With mixed configuration
@Post('register')
@ApiSafeErrorResponses([
400,
{ status: 401, description: 'Token expired' },
{ status: 409, code: 'EMAIL_TAKEN', message: 'Email already registered' },
])
async register(@Body() dto: RegisterDto) {
return this.authService.register(dto);
}@RawResponse()
Skips response wrapping for this route.
@Get('health')
@RawResponse()
healthCheck() {
return { status: 'ok' };
}Note: If your controller returns a
BufferorStream, use@RawResponse()to skip response wrapping. Without it, binary data will be serialized as{ type: 'Buffer', data: [...] }, which corrupts the original content.
@ResponseMessage(message)
Adds a custom message to meta.message.
@Post()
@ResponseMessage('User created successfully')
create(@Body() dto: CreateUserDto) {
return this.usersService.create(dto);
}@SafeResponse(options?)
Applies standard wrapping + basic Swagger schema. Options: description, statusCode.
@Paginated(options?)
Enables offset pagination metadata auto-calculation. Options: maxLimit, links.
@Get()
@Paginated({ maxLimit: 100, links: true }) // HATEOAS navigation links
findAll() { ... }When links: true, the response includes navigation links in meta.pagination.links:
{
"meta": {
"pagination": {
"type": "offset",
"page": 2, "limit": 20, "total": 100, "totalPages": 5,
"links": {
"self": "/api/users?page=2&limit=20",
"first": "/api/users?page=1&limit=20",
"prev": "/api/users?page=1&limit=20",
"next": "/api/users?page=3&limit=20",
"last": "/api/users?page=5&limit=20"
}
}
}
}@CursorPaginated(options?)
Enables cursor-based pagination metadata auto-calculation. Options: maxLimit, links.
@ProblemType(typeUri: string)
Set the RFC 9457 problem type URI for a specific route. Used when problemDetails is enabled.
@Get(':id')
@ProblemType('https://api.example.com/problems/user-not-found')
findOne(@Param('id') id: string) { ... }@SuccessCode(code: string)
Set a custom success code for this route (method-level only). Takes priority over successCodeMapper module option.
@Get()
@SuccessCode('FETCH_SUCCESS')
findAll() {
return this.usersService.findAll();
}Response:
{
"success": true,
"statusCode": 200,
"code": "FETCH_SUCCESS",
"data": [...]
}@SortMeta() / @FilterMeta()
Include sorting and filtering metadata in the response. The handler must return sort and/or filters fields alongside the data.
@Get()
@Paginated()
@SortMeta()
@FilterMeta()
@ApiPaginatedSafeResponse(UserDto)
async findAll(
@Query('sortBy') sortBy = 'createdAt',
@Query('order') order: 'asc' | 'desc' = 'desc',
@Query('status') status?: string,
) {
const [items, total] = await this.usersService.findAndCount({ sortBy, order, status });
return {
data: items, total, page: 1, limit: 20,
sort: { field: sortBy, order },
filters: { ...(status && { status }) },
};
}Response:
{
"success": true,
"statusCode": 200,
"data": [...],
"meta": {
"pagination": { "type": "offset", "page": 1, "limit": 20, "total": 100, "totalPages": 5, "hasNext": true, "hasPrev": false },
"sort": { "field": "createdAt", "order": "desc" },
"filters": { "status": "active" }
}
}@Deprecated(options?)
Mark an endpoint as deprecated. Sets deprecation headers and Swagger deprecated: true.
Options: since, sunset, message, link
@SkipGlobalErrors()
Excludes a route from applyGlobalErrors() global error injection. Useful for routes with custom error schemas.
Composite Decorators
Combine Swagger documentation, runtime behavior, and error responses into a single decorator.
@SafeEndpoint(Model, options?)
@Get()
@SafeEndpoint(UserDto, {
description: 'List users',
errors: [401, { status: 404, code: 'USER_NOT_FOUND' }],
message: 'Users fetched',
})
findAll() { ... }Equivalent to stacking @ApiSafeResponse() + @ResponseMessage() + @ApiSafeErrorResponses().
Options: statusCode, isArray, description, sort, filter, message, code, errors, deprecated, problemDetails
@SafePaginatedEndpoint(Model, options?)
@Get()
@SafePaginatedEndpoint(UserDto, {
maxLimit: 100,
links: true,
errors: [401],
})
findAll() { ... }Equivalent to @ApiPaginatedSafeResponse() + @Paginated() + @ApiSafeErrorResponses().
Options: maxLimit, links, sort, filter, description, message, code, errors, deprecated, problemDetails
@SafeCursorPaginatedEndpoint(Model, options?)
@Get()
@SafeCursorPaginatedEndpoint(UserDto, {
maxLimit: 50,
errors: [401],
})
findAll() { ... }Equivalent to @ApiCursorPaginatedSafeResponse() + @CursorPaginated() + @ApiSafeErrorResponses().
Options: maxLimit, links, sort, filter, description, message, code, errors, deprecated, problemDetails
problemDetailsoption: Whentrue, error responses useapplication/problem+jsonschema in Swagger. Must match the module-levelproblemDetailssetting — this option only controls Swagger documentation, not runtime behavior.
Global Error Swagger Documentation
Inject common error responses (e.g., 401, 403, 500) into all OpenAPI operations at once, instead of decorating every route.
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { applyGlobalErrors, SafeResponseModule } from '@nestarc/safe-response';
// 1. Register with swagger option
SafeResponseModule.register({
swagger: { globalErrors: [401, 403, { status: 500, message: 'Unexpected error' }] },
});
// 2. Apply after document creation
const config = new DocumentBuilder().setTitle('My API').build();
const document = SwaggerModule.createDocument(app, config);
applyGlobalErrors(document, moduleOptions); // mutates document in-place
SwaggerModule.setup('api', app, document);Use @SkipGlobalErrors() on routes that should not receive global error schemas.
Request ID
Enable request ID tracking to include a unique identifier in every response — essential for production debugging and distributed tracing.
SafeResponseModule.register({
requestId: true, // auto-generate UUID v4, read from X-Request-Id header
})Behavior:
- Checks incoming
X-Request-Idheader — reuses the value if present - If no header, generates a UUID v4 via
crypto.randomUUID()(no external dependencies) - Includes
requestIdfield in both success and error response bodies - Sets
X-Request-Idresponse header for downstream tracking
Custom Options
SafeResponseModule.register({
requestId: {
headerName: 'X-Correlation-Id', // custom header name (default: 'X-Request-Id')
generator: () => `req-${Date.now()}`, // custom ID generator
},
})Response Time
Track handler execution time in every response — useful for performance monitoring and SLA tracking.
SafeResponseModule.register({
responseTime: true, // adds meta.responseTime (milliseconds) to all responses
})Response:
{
"success": true,
"statusCode": 200,
"data": { "..." },
"meta": { "responseTime": 42 }
}Uses performance.now() for high-resolution timing. Included in both success and error responses (when the interceptor ran before the error).
RFC 9457 Problem Details
Enable RFC 9457 standard error responses — used by Stripe, GitHub, and Cloudflare.
SafeResponseModule.register({
problemDetails: true, // or { baseUrl: 'https://api.example.com/problems' }
})Error response:
{
"type": "https://api.example.com/problems/not-found",
"title": "Not Found",
"status": 404,
"detail": "User with ID 123 not found",
"instance": "/api/users/123",
"code": "NOT_FOUND",
"requestId": "abc-123"
}- Sets
Content-Type: application/problem+jsonautomatically - Uses
@ProblemType(uri)decorator for per-route type URIs, or auto-generates frombaseUrl+ error code - Preserves extension members:
code,requestId,details(validation errors),meta.responseTime - Success responses are not affected — only error responses change format
- Use
@ApiSafeProblemResponse(status)for Swagger documentation
Frontend Client Types
@nestarc/safe-response/client provides zero-dependency TypeScript types and type guards for frontend consumers. No NestJS, Swagger, or reflect-metadata required.
import type { SafeAnyResponse } from '@nestarc/safe-response/client';
import {
isSuccess, isError, isPaginated, isOffsetPagination, isCursorPagination,
isProblemDetailsResponse, hasResponseTime, hasSort, hasFilters,
isDeprecated, hasRateLimit,
} from '@nestarc/safe-response/client';
// SafeAnyResponse includes success, error, and Problem Details responses
const res: SafeAnyResponse<User[]> = await fetch('/api/users').then(r => r.json());
if (isSuccess(res)) {
console.log(res.data); // User[]
if (isPaginated(res.meta) && isOffsetPagination(res.meta.pagination)) {
console.log(`Page ${res.meta.pagination.page} of ${res.meta.pagination.totalPages}`);
}
}
if (isError(res)) {
console.error(res.error.code, res.error.message);
}
// RFC 9457 Problem Details (different shape from standard errors)
if (isProblemDetailsResponse(res)) {
console.error(res.type, res.detail, res.instance);
}Internationalization (i18n)
Translate error messages and @ResponseMessage() values automatically via nestjs-i18n or a custom adapter.
// Auto-detect nestjs-i18n (must be installed as peer dependency)
SafeResponseModule.register({ i18n: true });
// Or provide a custom adapter
SafeResponseModule.register({
i18n: {
translate: (key, opts) => myTranslator.t(key, opts?.lang),
resolveLanguage: (request) => request.headers['accept-language'] ?? 'en',
},
});Custom adapters are exception-safe — if translate() or resolveLanguage() throws, the original untranslated message is used.
Context Injection (nestjs-cls)
Inject request-scoped context values (e.g., traceId, correlationId) into every response's meta field. Requires nestjs-cls.
SafeResponseModule.register({
context: {
// Map CLS store keys to response meta fields
fields: { traceId: 'traceId', correlationId: 'correlationId' },
},
});
// Or use a custom resolver for full control
SafeResponseModule.register({
context: {
resolver: (clsService) => ({
traceId: clsService.get('traceId'),
region: clsService.get('region'),
}),
},
});Response:
{
"success": true,
"statusCode": 200,
"data": { "..." },
"meta": { "traceId": "abc-123", "correlationId": "req-456" }
}API Deprecation
Mark endpoints as deprecated with standard HTTP headers and response metadata.
@Get('v1/users')
@Deprecated({
since: '2026-01-01', // RFC 9745 Deprecation header
sunset: '2026-12-31', // RFC 8594 Sunset header
message: 'Use /v2/users instead',
link: '/v2/users', // Link header with rel="successor-version"
})
findAll() { ... }Headers set automatically:
Deprecation: @1735689600(ortrueif nosincedate)Sunset: Tue, 31 Dec 2026 00:00:00 GMTLink: </v2/users>; rel="successor-version"
Response includes meta.deprecation:
{
"success": true,
"data": [...],
"meta": {
"deprecation": {
"deprecated": true,
"since": "2026-01-01T00:00:00.000Z",
"sunset": "2026-12-31T00:00:00.000Z",
"message": "Use /v2/users instead",
"link": "/v2/users"
}
}
}Swagger automatically shows the endpoint as deprecated. Works with both success and error responses.
Note: If a Guard throws before the interceptor runs (e.g.,
AuthGuardreturns 401), the error response will not include deprecation headers. This is the same limitation as@ProblemType().
Rate Limit Metadata
Mirror rate limit response headers into the response body for frontend consumption.
SafeResponseModule.register({
rateLimit: true, // reads X-RateLimit-* headers
})Works with any rate limiter that sets standard headers (@nestjs/throttler, API gateways, custom middleware):
{
"success": true,
"data": [...],
"meta": {
"rateLimit": {
"limit": 100,
"remaining": 87,
"reset": 1712025600
}
}
}All three headers (Limit, Remaining, Reset) must be present; partial data is suppressed. Available in both success and error responses (including 429 Too Many Requests).
Custom Header Prefix
SafeResponseModule.register({
rateLimit: { headerPrefix: 'RateLimit' }, // reads RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset
})Module Options
SafeResponseModule.register({
timestamp: true, // include timestamp field (default: true)
path: true, // include path field (default: true)
requestId: true, // include request ID tracking (default: false)
responseTime: true, // include response time in meta (default: false)
problemDetails: true, // RFC 9457 error format (default: false)
errorCodeMapper: (exception) => {
if (exception instanceof TokenExpiredError) return 'TOKEN_EXPIRED';
return undefined; // fall back to default mapping
},
dateFormatter: () => new Date().toISOString(), // custom date format
})Async Registration
SafeResponseModule.registerAsync({
imports: [ConfigModule],
useFactory: (config: ConfigService) => ({
timestamp: config.get('RESPONSE_TIMESTAMP', true),
}),
inject: [ConfigService],
})Additional Options
| Option | Type | Default | Description |
|---|---|---|---|
requestId | boolean | RequestIdOptions | undefined | Enable request ID tracking in responses |
responseTime | boolean | false | Include response time (ms) in meta.responseTime |
problemDetails | boolean | ProblemDetailsOptions | false | Enable RFC 9457 Problem Details error format |
successCodeMapper | (statusCode: number) => string | undefined | undefined | Maps HTTP status codes to success code strings |
transformResponse | (data: unknown) => unknown | undefined | Transform data before response wrapping (sync only) |
swagger | SwaggerOptions | undefined | Swagger documentation options (e.g., globalErrors) |
context | ContextOptions | undefined | Inject CLS store values (traceId, etc.) into response meta. Requires nestjs-cls. |
rateLimit | boolean | RateLimitOptions | undefined | Mirror rate limit response headers into meta.rateLimit |
i18n | boolean | I18nAdapter | undefined | Enable i18n for error/success messages. true auto-detects nestjs-i18n, or pass a custom adapter. |
errorCodes | Record<number, string> | undefined | Declarative error code map merged on top of DEFAULT_ERROR_CODE_MAP |
suppressWarnings | boolean | false | Suppress shape-mismatch warnings for @Paginated, @CursorPaginated, @SortMeta, @FilterMeta |
Success Code Mapping
SafeResponseModule.register({
successCodeMapper: (statusCode) => {
const map: Record<number, string> = { 200: 'OK', 201: 'CREATED' };
return map[statusCode];
},
})Response Transformation
SafeResponseModule.register({
transformResponse: (data) => {
if (data && typeof data === 'object' && 'password' in data) {
const { password, ...rest } = data as Record<string, unknown>;
return rest;
}
return data;
},
})@Exclude() Integration
Using with class-transformer
@nestarc/safe-response works with NestJS's ClassSerializerInterceptor when registered in the correct order. SafeResponseModule must be imported before ClassSerializerInterceptor is registered, so that serialization runs first and response wrapping runs second.
import { ClassSerializerInterceptor, Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
@Module({
imports: [SafeResponseModule.register()],
providers: [
{ provide: APP_INTERCEPTOR, useClass: ClassSerializerInterceptor },
],
})
export class AppModule {}Default Error Code Mapping
| HTTP Status | Error Code |
|---|---|
| 400 | BAD_REQUEST |
| 401 | UNAUTHORIZED |
| 403 | FORBIDDEN |
| 404 | NOT_FOUND |
| 409 | CONFLICT |
| 422 | UNPROCESSABLE_ENTITY |
| 429 | TOO_MANY_REQUESTS |
| 500 | INTERNAL_SERVER_ERROR |
Override with errorCodeMapper option.
Declarative Error Codes
For simple status-to-code mappings without a mapper function:
SafeResponseModule.register({
errorCodes: {
404: 'RESOURCE_NOT_FOUND',
409: 'DUPLICATE_ENTRY',
},
})Resolution order: errorCodeMapper > errorCodes > DEFAULT_ERROR_CODE_MAP > 'INTERNAL_SERVER_ERROR'
Utility Functions
import { lookupErrorCode, lookupProblemTitle } from '@nestarc/safe-response';
lookupErrorCode(404); // 'NOT_FOUND'
lookupProblemTitle(404); // 'Not Found'Shape-mismatch Warnings
When @Paginated(), @CursorPaginated(), @SortMeta(), or @FilterMeta() are applied but the handler returns data that doesn't match the expected shape, a Logger.warn() is emitted with the route and expected shape.
SafeResponseModule.register({
suppressWarnings: true, // silence shape-mismatch warnings
})Testing & Reliability
This library is built with multiple layers of verification to ensure production reliability.
Test Suite
| Category | Count | What it covers |
|---|---|---|
| Unit tests | 427 | Interceptor, Exception Filter, Module DI, Decorators, Client Type Guards, i18n Adapter, Global Errors, Shared Utilities |
| E2E tests (Express) | 51 | Full HTTP request/response cycle including composite decorators and declarative error codes |
| E2E tests (Fastify) | 51 | Full platform parity with Express — all features verified on Fastify |
| E2E tests (Swagger) | 41 | OpenAPI schema output verification including Problem Details and Global Errors |
| Type tests | 84 | Public API type signature via tsd including client type guards and composite decorator options |
| Snapshots | 2 | Swagger components/schemas + paths regression detection |
npm test # unit tests
npm run test:e2e # E2E tests (Express + Fastify + Swagger)
npm run test:cov # unit tests with coverage (90%+ enforced)
npm run test:types # public API type verification
npm run bench # performance benchmarkCI Pipeline
Every push runs the full matrix on GitHub Actions:
Node 18/20/22 × NestJS 10/11 × @nestjs/swagger 8/11
→ build → test:cov (threshold enforced) → test:e2e → test:typesCoverage Threshold
Enforced in CI — the build fails if coverage drops below:
| Metric | Threshold |
|---|---|
| Lines | 90% |
| Statements | 90% |
| Branches | 80% |
| Functions | 60% |
OpenAPI Schema Validation
Generated Swagger documents are validated against the OpenAPI spec using @apidevtools/swagger-parser in E2E tests. If the library produces an invalid OpenAPI schema, the tests fail.
API Contract Snapshots
Swagger components/schemas and paths are snapshot-tested. Any unintended schema change breaks the test — update snapshots with npx jest --config test/jest-e2e.json -u when schema changes are intentional.
Performance
Example benchmark results (npm run bench, 500 iterations, single run on one machine — your results will vary):
| Path | Raw NestJS | With @nestarc/safe-response | Overhead |
|---|---|---|---|
| Success (200) | ~0.5ms | ~0.6ms | < 0.1ms |
| Error (404) | ~0.7ms | ~0.6ms | negligible |
Response wrapping overhead is sub-millisecond. The benchmark uses supertest in a single-process setup, so absolute numbers fluctuate across environments. Run npm run bench in your own environment for representative results.
Compatibility
| Dependency | Version |
|---|---|
| NestJS | v10, v11 |
| Platform | Express, Fastify |
| @nestjs/swagger | v8, v11 |
| Node.js | >= 18 |
| RxJS | v7 |
License
MIT