npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ciphercross/nestjs-contracts

v1.0.2

Published

Standardized API contracts for NestJS applications

Readme

@ciphercross/nestjs-contracts

Standardized API contracts for NestJS applications. Provides consistent response formats, request ID tracking, pagination metadata, and Swagger integration.

Table of Contents

Installation

npm install @ciphercross/nestjs-contracts

Peer Dependencies:

  • @nestjs/common ^10.0.0
  • @nestjs/swagger ^7.0.0

Quick Start

Method 1: Using ContractsModule (Recommended)

The easiest way - use ContractsModule.forRoot() which automatically configures everything:

In app.module.ts:

import { Module } from '@nestjs/common';
import { ContractsModule } from '@ciphercross/nestjs-contracts';

@Module({
  imports: [
    ContractsModule.forRoot({
      requestIdHeader: 'x-request-id',
      defaultPagination: { limit: 20, maxLimit: 100, maxPage: 1000 },
      enableRequestContext: true, // Optional: for accessing requestId from services
      extractUserId: (req) => req.user?.id, // Optional: custom userId extraction
    }),
    // ... other modules
  ],
})
export class AppModule {}

Benefits:

  • Automatic registration of middleware and interceptor
  • Centralized configuration
  • Less human error
  • Access to configuration via ContractsConfigService

Method 2: Manual Setup (Legacy)

If you need more control, you can configure manually:

In main.ts:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { requestIdMiddleware, ResponseWrapInterceptor } from '@ciphercross/nestjs-contracts';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // Add request ID middleware (must be first)
  app.use(requestIdMiddleware);

  // Wrap all successful responses
  app.useGlobalInterceptors(new ResponseWrapInterceptor());

  await app.listen(3000);
}
bootstrap();

Important: Middleware must be added before interceptors.

2. Use in Controllers

Simple Response

import { Controller, Get } from '@nestjs/common';
import { ApiOkContract } from '@ciphercross/nestjs-contracts';
import { UserDto } from './user.dto';

@Controller('users')
export class UsersController {
  @Get(':id')
  @ApiOkContract(UserDto)
  async findOne(@Param('id') id: string) {
    return this.usersService.findOne(id); // Returns UserDto, wrapped automatically
  }
}

Paginated Response (Automatic)

import { Controller, Get, Query } from '@nestjs/common';
import { ApiOkPaginatedContract } from '@ciphercross/nestjs-contracts';
import { UserDto } from './user.dto';

@Controller('users')
export class UsersController {
  @Get()
  @ApiOkPaginatedContract(UserDto) // Explicitly indicates pagination
  async list(@Query() query: { page?: number; limit?: number }) {
    // Just return { items, total, page?, limit? } - interceptor handles it automatically
    return this.usersService.findAll({
      page: query.page ?? 1,
      limit: query.limit ?? 20,
    });
  }
}

Paginated Response (Manual with @Req())

import { Controller, Get, Query, Req } from '@nestjs/common';
import { ApiOkContract, buildPaginationMeta } from '@ciphercross/nestjs-contracts';
import { UserDto } from './user.dto';

@Controller('users')
export class UsersController {
  @Get()
  @ApiOkContract(UserDto, true) // true = array response
  async list(@Query() query: ListUsersDto, @Req() req: any) {
    const { items, total } = await this.usersService.findAll(query);
    
    // Set pagination meta (will be included in response)
    req.paginationMeta = buildPaginationMeta({
      page: query.page ?? 1,
      limit: query.limit ?? 20,
      total,
    });

    return items; // Just return data, interceptor wraps it
  }
}

Skip Wrapping (for file downloads, streaming, etc.)

import { Controller, Get, Res } from '@nestjs/common';
import { SkipContract } from '@ciphercross/nestjs-contracts';
import { Response } from 'express';

@Controller('files')
export class FilesController {
  @SkipContract()
  @Get('download/:id')
  downloadFile(@Param('id') id: string, @Res() res: Response) {
    // Response won't be wrapped
    return res.sendFile(`/path/to/file-${id}.pdf`);
  }
}

Features

✅ Standardized Response Format

All successful responses follow this structure:

{
  "success": true,
  "data": { ... },
  "meta": {
    "timestamp": "2026-01-12T13:00:00.000Z",
    "requestId": "req_abc123",
    "pagination": {
      "page": 1,
      "limit": 20,
      "total": 145,
      "totalPages": 8
    }
  }
}

