Skip to content

mirzasaikatahmmed/nestjs-api-forge

Repository files navigation

⚑ NestJS API Forge

Plug-and-play response envelope, exception filter, and error formatting for NestJS REST APIs β€” zero boilerplate, fully typed.

npm version npm downloads License: MIT NestJS Built with TypeScript CI Sponsor Buy Me a Coffee


One import to standardize every response, every error, and every validation in your NestJS API.


🧩 What it does

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

Decorators at a glance

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

πŸ“¦ Installation

Requirements

  • Node.js v18 or higher
  • NestJS v9, v10, or v11

Install

npm install nestjs-api-forge

Verify the install:

node -e "require('nestjs-api-forge'); console.log('nestjs-api-forge installed')"

πŸš€ Quick Start

1. Register globally in app.module.ts

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

2. Add ForgeValidationPipe in main.ts

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.


πŸ“ Response Shapes

Success (2xx)

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

Error (4xx / 5xx)

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

Validation Error

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

Paginated

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

Deprecated Route

{
  "success": true,
  "statusCode": 200,
  "message": "OK",
  "data": {},
  "meta": {
    "timestamp": "2025-01-15T10:00:00.000Z",
    "deprecated": true,
    "deprecationNotice": "Use /v2/users instead"
  }
}

πŸ“– API Reference

ApiForgeModule.forRoot(options?)

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

ApiForgeModule.forRootAsync(asyncOptions)

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'),
  }),
})

ForgeValidationPipe

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,
}));

ApiResponseDto β€” manual builders

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' });

Built-in Exceptions

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

πŸ’‘ Usage Examples

Standard CRUD controller

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);
  }
}

Paginated list

@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');
}

Deprecated route

@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');
}

Custom meta per route

@Get()
@ForgeMeta({ region: 'us-east-1', cache: 'miss' })
findAll() {
  return this.usersService.findAll();
}

Per-controller scope (no global module)

@Controller('products')
@ApiForge({ version: '2.0', includeResponseTime: true })
export class ProductsController { ... }

Raw response (health check)

@Get('health')
@ForgeRawResponse()
health() {
  return { status: 'ok', uptime: process.uptime() };
}

Correlation ID tracing

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

Custom exception with field details

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' },
});

πŸ“ Project Structure

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

πŸ› οΈ Development

# 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

πŸ“¦ Peer Dependencies

@nestjs/common   ^9 | ^10 | ^11
@nestjs/core     ^9 | ^10 | ^11
reflect-metadata ^0.1 | ^0.2
rxjs             ^7

πŸ“‹ Changelog

v1.0.7 β€” Current

  • 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 /health endpoint 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_REQUIRED mappings

Full history: CHANGELOG.md

v1.0.6

  • Changelog updated β€” all versions documented with accurate entries aligned to npm release history

v1.0.5

  • 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-eslint integration; npm run lint now works out of the box

v1.0.4

  • ForgeValidationPipe β€” structured validation errors with nested field support (dot-notation)
  • @ForgeMeta(extra) β€” merge custom fields into response meta per route or controller
  • @ForgeDeprecated(notice?) β€” deprecation flag in meta + Deprecation: true response header
  • @ForgeMessage, @ForgeRawResponse, @ApiForge decorators
  • ApiResponseDto.accepted() β€” 202 Accepted helper
  • Correlation ID passthrough (correlationIdHeader option)
  • Response time measurement (includeResponseTime option)
  • forRootAsync fix β€” options factory now runs once instead of twice
  • 3 new exceptions: MethodNotAllowedException, PaymentRequiredException, GatewayTimeoutException
  • Response metadata support (includePath, includeTimestamp, includeRequestId, includeResponseTime)

v1.0.3

  • Update repository URL format in package.json

v1.0.0

  • Initial release β€” ApiForgeModule.forRoot(), ForgeExceptionFilter, ForgeResponseInterceptor, ApiResponseDto, and 10 typed exceptions

🀝 Contributing

Pull requests are welcome! For major changes, please open an issue first to discuss your approach.

  1. Fork the repository
  2. Create your branch: git checkout -b feat/your-feature
  3. Make your changes and run npm run format
  4. 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.


πŸ’› Support

If NestJS API Forge saves you boilerplate, consider supporting the project:

GitHub Sponsors Buy Me a Coffee


Made with ❀️ by Mirza Saikat Ahmmed

About

Resources

Code of conduct

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors