Skip to content

@nestarc/safe-response

CInpm versionnpm downloadslicensenodeTypeScript

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 response meta
  • Request ID tracking — opt-in requestId field 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 SwaggerapplyGlobalErrors() injects common error responses (401, 403, 500) into all OpenAPI operations
  • Frontend client types@nestarc/safe-response/client provides 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/8594 Deprecation/Sunset headers, Swagger deprecated: true, and response meta.deprecation
  • Rate limit metadata — opt-in meta.rateLimit mirroring of X-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 details array 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 codeserrorCodes option 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 Moduleregister() / registerAsync() with full DI support

Installation

bash
npm install @nestarc/safe-response

Peer Dependencies

bash
npm install @nestjs/common @nestjs/core @nestjs/swagger rxjs reflect-metadata

Quick Start

typescript
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:

typescript
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

json
{
  "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"
}

requestId is only present when requestId option is enabled. See Request ID.

Error

json
{
  "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.

typescript
@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.

typescript
@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:

json
{
  "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.

typescript
@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:

json
{
  "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>:

typescript
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.

typescript
@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 errorCodeMapper at runtime, the decorator cannot reflect those dynamic codes automatically — pass the code option 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.

typescript
@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.

typescript
@Get('health')
@RawResponse()
healthCheck() {
  return { status: 'ok' };
}

Note: If your controller returns a Buffer or Stream, 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.

typescript
@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.

typescript
@Get()
@Paginated({ maxLimit: 100, links: true })  // HATEOAS navigation links
findAll() { ... }

When links: true, the response includes navigation links in meta.pagination.links:

json
{
  "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.

typescript
@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.

typescript
@Get()
@SuccessCode('FETCH_SUCCESS')
findAll() {
  return this.usersService.findAll();
}

Response:

json
{
  "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.

typescript
@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:

json
{
  "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?)

typescript
@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?)

typescript
@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?)

typescript
@Get()
@SafeCursorPaginatedEndpoint(UserDto, {
  maxLimit: 50,
  errors: [401],
})
findAll() { ... }

Equivalent to @ApiCursorPaginatedSafeResponse() + @CursorPaginated() + @ApiSafeErrorResponses().

Options: maxLimit, links, sort, filter, description, message, code, errors, deprecated, problemDetails

problemDetails option: When true, error responses use application/problem+json schema in Swagger. Must match the module-level problemDetails setting — 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.

typescript
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.

typescript
SafeResponseModule.register({
  requestId: true, // auto-generate UUID v4, read from X-Request-Id header
})

Behavior:

  1. Checks incoming X-Request-Id header — reuses the value if present
  2. If no header, generates a UUID v4 via crypto.randomUUID() (no external dependencies)
  3. Includes requestId field in both success and error response bodies
  4. Sets X-Request-Id response header for downstream tracking

Custom Options

typescript
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.

typescript
SafeResponseModule.register({
  responseTime: true,  // adds meta.responseTime (milliseconds) to all responses
})

Response:

json
{
  "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.

typescript
SafeResponseModule.register({
  problemDetails: true,  // or { baseUrl: 'https://api.example.com/problems' }
})

Error response:

json
{
  "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+json automatically
  • Uses @ProblemType(uri) decorator for per-route type URIs, or auto-generates from baseUrl + 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.

typescript
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.

typescript
// 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.

typescript
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:

json
{
  "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.

typescript
@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 (or true if no since date)
  • Sunset: Tue, 31 Dec 2026 00:00:00 GMT
  • Link: </v2/users>; rel="successor-version"

Response includes meta.deprecation:

json
{
  "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., AuthGuard returns 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.

typescript
SafeResponseModule.register({
  rateLimit: true,  // reads X-RateLimit-* headers
})

Works with any rate limiter that sets standard headers (@nestjs/throttler, API gateways, custom middleware):

json
{
  "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

typescript
SafeResponseModule.register({
  rateLimit: { headerPrefix: 'RateLimit' },  // reads RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset
})

Module Options

typescript
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

typescript
SafeResponseModule.registerAsync({
  imports: [ConfigModule],
  useFactory: (config: ConfigService) => ({
    timestamp: config.get('RESPONSE_TIMESTAMP', true),
  }),
  inject: [ConfigService],
})

Additional Options

OptionTypeDefaultDescription
requestIdboolean | RequestIdOptionsundefinedEnable request ID tracking in responses
responseTimebooleanfalseInclude response time (ms) in meta.responseTime
problemDetailsboolean | ProblemDetailsOptionsfalseEnable RFC 9457 Problem Details error format
successCodeMapper(statusCode: number) => string | undefinedundefinedMaps HTTP status codes to success code strings
transformResponse(data: unknown) => unknownundefinedTransform data before response wrapping (sync only)
swaggerSwaggerOptionsundefinedSwagger documentation options (e.g., globalErrors)
contextContextOptionsundefinedInject CLS store values (traceId, etc.) into response meta. Requires nestjs-cls.
rateLimitboolean | RateLimitOptionsundefinedMirror rate limit response headers into meta.rateLimit
i18nboolean | I18nAdapterundefinedEnable i18n for error/success messages. true auto-detects nestjs-i18n, or pass a custom adapter.
errorCodesRecord<number, string>undefinedDeclarative error code map merged on top of DEFAULT_ERROR_CODE_MAP
suppressWarningsbooleanfalseSuppress shape-mismatch warnings for @Paginated, @CursorPaginated, @SortMeta, @FilterMeta

Success Code Mapping

typescript
SafeResponseModule.register({
  successCodeMapper: (statusCode) => {
    const map: Record<number, string> = { 200: 'OK', 201: 'CREATED' };
    return map[statusCode];
  },
})

Response Transformation

typescript
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.

typescript
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 StatusError Code
400BAD_REQUEST
401UNAUTHORIZED
403FORBIDDEN
404NOT_FOUND
409CONFLICT
422UNPROCESSABLE_ENTITY
429TOO_MANY_REQUESTS
500INTERNAL_SERVER_ERROR

Override with errorCodeMapper option.

Declarative Error Codes

For simple status-to-code mappings without a mapper function:

typescript
SafeResponseModule.register({
  errorCodes: {
    404: 'RESOURCE_NOT_FOUND',
    409: 'DUPLICATE_ENTRY',
  },
})

Resolution order: errorCodeMapper > errorCodes > DEFAULT_ERROR_CODE_MAP > 'INTERNAL_SERVER_ERROR'

Utility Functions

typescript
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.

typescript
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

CategoryCountWhat it covers
Unit tests427Interceptor, Exception Filter, Module DI, Decorators, Client Type Guards, i18n Adapter, Global Errors, Shared Utilities
E2E tests (Express)51Full HTTP request/response cycle including composite decorators and declarative error codes
E2E tests (Fastify)51Full platform parity with Express — all features verified on Fastify
E2E tests (Swagger)41OpenAPI schema output verification including Problem Details and Global Errors
Type tests84Public API type signature via tsd including client type guards and composite decorator options
Snapshots2Swagger components/schemas + paths regression detection
bash
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 benchmark

CI 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:types

Coverage Threshold

Enforced in CI — the build fails if coverage drops below:

MetricThreshold
Lines90%
Statements90%
Branches80%
Functions60%

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):

PathRaw NestJSWith @nestarc/safe-responseOverhead
Success (200)~0.5ms~0.6ms< 0.1ms
Error (404)~0.7ms~0.6msnegligible

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

DependencyVersion
NestJSv10, v11
PlatformExpress, Fastify
@nestjs/swaggerv8, v11
Node.js>= 18
RxJSv7

License

MIT

Released under the MIT License.