✅ Request ID Tracking

  • Reads x-request-id header if present
  • Generates unique ID if missing: req_<24 hex chars>
  • Sets in request object for use by interceptors/filters
  • Returns in response headers for debugging

✅ Automatic Response Wrapping

Controllers return only data - interceptor automatically wraps with:

  • success: true
  • data: <your data>
  • meta: { timestamp, requestId, pagination? }

Automatic Pagination Detection:

  • If controller returns { items, total, page?, limit? }, pagination is handled automatically
  • No need for separate PaginatedResponseInterceptor or @Req() injection
  • Values are automatically clamped to maxLimit and maxPage from configuration

✅ Swagger Integration

Decorators automatically generate correct Swagger schemas:

@ApiOkContract(UserDto)              // Single item
@ApiOkContract(UserDto, true)        // Array of items (pagination optional)
@ApiOkPaginatedContract(UserDto)      // Paginated list (pagination always present)

// Error responses
@ApiBadRequestContract()
@ApiUnauthorizedContract()
@ApiForbiddenContract()
@ApiNotFoundContract()
@ApiConflictContract()
@ApiInternalErrorContract()

✅ Type Exports & Unwrap Helpers for Frontend

All types are exported and can be used in frontend TypeScript projects:

import type { SuccessResponse, ErrorResponse, PaginationMeta } from '@ciphercross/nestjs-contracts';
import {
  unwrapData,
  unwrapOrThrow,
  getPagination,
  getRequestId,
  getErrors,
  isSuccessResponse,
  isErrorResponse,
  getErrorMessage,
} from '@ciphercross/nestjs-contracts';

// Option 1: Using unwrapOrThrow (throws Error if response is not successful)
// unwrapOrThrow throws Error with message formatted via getErrorMessage()
const response = await fetch('/api/users');
const data = await response.json();
try {
  const users = unwrapOrThrow(data);
  const pagination = getPagination({ ...data, data: users });
} catch (error) {
  console.error(error.message); // message formatted via getErrorMessage()
}

// Option 2: Using if-else checks
if (isSuccessResponse(data)) {
  const users = unwrapData(data);
  const pagination = getPagination(data);
} else {
  const message = getErrorMessage(data);
  const errors = getErrors(data);
}

✅ RequestContext (AsyncLocalStorage)

Access requestId and userId from anywhere in your application (services, repositories) without passing req:

import { RequestContext } from '@ciphercross/nestjs-contracts';

@Injectable()
export class UsersService {
  async findAll() {
    const requestId = RequestContext.getRequestId();
    const userId = RequestContext.getUserId();
    this.logger.log(`[${requestId}] Processing for user ${userId}`);
  }
}

API Reference

Types

SuccessResponse<T>

Type for successful API responses.

type SuccessResponse<T> = {
  success: true;
  data: T;
  meta: ResponseMeta;
};

ErrorResponse

Type for error responses (used by @ciphercross/nestjs-errors).

type ErrorResponse = {
  success: false;
  message: string;
  errors: ApiError[];
  meta: ResponseMeta;
};

ResponseMeta

Metadata included in all responses.

type ResponseMeta = {
  timestamp: string;  // ISO string
  requestId: string;
  pagination?: PaginationMeta;
};

PaginationMeta

Pagination information.

type PaginationMeta = {
  page: number;
  limit: number;
  total: number;
  totalPages: number;
  /**
   * Optional clamp information for debugging
   * Indicates if values were automatically clamped to limits
   */
  clamped?: {
    page?: boolean;
    limit?: boolean;
  };
};

PaginatedResult<T>

Official type for paginated results that ResponseWrapInterceptor recognizes.

export type PaginatedResult<T> = {
  items: T[];
  total: number;
  page?: number;
  limit?: number;
};

Important: This is the only format that the interceptor automatically recognizes and processes. If your service returns an object with this structure, pagination will be handled automatically.

Priority for page/limit:

  1. result.page / result.limit (if service returned)
  2. req.query.page / req.query.limit (if present in query params)
  3. config.defaultPagination.limit + page=1 (default)

ApiError

Error detail structure.

type ApiError = {
  code: string;
  message: string;
  field?: string;
  details?: unknown;
};

Enums

SortOrder

enum SortOrder {
  ASC = 'asc',
  DESC = 'desc',
}

TimeFilter

enum TimeFilter {
  ALL = 'ALL',
  TODAY = 'TODAY',
  LAST_7_DAYS = 'LAST_7_DAYS',
  LAST_30_DAYS = 'LAST_30_DAYS',
  EARLIER = 'EARLIER',
}

StatusFilter

enum StatusFilter {
  ALL = 'ALL',
  ACTIVE = 'ACTIVE',
  INACTIVE = 'INACTIVE',
}

Middleware

requestIdMiddleware

Extracts or generates request ID from x-request-id header.

Usage:

app.use(requestIdMiddleware);

Behavior:

  • Reads x-request-id header (max 200 chars)
  • Generates new ID if missing: req_<24 hex chars>
  • Sets req.requestId for use by interceptors/filters
  • Returns ID in response headers

Interceptors

ResponseWrapInterceptor

Automatically wraps controller responses with success format.

Usage:

app.useGlobalInterceptors(new ResponseWrapInterceptor());

Behavior:

  • Wraps response with { success: true, data, meta }
  • Adds timestamp (ISO format)
  • Adds requestId from middleware
  • Automatically detects pagination - if controller returns { items, total, page?, limit? } (type PaginatedResult<T>), handles it automatically
  • Includes pagination meta if req.paginationMeta is set or detected automatically
  • Skips wrapping for endpoints with @SkipContract() decorator
  • Skips wrapping for file downloads, streaming, redirects (auto-detected)

Skip wrapping decision order:

  1. @SkipContract() decorator on handler or class
  2. skipWrappingFor(ctx) from config (if configured)
  3. res.headersSent === true (headers already sent)
  4. content-type doesn't contain 'application/json'
  5. 3xx redirect status codes
  6. Otherwise - wraps response

Utilities

buildPaginationMeta(params)

Builds pagination metadata from parameters. Automatically clamps values to limits (does not throw).

Parameters:

  • page: number - Current page (1-indexed, min: 1, max: 1000 by default)
  • limit: number - Items per page (min: 1, max: 100 by default)
  • total: number - Total items (min: 0)
  • maxLimit?: number - Optional: override max limit (defaults to 100)
  • maxPage?: number - Optional: override max page (defaults to 1000)

Important: If maxLimit / maxPage are not passed to buildPaginationMeta, values from ContractsModule.forRoot() are used.

Returns: PaginationMeta (may include clamped flags for debugging)

Example:

const meta = buildPaginationMeta({
  page: 1,
  limit: 20,
  total: 145,
  maxLimit: 100, // Optional override
  maxPage: 1000, // Optional override
});
// { page: 1, limit: 20, total: 145, totalPages: 8 }

generateRequestId()

Generates a unique request ID.

Returns: string - Format: req_<24 hex chars>

getTimestamp()

Gets current timestamp in ISO format.

Returns: string - ISO timestamp

Swagger Decorators

@ApiOkContract(model, isArray?)

Decorator for Swagger documentation of standardized responses.

Parameters:

  • model: Type<T> - DTO class for data type
  • isArray?: boolean - Whether response is array (default: false)

Example:

@ApiOkContract(UserDto)        // Single user
@Get(':id')
async findOne() { ... }

@ApiOkContract(UserDto, true)  // Array of users (pagination optional)
@Get()
async findAll() { ... }

@ApiOkPaginatedContract(model)

Decorator for Swagger documentation of paginated responses. Explicitly indicates that meta.pagination will always be present and required in Swagger schema.

Parameters:

  • model: Type<T> - DTO class for data type

Example:

@ApiOkPaginatedContract(UserDto)  // Paginated list
@Get()
async list() { ... }

Difference from @ApiOkContract(UserDto, true):

  • @ApiOkContract(UserDto, true) - simple array, meta.pagination is optional in Swagger
  • @ApiOkPaginatedContract(UserDto) - explicitly marks meta.pagination as required in Swagger schema

Error Response Decorators

Decorators for documenting error responses in Swagger:

@ApiBadRequestContract()      // 400 Bad Request
@ApiUnauthorizedContract()     // 401 Unauthorized
@ApiForbiddenContract()        // 403 Forbidden
@ApiNotFoundContract()         // 404 Not Found
@ApiConflictContract()         // 409 Conflict
@ApiInternalErrorContract()    // 500 Internal Server Error

