Plug-and-play response envelope, exception filter, and error formatting for NestJS REST APIs β zero boilerplate, fully typed.
One import to standardize every response, every error, and every validation in your NestJS API.
Register ApiForgeModule once and every response from your API is automatically shaped into a consistent envelope. No more manual { success, data, message } wrappers scattered across controllers.
| Layer | What Forge provides |
|---|---|
| π¦ | Response envelope β every handler wrapped with success, statusCode, message, data, and meta |
| π‘οΈ | Global exception filter β handles HttpException, validation errors, and unexpected crashes uniformly |
| β | Structured validation β ForgeValidationPipe replaces ValidationPipe with typed field-level error details |
| π | Paginated responses β ApiResponseDto.paginated() with full pagination meta in one call |
| π·οΈ | Per-route decorators β override messages, skip wrapping, add custom meta, or mark routes deprecated |
| π | Request tracing β correlation ID passthrough, auto UUID generation, and response time measurement |
| π₯ | Typed exceptions β drop-in replacements for every NestJS HttpException with structured error codes |
| Decorator | Scope | Description |
|---|---|---|
@ApiForge(options?) |
Controller / Method | Apply filter + interceptor without going global |
@ForgeMessage(msg) |
Controller / Method | Override the success message for that route |
@ForgeRawResponse() |
Controller / Method | Skip envelope wrapping β return raw handler value |
@ForgeMeta(extra) |
Controller / Method | Merge extra key-value pairs into meta |
@ForgeDeprecated(notice?) |
Controller / Method | Mark route deprecated β adds meta.deprecated + Deprecation header |
- Node.js v18 or higher
- NestJS v9, v10, or v11
npm install nestjs-api-forgeVerify the install:
node -e "require('nestjs-api-forge'); console.log('nestjs-api-forge installed')"import { Module } from '@nestjs/common';
import { ApiForgeModule } from 'nestjs-api-forge';
@Module({
imports: [
ApiForgeModule.forRoot({
version: '1.0.0',
defaultSuccessMessage: 'Request successful',
includePath: true,
includeTimestamp: true,
includeRequestId: true,
includeResponseTime: true,
correlationIdHeader: 'x-request-id',
}),
],
})
export class AppModule {}import { NestFactory } from '@nestjs/core';
import { ForgeValidationPipe } from 'nestjs-api-forge';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ForgeValidationPipe());
await app.listen(3000);
}
bootstrap();That's it. Every response is now wrapped, every error is structured, and every validation failure returns typed field details.
{
"success": true,
"statusCode": 200,
"message": "User fetched successfully",
"data": { "id": 1, "name": "Alice Johnson", "email": "alice@example.com" },
"meta": {
"timestamp": "2025-01-15T10:00:00.000Z",
"path": "/api/users/1",
"version": "1.0.0",
"requestId": "a3f2c1d0-84e5-4b6a-9123-abc123def456",
"responseTime": "4ms"
}
}{
"success": false,
"statusCode": 404,
"message": "User not found",
"error": { "code": "NOT_FOUND" },
"meta": {
"timestamp": "2025-01-15T10:00:00.000Z",
"path": "/api/users/99",
"requestId": "b1e2f3a4-0000-4b5c-8d9e-fedcba987654",
"responseTime": "2ms"
}
}{
"success": false,
"statusCode": 400,
"message": "Validation failed",
"error": {
"code": "VALIDATION_ERROR",
"details": [
{ "field": "email", "message": "must be an email" },
{ "field": "address.zip", "message": "must be a string" }
]
},
"meta": { "timestamp": "2025-01-15T10:00:00.000Z", "path": "/api/users" }
}{
"success": true,
"statusCode": 200,
"message": "Users fetched successfully",
"data": [{ "id": 1, "name": "Alice Johnson" }],
"pagination": {
"total": 42,
"page": 2,
"limit": 10,
"totalPages": 5,
"hasNextPage": true,
"hasPrevPage": true
},
"meta": {
"timestamp": "2025-01-15T10:00:00.000Z",
"path": "/api/users?page=2&limit=10",
"responseTime": "8ms"
}
}{
"success": true,
"statusCode": 200,
"message": "OK",
"data": {},
"meta": {
"timestamp": "2025-01-15T10:00:00.000Z",
"deprecated": true,
"deprecationNotice": "Use /v2/users instead"
}
}| Option | Type | Default | Description |
|---|---|---|---|
includePath |
boolean |
true |
Include request path in meta |
includeTimestamp |
boolean |
true |
Include ISO timestamp in meta |
includeRequestId |
boolean |
false |
Auto-generate UUID requestId in meta if no correlation header found |
correlationIdHeader |
string | string[] |
['x-request-id', 'x-correlation-id'] |
Header(s) to read request ID from; echoed back on the response |
includeResponseTime |
boolean |
false |
Include handler duration as meta.responseTime (e.g. "12ms") |
version |
string |
undefined |
API version string added to meta |
defaultSuccessMessage |
string |
'Request successful' |
Fallback success message |
Use when options depend on a config service:
ApiForgeModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
version: config.get('API_VERSION'),
includeRequestId: true,
includeResponseTime: true,
correlationIdHeader: config.get('CORRELATION_HEADER'),
}),
})Drop-in replacement for NestJS ValidationPipe. Throws ValidationException with structured details instead of a string-message array. Supports nested objects using dot-notation field paths.
// Use defaults
app.useGlobalPipes(new ForgeValidationPipe());
// Override defaults
app.useGlobalPipes(new ForgeValidationPipe({
whitelist: false,
forbidNonWhitelisted: false,
}));import { ApiResponseDto } from 'nestjs-api-forge';
ApiResponseDto.success(data, 'OK', 200, { path: '/users/1' });
ApiResponseDto.created(data, 'User created');
ApiResponseDto.accepted(jobRef, 'Export queued'); // 202
ApiResponseDto.noContent('Deleted'); // 204
ApiResponseDto.paginated(data, total, page, limit, 'Users fetched');
ApiResponseDto.error('Not found', 404, { code: 'NOT_FOUND' });All extend ApiException β HttpException. Pass an optional message and details array to any of them.
| Class | Status | Code |
|---|---|---|
BadRequestException |
400 | BAD_REQUEST |
UnauthorizedException |
401 | UNAUTHORIZED |
PaymentRequiredException |
402 | PAYMENT_REQUIRED |
ForbiddenException |
403 | FORBIDDEN |
NotFoundException |
404 | NOT_FOUND |
MethodNotAllowedException |
405 | METHOD_NOT_ALLOWED |
ConflictException |
409 | CONFLICT |
UnprocessableEntityException |
422 | UNPROCESSABLE_ENTITY |
TooManyRequestsException |
429 | TOO_MANY_REQUESTS |
InternalServerException |
500 | INTERNAL_SERVER_ERROR |
ServiceUnavailableException |
503 | SERVICE_UNAVAILABLE |
GatewayTimeoutException |
504 | GATEWAY_TIMEOUT |
ValidationException |
400 | VALIDATION_ERROR |
import { Controller, Get, Delete, Param, HttpCode, HttpStatus } from '@nestjs/common';
import { ForgeMessage, NotFoundException } from 'nestjs-api-forge';
@Controller('users')
export class UsersController {
@Get(':id')
@ForgeMessage('User fetched successfully')
findOne(@Param('id') id: number) {
const user = this.usersService.findById(id);
if (!user) throw new NotFoundException('User not found');
return user;
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
remove(@Param('id') id: number) {
this.usersService.remove(id);
}
}@Get()
@ForgeRawResponse()
findAll(@Query('page') page = '1', @Query('limit') limit = '10') {
const p = parseInt(page, 10);
const l = parseInt(limit, 10);
const { data, total } = this.usersService.findAll(p, l);
return ApiResponseDto.paginated(data, total, p, l, 'Users fetched');
}@Get('export')
@ForgeDeprecated('Use POST /v2/users/export instead')
@ForgeMessage('Export started')
startExport() {
const job = this.usersService.queueExport();
return ApiResponseDto.accepted({ jobId: job.id }, 'Export queued');
}@Get()
@ForgeMeta({ region: 'us-east-1', cache: 'miss' })
findAll() {
return this.usersService.findAll();
}@Controller('products')
@ApiForge({ version: '2.0', includeResponseTime: true })
export class ProductsController { ... }@Get('health')
@ForgeRawResponse()
health() {
return { status: 'ok', uptime: process.uptime() };
}Send X-Request-ID: abc-123 in the request β the same ID appears in meta.requestId and is echoed in the response header. Useful for distributed tracing across microservices.
ApiForgeModule.forRoot({
correlationIdHeader: 'x-request-id', // or an array of headers
includeRequestId: true, // generate UUID if header is absent
})throw new BadRequestException('Invalid input', [
{ field: 'price', message: 'Must be a positive number', value: -5 },
]);
// From class-validator constraint map
throw ValidationException.fromConstraints({
email: { isEmail: 'must be an email' },
age: { min: 'must be at least 1' },
});nestjs-api-forge/
βββ src/
β βββ api-forge.module.ts # forRoot / forRootAsync registration
β βββ index.ts # Public exports
β βββ decorators/
β β βββ api-response.decorator.ts # @ApiForge, @ForgeMessage, @ForgeRawResponse, @ForgeMeta, @ForgeDeprecated
β βββ dto/
β β βββ api-response.dto.ts # ApiResponseDto static builders
β βββ exceptions/
β β βββ api.exception.ts # Base ApiException class
β β βββ validation.exception.ts # ValidationException with field details
β βββ filters/
β β βββ global-exception.filter.ts # ForgeExceptionFilter
β βββ interceptors/
β β βββ response.interceptor.ts # ForgeResponseInterceptor
β βββ interfaces/
β β βββ api-response.interface.ts # TypeScript interfaces and ForgeOptions
β βββ pipes/
β β βββ forge-validation.pipe.ts # ForgeValidationPipe
β βββ utils/
β βββ error-code.util.ts
βββ app/ # Example NestJS app demonstrating the library
β βββ src/
β βββ users/ # Full CRUD example with pagination
β βββ products/ # Per-controller @ApiForge example
βββ .github/
β βββ ISSUE_TEMPLATE/ # Bug report & feature request templates
β βββ workflows/
β βββ publish.yml # npm publish on tag push
β βββ malware-scan.yml # Security scan on every push
βββ CONTRIBUTING.md
βββ CODE_OF_CONDUCT.md
βββ package.json
# Clone the repo
git clone https://github.com/mirzasaikatahmmed/nestjs-api-forge.git
cd nestjs-api-forge
# Install dependencies
npm install
# Build the library
npm run build
# Watch mode
npm run build:watch
# Format code
npm run format
# Run the example app
cd app && npm install && npm run start:dev@nestjs/common ^9 | ^10 | ^11
@nestjs/core ^9 | ^10 | ^11
reflect-metadata ^0.1 | ^0.2
rxjs ^7
- 10 new HTTP exception classes β
NotAcceptableException(406),RequestTimeoutException(408),GoneException(410),PayloadTooLargeException(413),UnsupportedMediaTypeException(415),LockedException(423),FailedDependencyException(424),PreconditionRequiredException(428),NotImplementedException(501),BadGatewayException(502) ForgeHealthModuleβ configurable/healthendpoint with custom checks, uptime, memory stats, and Forge-formatted responses; throws 503 automatically when any check fails- Updated error-code map β added
LOCKED,FAILED_DEPENDENCY,PRECONDITION_REQUIREDmappings
Full history: CHANGELOG.md
- Changelog updated β all versions documented with accurate entries aligned to npm release history
- Open source documentation β full README rewrite with badges, "What it does" table, response shape examples, and complete API reference
- Community health files β
CONTRIBUTING.md,CODE_OF_CONDUCT.md,CONTRIBUTORS.md,REPOSITORY_RULES.md - GitHub templates β bug report and feature request issue templates,
FUNDING.yml,CODEOWNERS - ESLint setup β ESLint v10 flat config (
eslint.config.mjs) with@typescript-eslintintegration;npm run lintnow works out of the box
ForgeValidationPipeβ structured validation errors with nested field support (dot-notation)@ForgeMeta(extra)β merge custom fields into responsemetaper route or controller@ForgeDeprecated(notice?)β deprecation flag inmeta+Deprecation: trueresponse header@ForgeMessage,@ForgeRawResponse,@ApiForgedecoratorsApiResponseDto.accepted()β 202 Accepted helper- Correlation ID passthrough (
correlationIdHeaderoption) - Response time measurement (
includeResponseTimeoption) forRootAsyncfix β options factory now runs once instead of twice- 3 new exceptions:
MethodNotAllowedException,PaymentRequiredException,GatewayTimeoutException - Response metadata support (
includePath,includeTimestamp,includeRequestId,includeResponseTime)
- Update repository URL format in
package.json
- Initial release β
ApiForgeModule.forRoot(),ForgeExceptionFilter,ForgeResponseInterceptor,ApiResponseDto, and 10 typed exceptions
Pull requests are welcome! For major changes, please open an issue first to discuss your approach.
- Fork the repository
- Create your branch:
git checkout -b feat/your-feature - Make your changes and run
npm run format - Push and open a PR against
main
Please read CONTRIBUTING.md for full guidelines and CODE_OF_CONDUCT.md before participating. All contributors are listed in CONTRIBUTORS.md.
If NestJS API Forge saves you boilerplate, consider supporting the project:
Made with β€οΈ by Mirza Saikat Ahmmed