Skip to main content

@nest-toolbox/response-envelope

npm version

Standard API response envelope with interceptor, exception filter, and helpers for NestJS. Zero-config consistent responses across your entire API.

Installation

npm install @nest-toolbox/response-envelope

Peer dependencies: @nestjs/common, @nestjs/core

Quick Start

// app.module.ts
import { Module } from '@nestjs/common';
import { ResponseEnvelopeModule } from '@nest-toolbox/response-envelope';

@Module({
imports: [ResponseEnvelopeModule.forRoot()],
})
export class AppModule {}

That's it. Every route now returns a consistent envelope — no interceptors to register, no filters to bind.

// Before (raw)
@Get(':id')
findOne(@Param('id') id: string) {
return { id: 1, name: 'Alice' };
}

// After (automatic envelope)
// → { success: true, data: { id: 1, name: "Alice" }, message: "OK", meta: { ... } }

Features

  • 🚀 Zero-config — Import the module, every route gets a consistent envelope
  • 🎯 Decorator-first@SkipEnvelope() and @ApiMessage() for per-route control
  • 🛡️ Consistent error formattingclass-validator errors auto-parsed into structured field errors
  • 🔄 Idempotent — Already-wrapped responses pass through untouched
  • ⚙️ ConfigurableforRoot() / forRootAsync() for custom defaults
  • 📦 Manual helperssuccess(), error(), paginated() for WebSockets, queues, etc.

Response Format

Success

{
"success": true,
"data": { "id": 1, "name": "Alice" },
"message": "OK",
"meta": {
"timestamp": "2025-02-05T07:32:00.000Z",
"path": "/api/users/1",
"statusCode": 200
}
}

Paginated Success

{
"success": true,
"data": [{ "id": 1 }, { "id": 2 }],
"message": "OK",
"meta": {
"timestamp": "2025-02-05T07:32:00.000Z",
"path": "/api/users",
"statusCode": 200,
"pagination": {
"page": 1,
"limit": 20,
"total": 100,
"totalPages": 5
}
}
}

Error

{
"success": false,
"data": null,
"message": "Validation failed",
"errors": [
{ "field": "email", "message": "must be a valid email" },
{ "field": "name", "message": "should not be empty" }
],
"meta": {
"timestamp": "2025-02-05T07:32:00.000Z",
"path": "/api/users",
"statusCode": 400
}
}

API Reference

ResponseEnvelopeModule

ResponseEnvelopeModule.forRoot(options?)

Register the module globally with static options. Automatically registers the interceptor and exception filter via APP_INTERCEPTOR and APP_FILTER.

ResponseEnvelopeModule.forRoot({
defaultMessage: 'Success',
includePath: true,
includeTimestamp: true,
});
OptionTypeDefaultDescription
defaultMessagestring'OK'Default success message when @ApiMessage() is not used
includePathbooleantrueInclude the request path in meta
includeTimestampbooleantrueInclude the ISO timestamp in meta

ResponseEnvelopeModule.forRootAsync(options)

Register with async factory injection.

ResponseEnvelopeModule.forRootAsync({
imports: [ConfigModule],
useFactory: (config: ConfigService) => ({
defaultMessage: config.get('API_DEFAULT_MESSAGE', 'OK'),
includePath: config.get('API_INCLUDE_PATH') !== 'false',
}),
inject: [ConfigService],
});

Decorators

@SkipEnvelope()

Skip the envelope for a specific route handler. The raw return value is sent as-is.

import { SkipEnvelope } from '@nest-toolbox/response-envelope';

@Controller()
export class AppController {
@Get('health')
@SkipEnvelope()
health() {
return { status: 'ok' }; // Sent as-is, no envelope
}
}

@ApiMessage(message)

Set a custom success message for a route handler.

import { ApiMessage } from '@nest-toolbox/response-envelope';

@Controller('users')
export class UsersController {
@Post()
@ApiMessage('User created successfully')
create(@Body() dto: CreateUserDto) {
return this.usersService.create(dto);
}
// → { success: true, data: { ... }, message: "User created successfully", meta: { ... } }

@Delete(':id')
@ApiMessage('User deleted')
remove(@Param('id') id: string) {
return this.usersService.remove(id);
}
}

Helper Functions

Manual envelope constructors for contexts outside the HTTP interceptor — WebSocket gateways, message queue handlers, CLI scripts, etc.

success(data, options?)

Create a success envelope.

import { success } from '@nest-toolbox/response-envelope';

const response = success(user, {
message: 'User found',
path: '/api/users/1',
statusCode: 200,
});

Options:

OptionTypeDefaultDescription
messagestring'OK'Success message
pathstring''Request path
statusCodenumber200HTTP status code
paginationPaginationMetaPagination metadata

Returns: ApiResponse<T>

error(message, options?)

Create an error envelope.

import { error } from '@nest-toolbox/response-envelope';

const response = error('Validation failed', {
statusCode: 400,
errors: [{ field: 'email', message: 'must be a valid email' }],
path: '/api/users',
});

Options:

OptionTypeDefaultDescription
errorsFieldError[][]Structured field-level errors
pathstring''Request path
statusCodenumber500HTTP status code

Returns: ApiErrorResponse

paginated(data, pagination, options?)

Create a paginated success envelope. Automatically calculates totalPages.

import { paginated } from '@nest-toolbox/response-envelope';

const response = paginated(users, { page: 1, limit: 20, total: 100 }, {
message: 'Users retrieved',
path: '/api/users',
});
// → meta.pagination = { page: 1, limit: 20, total: 100, totalPages: 5 }

Pagination parameter:

FieldTypeDescription
pagenumberCurrent page number
limitnumberItems per page
totalnumberTotal items across all pages

Options: Same as success() (without pagination).

Returns: ApiResponse<T[]>


Exception Filter

The EnvelopeExceptionFilter is automatically registered when you import the module. It catches all exceptions and formats them as ApiErrorResponse.

Handled exception types:

ExceptionStatus CodeBehavior
BadRequestException (class-validator)400Parses validation messages into FieldError[]
NotFoundException404Standard error envelope
Other HttpExceptionVariesUses exception status and message
Unknown errors500"Internal server error"

class-validator integration: When using ValidationPipe, error messages like "email must be a valid email" are automatically parsed — the first word becomes the field and the rest becomes the message.


Types

All types are exported from the package:

import type {
ApiResponse, // { success: true, data: T, message: string, meta: ResponseMeta }
ApiErrorResponse, // { success: false, data: null, message: string, errors: FieldError[], meta: ResponseMeta }
ResponseMeta, // { timestamp: string, path: string, statusCode: number, pagination?: PaginationMeta }
PaginationMeta, // { page: number, limit: number, total: number, totalPages: number }
FieldError, // { field: string, message: string }
ResponseEnvelopeOptions, // { defaultMessage?, includePath?, includeTimestamp? }
ResponseEnvelopeAsyncOptions,
} from '@nest-toolbox/response-envelope';

Examples

Full CRUD controller

import { Controller, Get, Post, Delete, Param, Body, Query } from '@nestjs/common';
import { ApiMessage, SkipEnvelope } from '@nest-toolbox/response-envelope';

@Controller('users')
export class UsersController {
constructor(private usersService: UsersService) {}

@Get()
@ApiMessage('Users retrieved')
findAll(@Query('page') page = 1, @Query('limit') limit = 20) {
return this.usersService.findAll(page, limit);
}

@Get(':id')
findOne(@Param('id') id: string) {
return this.usersService.findOne(id);
// → { success: true, data: { ... }, message: "OK", meta: { ... } }
}

@Post()
@ApiMessage('User created successfully')
create(@Body() dto: CreateUserDto) {
return this.usersService.create(dto);
}

@Delete(':id')
@ApiMessage('User deleted')
remove(@Param('id') id: string) {
return this.usersService.remove(id);
}
}

WebSocket gateway with manual helpers

import { WebSocketGateway, SubscribeMessage } from '@nestjs/websockets';
import { success, error } from '@nest-toolbox/response-envelope';

@WebSocketGateway()
export class ChatGateway {
@SubscribeMessage('message')
handleMessage(client: any, payload: any) {
try {
const result = this.chatService.processMessage(payload);
return success(result, { message: 'Message sent' });
} catch (e) {
return error(e.message, { statusCode: 400 });
}
}
}

Integration with @nest-toolbox/typeorm-paginate

The paginated() helper pairs naturally with typeorm-paginate:

import { rows } from '@nest-toolbox/typeorm-paginate';
import { paginated } from '@nest-toolbox/response-envelope';

@Get()
async findAll(@Query('page') page = 1, @Query('limit') limit = 20) {
const [items, total] = await this.userRepo.findAndCount({
skip: (page - 1) * limit,
take: limit,
});

return paginated(items, { page, limit, total });
}

Health check endpoint (skip envelope)

@Get('health')
@SkipEnvelope()
health() {
return {
status: 'ok',
uptime: process.uptime(),
timestamp: new Date().toISOString(),
};
}

Already-wrapped responses pass through

If your code already returns an envelope-shaped object (with success boolean and meta object), the interceptor won't double-wrap it:

@Get('custom')
custom() {
return success({ id: 1 }, { message: 'Custom envelope' });
// Passes through unchanged — no double-wrapping
}