Constants

REQUEST_ID_HEADER

Header name for request ID: 'x-request-id'

PAGINATION

Pagination constants:

  • DEFAULT_PAGE: 1
  • DEFAULT_LIMIT: 20
  • MIN_PAGE: 1
  • MAX_PAGE: 1000 (changed from 10000)
  • MIN_LIMIT: 1
  • MAX_LIMIT: 100 (changed from 1000)

Integration Guide

Step 1: Install Package

npm install @ciphercross/nestjs-contracts

Step 2: Setup (Choose One Method)

Method A: Using ContractsModule (Recommended)

In app.module.ts:

import { Module } from '@nestjs/common';
import { ContractsModule } from '@ciphercross/nestjs-contracts';

@Module({
  imports: [
    ContractsModule.forRoot({
      defaultPagination: { limit: 20, maxLimit: 100, maxPage: 1000 },
      enableRequestContext: true,
      extractUserId: (req) => req.user?.id,
    }),
  ],
})
export class AppModule {}

Method B: Manual Setup

In main.ts:

import { requestIdMiddleware, ResponseWrapInterceptor } from '@ciphercross/nestjs-contracts';

app.use(requestIdMiddleware);
app.useGlobalInterceptors(new ResponseWrapInterceptor());

Important: Middleware must be added before interceptors.

Step 3: Use in Controllers

Simple Response

@Get(':id')
@ApiOkContract(UserDto)
async findOne(@Param('id') id: string) {
  return this.service.findOne(id); // Automatically wrapped
}

Paginated Response (Automatic - Recommended)

@Get()
@ApiOkPaginatedContract(UserDto) // Explicitly indicates pagination
async list(@Query() query: { page?: number; limit?: number }) {
  // Just return { items, total, page?, limit? } - interceptor handles it
  return this.service.findAll({
    page: query.page ?? 1,
    limit: query.limit ?? 20,
  });
}

Paginated Response (Manual with @Req())

@Get()
@ApiOkContract(UserDto, true)
async list(@Query() query: ListDto, @Req() req: any) {
  const { items, total } = await this.service.findAll(query);
  
  req.paginationMeta = buildPaginationMeta({
    page: query.page ?? 1,
    limit: query.limit ?? 20,
    total,
  });
  
  return items; // Pagination meta added automatically
}

Step 4: Frontend Integration

Install in frontend project:

npm install @ciphercross/nestjs-contracts

Use types and helpers:

import type { SuccessResponse, PaginationMeta } from '@ciphercross/nestjs-contracts';
import {
  unwrapData,
  getPagination,
  getRequestId,
  isSuccessResponse,
  isErrorResponse,
  getErrorMessage,
} from '@ciphercross/nestjs-contracts';

async function fetchUsers() {
  const response = await fetch('/api/users');
  const data = await response.json();

  if (isSuccessResponse(data)) {
    const users = unwrapData(data);
    const pagination = getPagination(data);
    const requestId = getRequestId(data);
    return { users, pagination, requestId };
  } else {
    const message = getErrorMessage(data);
    throw new Error(message);
  }
}

Examples

Example 1: Simple GET Endpoint

@Controller('products')
export class ProductsController {
  @Get(':id')
  @ApiOkContract(ProductDto)
  async getProduct(@Param('id') id: string) {
    return this.productsService.findOne(id);
  }
}

Response:

{
  "success": true,
  "data": {
    "id": "123",
    "name": "Product Name",
    "price": 99.99
  },
  "meta": {
    "timestamp": "2026-01-12T13:00:00.000Z",
    "requestId": "req_abc123"
  }
}

Example 2: Paginated List (Automatic)

@Controller('users')
export class UsersController {
  @Get()
  @ApiOkPaginatedContract(UserDto)
  async listUsers(@Query() query: { page?: number; limit?: number }) {
    // Just return { items, total, page?, limit? } - no @Req() needed!
    return this.usersService.findAll({
      page: query.page ?? 1,
      limit: query.limit ?? 20,
    });
  }
}

Example 2b: Paginated List (Manual)

@Controller('users')
export class UsersController {
  @Get()
  @ApiOkContract(UserDto, true)
  async listUsers(
    @Query() query: { page?: number; limit?: number },
    @Req() req: any,
  ) {
    const { items, total } = await this.usersService.findAll({
      page: query.page ?? 1,
      limit: query.limit ?? 20,
    });

    req.paginationMeta = buildPaginationMeta({
      page: query.page ?? 1,
      limit: query.limit ?? 20,
      total,
    });

    return items;
  }
}

Response:

{
  "success": true,
  "data": [
    { "id": "1", "name": "User 1" },
    { "id": "2", "name": "User 2" }
  ],
  "meta": {
    "timestamp": "2026-01-12T13:00:00.000Z",
    "requestId": "req_abc123",
    "pagination": {
      "page": 1,
      "limit": 20,
      "total": 145,
      "totalPages": 8
    }
  }
}

Example 3: Using RequestContext

Access requestId and userId from services without req:

import { RequestContext } from '@ciphercross/nestjs-contracts';

@Injectable()
export class UsersService {
  private readonly logger = new Logger(UsersService.name);

  async findAll() {
    const requestId = RequestContext.getRequestId();
    const userId = RequestContext.getUserId();
    
    this.logger.log(`[${requestId}] Finding users for ${userId}`);
    return this.repository.findAll();
  }
}

Example 4: Custom Request ID

If your gateway/ingress sets x-request-id header, it will be used:

GET /api/users HTTP/1.1
x-request-id: gateway-request-12345

The same ID will be:

  • Stored in req.requestId
  • Included in response meta.requestId
  • Returned in response header x-request-id
  • Available via RequestContext.getRequestId() (if enabled)

Example 5: Skip Wrapping

For file downloads, streaming, redirects:

import { SkipContract } from '@ciphercross/nestjs-contracts';

@Controller('files')
export class FilesController {
  @SkipContract()
  @Get('download/:id')
  downloadFile(@Param('id') id: string, @Res() res: Response) {
    return res.sendFile(`/path/to/file-${id}.pdf`);
  }
}

Type Safety

All types are fully typed and exported:

import type {
  SuccessResponse,
  ErrorResponse,
  ResponseMeta,
  PaginationMeta,
  ApiError,
} from '@ciphercross/nestjs-contracts';

// Type inference works automatically
const result: SuccessResponse<User> = await fetchUser();

Best Practices

  1. Use ContractsModule.forRoot() for easy setup
  2. Return { items, total, page?, limit? } (type PaginatedResult<T>) for automatic pagination (no @Req() needed)
    • Legacy/Escape hatch: Use req.paginationMeta if you need more control
  3. Use @ApiOkPaginatedContract() for paginated endpoints in Swagger
  4. Return only data from controllers (interceptor wraps it)
  5. Use @SkipContract() for file downloads, streaming, redirects
  6. Use RequestContext for logging in services (no need to pass req)
  7. Use unwrap helpers (unwrapOrThrow, getErrors) in frontend for cleaner code
  8. Import types in frontend for type safety

Troubleshooting

Request ID is "unknown"

Problem: Request ID shows as "unknown" in responses.

Solution: Ensure requestIdMiddleware is added before interceptors:

app.use(requestIdMiddleware); // Must be first
app.useGlobalInterceptors(new ResponseWrapInterceptor());

Pagination Meta Not Included

Problem: meta.pagination is missing in response.

Solution A (Automatic - Recommended): Return { items, total, page?, limit? } (type PaginatedResult<T>) from controller:

return {
  items: [...],
  total: 145,
  page: 1,  // Optional: will use req.query.page if not provided
  limit: 20, // Optional: will use req.query.limit if not provided
};

Priority for page/limit:

  1. result.page / result.limit (if service returned)
  2. req.query.page / req.query.limit (if present)
  3. config.defaultPagination.limit + page=1 (default)

Solution B (Manual - Legacy): Set req.paginationMeta in controller:

req.paginationMeta = buildPaginationMeta({ page, limit, total });

RequestContext Returns undefined

Problem: RequestContext.getRequestId() or RequestContext.getUserId() returns undefined.

Solution: Enable RequestContext in ContractsModule.forRoot():

ContractsModule.forRoot({
  enableRequestContext: true,
  extractUserId: (req) => req.user?.id, // Optional: if userId not in req.userId
})

Swagger Schema Not Showing

Problem: Swagger doesn't show correct response schema.

Solution: Use @ApiOkContract decorator:

@ApiOkContract(UserDto)
@Get(':id')
async findOne() { ... }

License

MIT