nestjs-api-forge 1.0.3 → 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +290 -105
- package/dist/decorators/api-response.decorator.d.ts +4 -0
- package/dist/decorators/api-response.decorator.js +7 -1
- package/dist/decorators/api-response.decorator.js.map +1 -1
- package/dist/dto/api-response.dto.d.ts +1 -0
- package/dist/dto/api-response.dto.js +3 -0
- package/dist/dto/api-response.dto.js.map +1 -1
- package/dist/filters/global-exception.filter.d.ts +1 -0
- package/dist/filters/global-exception.filter.js +23 -4
- package/dist/filters/global-exception.filter.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/interceptors/response.interceptor.d.ts +1 -0
- package/dist/interceptors/response.interceptor.js +45 -5
- package/dist/interceptors/response.interceptor.js.map +1 -1
- package/dist/interfaces/api-response.interface.d.ts +6 -0
- package/dist/pipes/forge-validation.pipe.d.ts +4 -0
- package/dist/pipes/forge-validation.pipe.js +34 -0
- package/dist/pipes/forge-validation.pipe.js.map +1 -0
- package/dist/pipes/index.d.ts +1 -0
- package/dist/pipes/index.js +18 -0
- package/dist/pipes/index.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -1,39 +1,74 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
[](https://www.npmjs.com/package/nestjs-api-forge)
|
|
5
|
-
[](https://www.npmjs.com/package/nestjs-api-forge)
|
|
6
|
-
[](https://github.com/mirzasaikatahmmed/nestjs-api-forge/blob/main/LICENSE)
|
|
3
|
+
# ⚡ NestJS API Forge
|
|
7
4
|
|
|
8
|
-
Plug-and-play response envelope, exception filter, and error formatting for
|
|
5
|
+
**Plug-and-play response envelope, exception filter, and error formatting for NestJS REST APIs — zero boilerplate, fully typed.**
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/nestjs-api-forge)
|
|
8
|
+
[](https://www.npmjs.com/package/nestjs-api-forge)
|
|
9
|
+
[](https://opensource.org/licenses/MIT)
|
|
10
|
+
[](https://nestjs.com)
|
|
11
|
+
[](https://www.typescriptlang.org)
|
|
12
|
+
[](https://github.com/mirzasaikatahmmed/nestjs-api-forge/actions/workflows/publish.yml)
|
|
13
|
+
[](https://github.com/sponsors/mirzasaikatahmmed)
|
|
14
|
+
[](https://buymeacoffee.com/saikat)
|
|
15
|
+
|
|
16
|
+
<br/>
|
|
17
|
+
|
|
18
|
+
> One import to standardize every response, every error, and every validation in your NestJS API.
|
|
19
|
+
|
|
20
|
+
</div>
|
|
9
21
|
|
|
10
22
|
---
|
|
11
23
|
|
|
12
|
-
##
|
|
24
|
+
## 🧩 What it does
|
|
25
|
+
|
|
26
|
+
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.
|
|
27
|
+
|
|
28
|
+
| Layer | What Forge provides |
|
|
29
|
+
|-------|-------------------|
|
|
30
|
+
| 📦 | **Response envelope** — every handler wrapped with `success`, `statusCode`, `message`, `data`, and `meta` |
|
|
31
|
+
| 🛡️ | **Global exception filter** — handles `HttpException`, validation errors, and unexpected crashes uniformly |
|
|
32
|
+
| ✅ | **Structured validation** — `ForgeValidationPipe` replaces `ValidationPipe` with typed field-level error details |
|
|
33
|
+
| 📄 | **Paginated responses** — `ApiResponseDto.paginated()` with full pagination meta in one call |
|
|
34
|
+
| 🏷️ | **Per-route decorators** — override messages, skip wrapping, add custom meta, or mark routes deprecated |
|
|
35
|
+
| 🔍 | **Request tracing** — correlation ID passthrough, auto UUID generation, and response time measurement |
|
|
36
|
+
| 💥 | **Typed exceptions** — drop-in replacements for every NestJS `HttpException` with structured error codes |
|
|
37
|
+
|
|
38
|
+
### Decorators at a glance
|
|
13
39
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
- **`@ApiForge`** — apply filter + interceptor to a single controller without going global
|
|
22
|
-
- **`ApiForgeModule.forRoot()`** — one-line global registration
|
|
23
|
-
- **`ApiForgeModule.forRootAsync()`** — config-service-driven async options
|
|
24
|
-
- **Request ID tracing** — optional UUID `requestId` in every response `meta`
|
|
40
|
+
| Decorator | Scope | Description |
|
|
41
|
+
|-----------|-------|-------------|
|
|
42
|
+
| `@ApiForge(options?)` | Controller / Method | Apply filter + interceptor without going global |
|
|
43
|
+
| `@ForgeMessage(msg)` | Controller / Method | Override the success message for that route |
|
|
44
|
+
| `@ForgeRawResponse()` | Controller / Method | Skip envelope wrapping — return raw handler value |
|
|
45
|
+
| `@ForgeMeta(extra)` | Controller / Method | Merge extra key-value pairs into `meta` |
|
|
46
|
+
| `@ForgeDeprecated(notice?)` | Controller / Method | Mark route deprecated — adds `meta.deprecated` + `Deprecation` header |
|
|
25
47
|
|
|
26
48
|
---
|
|
27
49
|
|
|
28
|
-
## Installation
|
|
50
|
+
## 📦 Installation
|
|
51
|
+
|
|
52
|
+
### Requirements
|
|
53
|
+
|
|
54
|
+
- **Node.js** v18 or higher
|
|
55
|
+
- **NestJS** v9, v10, or v11
|
|
56
|
+
|
|
57
|
+
### Install
|
|
29
58
|
|
|
30
59
|
```bash
|
|
31
60
|
npm install nestjs-api-forge
|
|
32
61
|
```
|
|
33
62
|
|
|
63
|
+
Verify the install:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
node -e "require('nestjs-api-forge'); console.log('nestjs-api-forge installed')"
|
|
67
|
+
```
|
|
68
|
+
|
|
34
69
|
---
|
|
35
70
|
|
|
36
|
-
## Quick Start
|
|
71
|
+
## 🚀 Quick Start
|
|
37
72
|
|
|
38
73
|
### 1. Register globally in `app.module.ts`
|
|
39
74
|
|
|
@@ -49,33 +84,34 @@ import { ApiForgeModule } from 'nestjs-api-forge';
|
|
|
49
84
|
includePath: true,
|
|
50
85
|
includeTimestamp: true,
|
|
51
86
|
includeRequestId: true,
|
|
87
|
+
includeResponseTime: true,
|
|
88
|
+
correlationIdHeader: 'x-request-id',
|
|
52
89
|
}),
|
|
53
90
|
],
|
|
54
91
|
})
|
|
55
92
|
export class AppModule {}
|
|
56
93
|
```
|
|
57
94
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
### 2. Add `ValidationPipe` in `main.ts`
|
|
95
|
+
### 2. Add `ForgeValidationPipe` in `main.ts`
|
|
61
96
|
|
|
62
97
|
```typescript
|
|
63
|
-
import {
|
|
64
|
-
|
|
65
|
-
app.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
98
|
+
import { NestFactory } from '@nestjs/core';
|
|
99
|
+
import { ForgeValidationPipe } from 'nestjs-api-forge';
|
|
100
|
+
import { AppModule } from './app.module';
|
|
101
|
+
|
|
102
|
+
async function bootstrap() {
|
|
103
|
+
const app = await NestFactory.create(AppModule);
|
|
104
|
+
app.useGlobalPipes(new ForgeValidationPipe());
|
|
105
|
+
await app.listen(3000);
|
|
106
|
+
}
|
|
107
|
+
bootstrap();
|
|
72
108
|
```
|
|
73
109
|
|
|
74
|
-
|
|
110
|
+
That's it. Every response is now wrapped, every error is structured, and every validation failure returns typed field details.
|
|
75
111
|
|
|
76
112
|
---
|
|
77
113
|
|
|
78
|
-
## Response Shapes
|
|
114
|
+
## 📐 Response Shapes
|
|
79
115
|
|
|
80
116
|
### Success (`2xx`)
|
|
81
117
|
|
|
@@ -86,10 +122,11 @@ The exception filter automatically parses `ValidationPipe` errors and formats th
|
|
|
86
122
|
"message": "User fetched successfully",
|
|
87
123
|
"data": { "id": 1, "name": "Alice Johnson", "email": "alice@example.com" },
|
|
88
124
|
"meta": {
|
|
89
|
-
"timestamp": "
|
|
125
|
+
"timestamp": "2025-01-15T10:00:00.000Z",
|
|
90
126
|
"path": "/api/users/1",
|
|
91
127
|
"version": "1.0.0",
|
|
92
|
-
"requestId": "a3f2c1d0-84e5-4b6a-9123-abc123def456"
|
|
128
|
+
"requestId": "a3f2c1d0-84e5-4b6a-9123-abc123def456",
|
|
129
|
+
"responseTime": "4ms"
|
|
93
130
|
}
|
|
94
131
|
}
|
|
95
132
|
```
|
|
@@ -101,19 +138,17 @@ The exception filter automatically parses `ValidationPipe` errors and formats th
|
|
|
101
138
|
"success": false,
|
|
102
139
|
"statusCode": 404,
|
|
103
140
|
"message": "User not found",
|
|
104
|
-
"error": {
|
|
105
|
-
"code": "NOT_FOUND"
|
|
106
|
-
},
|
|
141
|
+
"error": { "code": "NOT_FOUND" },
|
|
107
142
|
"meta": {
|
|
108
|
-
"timestamp": "
|
|
143
|
+
"timestamp": "2025-01-15T10:00:00.000Z",
|
|
109
144
|
"path": "/api/users/99",
|
|
110
|
-
"
|
|
111
|
-
"
|
|
145
|
+
"requestId": "b1e2f3a4-0000-4b5c-8d9e-fedcba987654",
|
|
146
|
+
"responseTime": "2ms"
|
|
112
147
|
}
|
|
113
148
|
}
|
|
114
149
|
```
|
|
115
150
|
|
|
116
|
-
### Validation Error
|
|
151
|
+
### Validation Error
|
|
117
152
|
|
|
118
153
|
```json
|
|
119
154
|
{
|
|
@@ -124,24 +159,21 @@ The exception filter automatically parses `ValidationPipe` errors and formats th
|
|
|
124
159
|
"code": "VALIDATION_ERROR",
|
|
125
160
|
"details": [
|
|
126
161
|
{ "field": "email", "message": "must be an email" },
|
|
127
|
-
{ "field": "
|
|
162
|
+
{ "field": "address.zip", "message": "must be a string" }
|
|
128
163
|
]
|
|
129
164
|
},
|
|
130
|
-
"meta": {
|
|
131
|
-
"timestamp": "2026-05-15T10:00:00.000Z",
|
|
132
|
-
"path": "/api/users"
|
|
133
|
-
}
|
|
165
|
+
"meta": { "timestamp": "2025-01-15T10:00:00.000Z", "path": "/api/users" }
|
|
134
166
|
}
|
|
135
167
|
```
|
|
136
168
|
|
|
137
|
-
### Paginated
|
|
169
|
+
### Paginated
|
|
138
170
|
|
|
139
171
|
```json
|
|
140
172
|
{
|
|
141
173
|
"success": true,
|
|
142
174
|
"statusCode": 200,
|
|
143
175
|
"message": "Users fetched successfully",
|
|
144
|
-
"data": [
|
|
176
|
+
"data": [{ "id": 1, "name": "Alice Johnson" }],
|
|
145
177
|
"pagination": {
|
|
146
178
|
"total": 42,
|
|
147
179
|
"page": 2,
|
|
@@ -151,75 +183,96 @@ The exception filter automatically parses `ValidationPipe` errors and formats th
|
|
|
151
183
|
"hasPrevPage": true
|
|
152
184
|
},
|
|
153
185
|
"meta": {
|
|
154
|
-
"timestamp": "
|
|
155
|
-
"path": "/api/users?page=2&limit=10"
|
|
186
|
+
"timestamp": "2025-01-15T10:00:00.000Z",
|
|
187
|
+
"path": "/api/users?page=2&limit=10",
|
|
188
|
+
"responseTime": "8ms"
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Deprecated Route
|
|
194
|
+
|
|
195
|
+
```json
|
|
196
|
+
{
|
|
197
|
+
"success": true,
|
|
198
|
+
"statusCode": 200,
|
|
199
|
+
"message": "OK",
|
|
200
|
+
"data": {},
|
|
201
|
+
"meta": {
|
|
202
|
+
"timestamp": "2025-01-15T10:00:00.000Z",
|
|
203
|
+
"deprecated": true,
|
|
204
|
+
"deprecationNotice": "Use /v2/users instead"
|
|
156
205
|
}
|
|
157
206
|
}
|
|
158
207
|
```
|
|
159
208
|
|
|
160
209
|
---
|
|
161
210
|
|
|
162
|
-
## API Reference
|
|
211
|
+
## 📖 API Reference
|
|
163
212
|
|
|
164
213
|
### `ApiForgeModule.forRoot(options?)`
|
|
165
214
|
|
|
166
215
|
| Option | Type | Default | Description |
|
|
167
|
-
|
|
216
|
+
|--------|------|---------|-------------|
|
|
168
217
|
| `includePath` | `boolean` | `true` | Include request path in `meta` |
|
|
169
218
|
| `includeTimestamp` | `boolean` | `true` | Include ISO timestamp in `meta` |
|
|
170
|
-
| `includeRequestId` | `boolean` | `false` |
|
|
219
|
+
| `includeRequestId` | `boolean` | `false` | Auto-generate UUID `requestId` in `meta` if no correlation header found |
|
|
220
|
+
| `correlationIdHeader` | `string \| string[]` | `['x-request-id', 'x-correlation-id']` | Header(s) to read request ID from; echoed back on the response |
|
|
221
|
+
| `includeResponseTime` | `boolean` | `false` | Include handler duration as `meta.responseTime` (e.g. `"12ms"`) |
|
|
171
222
|
| `version` | `string` | `undefined` | API version string added to `meta` |
|
|
172
223
|
| `defaultSuccessMessage` | `string` | `'Request successful'` | Fallback success message |
|
|
173
224
|
|
|
174
225
|
### `ApiForgeModule.forRootAsync(asyncOptions)`
|
|
175
226
|
|
|
227
|
+
Use when options depend on a config service:
|
|
228
|
+
|
|
176
229
|
```typescript
|
|
177
230
|
ApiForgeModule.forRootAsync({
|
|
178
231
|
imports: [ConfigModule],
|
|
179
232
|
inject: [ConfigService],
|
|
180
233
|
useFactory: (config: ConfigService) => ({
|
|
181
234
|
version: config.get('API_VERSION'),
|
|
182
|
-
defaultSuccessMessage: config.get('DEFAULT_SUCCESS_MSG'),
|
|
183
235
|
includeRequestId: true,
|
|
236
|
+
includeResponseTime: true,
|
|
237
|
+
correlationIdHeader: config.get('CORRELATION_HEADER'),
|
|
184
238
|
}),
|
|
185
239
|
})
|
|
186
240
|
```
|
|
187
241
|
|
|
188
|
-
###
|
|
242
|
+
### `ForgeValidationPipe`
|
|
189
243
|
|
|
190
|
-
|
|
191
|
-
|---|---|---|
|
|
192
|
-
| `@ApiForge(options?)` | Controller / Method | Apply filter + interceptor without going global |
|
|
193
|
-
| `@ForgeMessage(msg)` | Controller / Method | Override the success message for that route |
|
|
194
|
-
| `@ForgeRawResponse()` | Controller / Method | Skip envelope wrapping; return raw handler value |
|
|
244
|
+
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.
|
|
195
245
|
|
|
196
|
-
|
|
246
|
+
```typescript
|
|
247
|
+
// Use defaults
|
|
248
|
+
app.useGlobalPipes(new ForgeValidationPipe());
|
|
249
|
+
|
|
250
|
+
// Override defaults
|
|
251
|
+
app.useGlobalPipes(new ForgeValidationPipe({
|
|
252
|
+
whitelist: false,
|
|
253
|
+
forbidNonWhitelisted: false,
|
|
254
|
+
}));
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### `ApiResponseDto` — manual builders
|
|
197
258
|
|
|
198
259
|
```typescript
|
|
199
260
|
import { ApiResponseDto } from 'nestjs-api-forge';
|
|
200
261
|
|
|
201
|
-
|
|
202
|
-
ApiResponseDto.success(data, 'User fetched', 200, { path: '/users/1' });
|
|
203
|
-
|
|
204
|
-
// Created (201)
|
|
262
|
+
ApiResponseDto.success(data, 'OK', 200, { path: '/users/1' });
|
|
205
263
|
ApiResponseDto.created(data, 'User created');
|
|
206
|
-
|
|
207
|
-
//
|
|
208
|
-
ApiResponseDto.noContent('Deleted');
|
|
209
|
-
|
|
210
|
-
// Paginated
|
|
264
|
+
ApiResponseDto.accepted(jobRef, 'Export queued'); // 202
|
|
265
|
+
ApiResponseDto.noContent('Deleted'); // 204
|
|
211
266
|
ApiResponseDto.paginated(data, total, page, limit, 'Users fetched');
|
|
212
|
-
|
|
213
|
-
// Error
|
|
214
267
|
ApiResponseDto.error('Not found', 404, { code: 'NOT_FOUND' });
|
|
215
268
|
```
|
|
216
269
|
|
|
217
270
|
### Built-in Exceptions
|
|
218
271
|
|
|
219
|
-
All
|
|
272
|
+
All extend `ApiException` → `HttpException`. Pass an optional message and `details` array to any of them.
|
|
220
273
|
|
|
221
274
|
| Class | Status | Code |
|
|
222
|
-
|
|
275
|
+
|-------|--------|------|
|
|
223
276
|
| `BadRequestException` | 400 | `BAD_REQUEST` |
|
|
224
277
|
| `UnauthorizedException` | 401 | `UNAUTHORIZED` |
|
|
225
278
|
| `PaymentRequiredException` | 402 | `PAYMENT_REQUIRED` |
|
|
@@ -236,23 +289,21 @@ All exceptions extend `ApiException` → `HttpException` and produce a structure
|
|
|
236
289
|
|
|
237
290
|
---
|
|
238
291
|
|
|
239
|
-
## Usage Examples
|
|
292
|
+
## 💡 Usage Examples
|
|
240
293
|
|
|
241
294
|
### Standard CRUD controller
|
|
242
295
|
|
|
243
296
|
```typescript
|
|
244
|
-
import { Controller, Get,
|
|
245
|
-
import { ForgeMessage,
|
|
297
|
+
import { Controller, Get, Delete, Param, HttpCode, HttpStatus } from '@nestjs/common';
|
|
298
|
+
import { ForgeMessage, NotFoundException } from 'nestjs-api-forge';
|
|
246
299
|
|
|
247
300
|
@Controller('users')
|
|
248
301
|
export class UsersController {
|
|
249
|
-
constructor(private readonly usersService: UsersService) {}
|
|
250
|
-
|
|
251
302
|
@Get(':id')
|
|
252
303
|
@ForgeMessage('User fetched successfully')
|
|
253
304
|
findOne(@Param('id') id: number) {
|
|
254
305
|
const user = this.usersService.findById(id);
|
|
255
|
-
if (!user) throw new NotFoundException('User');
|
|
306
|
+
if (!user) throw new NotFoundException('User not found');
|
|
256
307
|
return user;
|
|
257
308
|
}
|
|
258
309
|
|
|
@@ -264,11 +315,9 @@ export class UsersController {
|
|
|
264
315
|
}
|
|
265
316
|
```
|
|
266
317
|
|
|
267
|
-
### Paginated list
|
|
318
|
+
### Paginated list
|
|
268
319
|
|
|
269
320
|
```typescript
|
|
270
|
-
import { ApiResponseDto, ForgeRawResponse } from 'nestjs-api-forge';
|
|
271
|
-
|
|
272
321
|
@Get()
|
|
273
322
|
@ForgeRawResponse()
|
|
274
323
|
findAll(@Query('page') page = '1', @Query('limit') limit = '10') {
|
|
@@ -279,22 +328,36 @@ findAll(@Query('page') page = '1', @Query('limit') limit = '10') {
|
|
|
279
328
|
}
|
|
280
329
|
```
|
|
281
330
|
|
|
282
|
-
###
|
|
331
|
+
### Deprecated route
|
|
283
332
|
|
|
284
333
|
```typescript
|
|
285
|
-
|
|
334
|
+
@Get('export')
|
|
335
|
+
@ForgeDeprecated('Use POST /v2/users/export instead')
|
|
336
|
+
@ForgeMessage('Export started')
|
|
337
|
+
startExport() {
|
|
338
|
+
const job = this.usersService.queueExport();
|
|
339
|
+
return ApiResponseDto.accepted({ jobId: job.id }, 'Export queued');
|
|
340
|
+
}
|
|
341
|
+
```
|
|
286
342
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
}
|
|
343
|
+
### Custom meta per route
|
|
344
|
+
|
|
345
|
+
```typescript
|
|
346
|
+
@Get()
|
|
347
|
+
@ForgeMeta({ region: 'us-east-1', cache: 'miss' })
|
|
348
|
+
findAll() {
|
|
349
|
+
return this.usersService.findAll();
|
|
295
350
|
}
|
|
296
351
|
```
|
|
297
352
|
|
|
353
|
+
### Per-controller scope (no global module)
|
|
354
|
+
|
|
355
|
+
```typescript
|
|
356
|
+
@Controller('products')
|
|
357
|
+
@ApiForge({ version: '2.0', includeResponseTime: true })
|
|
358
|
+
export class ProductsController { ... }
|
|
359
|
+
```
|
|
360
|
+
|
|
298
361
|
### Raw response (health check)
|
|
299
362
|
|
|
300
363
|
```typescript
|
|
@@ -305,17 +368,25 @@ health() {
|
|
|
305
368
|
}
|
|
306
369
|
```
|
|
307
370
|
|
|
308
|
-
###
|
|
371
|
+
### Correlation ID tracing
|
|
372
|
+
|
|
373
|
+
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.
|
|
309
374
|
|
|
310
375
|
```typescript
|
|
311
|
-
|
|
376
|
+
ApiForgeModule.forRoot({
|
|
377
|
+
correlationIdHeader: 'x-request-id', // or an array of headers
|
|
378
|
+
includeRequestId: true, // generate UUID if header is absent
|
|
379
|
+
})
|
|
380
|
+
```
|
|
312
381
|
|
|
313
|
-
|
|
382
|
+
### Custom exception with field details
|
|
383
|
+
|
|
384
|
+
```typescript
|
|
314
385
|
throw new BadRequestException('Invalid input', [
|
|
315
386
|
{ field: 'price', message: 'Must be a positive number', value: -5 },
|
|
316
387
|
]);
|
|
317
388
|
|
|
318
|
-
// From class-validator
|
|
389
|
+
// From class-validator constraint map
|
|
319
390
|
throw ValidationException.fromConstraints({
|
|
320
391
|
email: { isEmail: 'must be an email' },
|
|
321
392
|
age: { min: 'must be at least 1' },
|
|
@@ -324,17 +395,131 @@ throw ValidationException.fromConstraints({
|
|
|
324
395
|
|
|
325
396
|
---
|
|
326
397
|
|
|
327
|
-
##
|
|
398
|
+
## 📁 Project Structure
|
|
328
399
|
|
|
329
400
|
```
|
|
330
|
-
|
|
331
|
-
|
|
401
|
+
nestjs-api-forge/
|
|
402
|
+
├── src/
|
|
403
|
+
│ ├── api-forge.module.ts # forRoot / forRootAsync registration
|
|
404
|
+
│ ├── index.ts # Public exports
|
|
405
|
+
│ ├── decorators/
|
|
406
|
+
│ │ └── api-response.decorator.ts # @ApiForge, @ForgeMessage, @ForgeRawResponse, @ForgeMeta, @ForgeDeprecated
|
|
407
|
+
│ ├── dto/
|
|
408
|
+
│ │ └── api-response.dto.ts # ApiResponseDto static builders
|
|
409
|
+
│ ├── exceptions/
|
|
410
|
+
│ │ ├── api.exception.ts # Base ApiException class
|
|
411
|
+
│ │ └── validation.exception.ts # ValidationException with field details
|
|
412
|
+
│ ├── filters/
|
|
413
|
+
│ │ └── global-exception.filter.ts # ForgeExceptionFilter
|
|
414
|
+
│ ├── interceptors/
|
|
415
|
+
│ │ └── response.interceptor.ts # ForgeResponseInterceptor
|
|
416
|
+
│ ├── interfaces/
|
|
417
|
+
│ │ └── api-response.interface.ts # TypeScript interfaces and ForgeOptions
|
|
418
|
+
│ ├── pipes/
|
|
419
|
+
│ │ └── forge-validation.pipe.ts # ForgeValidationPipe
|
|
420
|
+
│ └── utils/
|
|
421
|
+
│ └── error-code.util.ts
|
|
422
|
+
├── app/ # Example NestJS app demonstrating the library
|
|
423
|
+
│ └── src/
|
|
424
|
+
│ ├── users/ # Full CRUD example with pagination
|
|
425
|
+
│ └── products/ # Per-controller @ApiForge example
|
|
426
|
+
├── .github/
|
|
427
|
+
│ ├── ISSUE_TEMPLATE/ # Bug report & feature request templates
|
|
428
|
+
│ └── workflows/
|
|
429
|
+
│ ├── publish.yml # npm publish on tag push
|
|
430
|
+
│ └── malware-scan.yml # Security scan on every push
|
|
431
|
+
├── CONTRIBUTING.md
|
|
432
|
+
├── CODE_OF_CONDUCT.md
|
|
433
|
+
└── package.json
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
---
|
|
437
|
+
|
|
438
|
+
## 🛠️ Development
|
|
439
|
+
|
|
440
|
+
```bash
|
|
441
|
+
# Clone the repo
|
|
442
|
+
git clone https://github.com/mirzasaikatahmmed/nestjs-api-forge.git
|
|
443
|
+
cd nestjs-api-forge
|
|
444
|
+
|
|
445
|
+
# Install dependencies
|
|
446
|
+
npm install
|
|
447
|
+
|
|
448
|
+
# Build the library
|
|
449
|
+
npm run build
|
|
450
|
+
|
|
451
|
+
# Watch mode
|
|
452
|
+
npm run build:watch
|
|
453
|
+
|
|
454
|
+
# Format code
|
|
455
|
+
npm run format
|
|
456
|
+
|
|
457
|
+
# Run the example app
|
|
458
|
+
cd app && npm install && npm run start:dev
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
---
|
|
462
|
+
|
|
463
|
+
## 📦 Peer Dependencies
|
|
464
|
+
|
|
465
|
+
```
|
|
466
|
+
@nestjs/common ^9 | ^10 | ^11
|
|
467
|
+
@nestjs/core ^9 | ^10 | ^11
|
|
332
468
|
reflect-metadata ^0.1 | ^0.2
|
|
333
|
-
rxjs
|
|
469
|
+
rxjs ^7
|
|
334
470
|
```
|
|
335
471
|
|
|
336
472
|
---
|
|
337
473
|
|
|
338
|
-
##
|
|
474
|
+
## 📋 Changelog
|
|
475
|
+
|
|
476
|
+
### v1.1.0
|
|
477
|
+
- `ForgeValidationPipe` — structured validation errors with nested field support (dot-notation)
|
|
478
|
+
- `@ForgeMeta(extra)` — merge custom fields into response `meta` per route or controller
|
|
479
|
+
- `@ForgeDeprecated(notice?)` — deprecation flag in `meta` + `Deprecation: true` response header
|
|
480
|
+
- `ApiResponseDto.accepted()` — 202 Accepted helper
|
|
481
|
+
- Correlation ID passthrough (`correlationIdHeader` option)
|
|
482
|
+
- Response time measurement (`includeResponseTime` option)
|
|
483
|
+
- `forRootAsync` fix — options factory now runs once instead of twice
|
|
484
|
+
- 3 new exceptions: `MethodNotAllowedException`, `PaymentRequiredException`, `GatewayTimeoutException`
|
|
485
|
+
|
|
486
|
+
### v1.0.4
|
|
487
|
+
- Decorators: `@ForgeMessage`, `@ForgeRawResponse`, `@ForgeMeta`, `@ForgeDeprecated`, `@ApiForge`
|
|
488
|
+
- `ForgeValidationPipe` with structured field-level errors
|
|
489
|
+
- Response metadata support (`includePath`, `includeTimestamp`, `includeRequestId`, `includeResponseTime`)
|
|
490
|
+
|
|
491
|
+
### v1.0.3
|
|
492
|
+
- Bump version, update repository URL
|
|
493
|
+
|
|
494
|
+
### v1.0.0
|
|
495
|
+
- Initial release — `ApiForgeModule.forRoot()`, `ForgeExceptionFilter`, `ForgeResponseInterceptor`, `ApiResponseDto`, and 10 typed exceptions
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
## 🤝 Contributing
|
|
500
|
+
|
|
501
|
+
Pull requests are welcome! For major changes, please open an issue first to discuss your approach.
|
|
502
|
+
|
|
503
|
+
1. Fork the repository
|
|
504
|
+
2. Create your branch: `git checkout -b feat/your-feature`
|
|
505
|
+
3. Make your changes and run `npm run format`
|
|
506
|
+
4. Push and open a PR against `main`
|
|
507
|
+
|
|
508
|
+
Please read [CONTRIBUTING.md](CONTRIBUTING.md) for full guidelines and [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) before participating. All contributors are listed in [CONTRIBUTORS.md](CONTRIBUTORS.md).
|
|
509
|
+
|
|
510
|
+
---
|
|
511
|
+
|
|
512
|
+
## 💛 Support
|
|
513
|
+
|
|
514
|
+
If NestJS API Forge saves you boilerplate, consider supporting the project:
|
|
515
|
+
|
|
516
|
+
[](https://github.com/sponsors/mirzasaikatahmmed)
|
|
517
|
+
[](https://buymeacoffee.com/saikat)
|
|
518
|
+
|
|
519
|
+
---
|
|
520
|
+
|
|
521
|
+
<div align="center">
|
|
522
|
+
|
|
523
|
+
Made with ❤️ by [Mirza Saikat Ahmmed](https://github.com/mirzasaikatahmmed)
|
|
339
524
|
|
|
340
|
-
|
|
525
|
+
</div>
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { ForgeOptions } from '../interfaces/api-response.interface';
|
|
2
2
|
export declare const FORGE_MESSAGE_KEY = "forge:response_message";
|
|
3
3
|
export declare const FORGE_RAW_RESPONSE_KEY = "forge:raw_response";
|
|
4
|
+
export declare const FORGE_META_KEY = "forge:extra_meta";
|
|
5
|
+
export declare const FORGE_DEPRECATED_KEY = "forge:deprecated";
|
|
4
6
|
export declare const ForgeMessage: (message: string) => import("@nestjs/common").CustomDecorator<string>;
|
|
5
7
|
export declare const ForgeRawResponse: () => import("@nestjs/common").CustomDecorator<string>;
|
|
8
|
+
export declare const ForgeMeta: (extra: Record<string, unknown>) => import("@nestjs/common").CustomDecorator<string>;
|
|
9
|
+
export declare const ForgeDeprecated: (notice?: string) => import("@nestjs/common").CustomDecorator<string>;
|
|
6
10
|
export declare function ApiForge(options?: ForgeOptions): <TFunction extends Function, Y>(target: TFunction | object, propertyKey?: string | symbol, descriptor?: TypedPropertyDescriptor<Y>) => void;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ForgeRawResponse = exports.ForgeMessage = exports.FORGE_RAW_RESPONSE_KEY = exports.FORGE_MESSAGE_KEY = void 0;
|
|
3
|
+
exports.ForgeDeprecated = exports.ForgeMeta = exports.ForgeRawResponse = exports.ForgeMessage = exports.FORGE_DEPRECATED_KEY = exports.FORGE_META_KEY = exports.FORGE_RAW_RESPONSE_KEY = exports.FORGE_MESSAGE_KEY = void 0;
|
|
4
4
|
exports.ApiForge = ApiForge;
|
|
5
5
|
const common_1 = require("@nestjs/common");
|
|
6
6
|
const core_1 = require("@nestjs/core");
|
|
@@ -8,10 +8,16 @@ const response_interceptor_1 = require("../interceptors/response.interceptor");
|
|
|
8
8
|
const global_exception_filter_1 = require("../filters/global-exception.filter");
|
|
9
9
|
exports.FORGE_MESSAGE_KEY = 'forge:response_message';
|
|
10
10
|
exports.FORGE_RAW_RESPONSE_KEY = 'forge:raw_response';
|
|
11
|
+
exports.FORGE_META_KEY = 'forge:extra_meta';
|
|
12
|
+
exports.FORGE_DEPRECATED_KEY = 'forge:deprecated';
|
|
11
13
|
const ForgeMessage = (message) => (0, common_1.SetMetadata)(exports.FORGE_MESSAGE_KEY, message);
|
|
12
14
|
exports.ForgeMessage = ForgeMessage;
|
|
13
15
|
const ForgeRawResponse = () => (0, common_1.SetMetadata)(exports.FORGE_RAW_RESPONSE_KEY, true);
|
|
14
16
|
exports.ForgeRawResponse = ForgeRawResponse;
|
|
17
|
+
const ForgeMeta = (extra) => (0, common_1.SetMetadata)(exports.FORGE_META_KEY, extra);
|
|
18
|
+
exports.ForgeMeta = ForgeMeta;
|
|
19
|
+
const ForgeDeprecated = (notice) => (0, common_1.SetMetadata)(exports.FORGE_DEPRECATED_KEY, notice ?? true);
|
|
20
|
+
exports.ForgeDeprecated = ForgeDeprecated;
|
|
15
21
|
function ApiForge(options = {}) {
|
|
16
22
|
const reflector = new core_1.Reflector();
|
|
17
23
|
return (0, common_1.applyDecorators)((0, common_1.UseInterceptors)(new response_interceptor_1.ForgeResponseInterceptor(reflector, options)), (0, common_1.UseFilters)(new global_exception_filter_1.ForgeExceptionFilter(options)));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-response.decorator.js","sourceRoot":"","sources":["../../src/decorators/api-response.decorator.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"api-response.decorator.js","sourceRoot":"","sources":["../../src/decorators/api-response.decorator.ts"],"names":[],"mappings":";;;AA0CA,4BAMC;AAhDD,2CAA2F;AAC3F,uCAAyC;AACzC,+EAAgF;AAChF,gFAA0E;AAG7D,QAAA,iBAAiB,GAAG,wBAAwB,CAAC;AAC7C,QAAA,sBAAsB,GAAG,oBAAoB,CAAC;AAC9C,QAAA,cAAc,GAAG,kBAAkB,CAAC;AACpC,QAAA,oBAAoB,GAAG,kBAAkB,CAAC;AAGhD,MAAM,YAAY,GAAG,CAAC,OAAe,EAAE,EAAE,CAC9C,IAAA,oBAAW,EAAC,yBAAiB,EAAE,OAAO,CAAC,CAAC;AAD7B,QAAA,YAAY,gBACiB;AAGnC,MAAM,gBAAgB,GAAG,GAAG,EAAE,CAAC,IAAA,oBAAW,EAAC,8BAAsB,EAAE,IAAI,CAAC,CAAC;AAAnE,QAAA,gBAAgB,oBAAmD;AAQzE,MAAM,SAAS,GAAG,CAAC,KAA8B,EAAE,EAAE,CAC1D,IAAA,oBAAW,EAAC,sBAAc,EAAE,KAAK,CAAC,CAAC;AADxB,QAAA,SAAS,aACe;AAU9B,MAAM,eAAe,GAAG,CAAC,MAAe,EAAE,EAAE,CACjD,IAAA,oBAAW,EAAC,4BAAoB,EAAE,MAAM,IAAI,IAAI,CAAC,CAAC;AADvC,QAAA,eAAe,mBACwB;AAMpD,SAAgB,QAAQ,CAAC,UAAwB,EAAE;IACjD,MAAM,SAAS,GAAG,IAAI,gBAAS,EAAE,CAAC;IAClC,OAAO,IAAA,wBAAe,EACpB,IAAA,wBAAe,EAAC,IAAI,+CAAwB,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC,EACjE,IAAA,mBAAU,EAAC,IAAI,8CAAoB,CAAC,OAAO,CAAC,CAAC,CAC9C,CAAC;AACJ,CAAC"}
|
|
@@ -2,6 +2,7 @@ import { ApiErrorPayload, ApiMeta, ApiSuccessResponse, ApiErrorResponse, ApiPagi
|
|
|
2
2
|
export declare class ApiResponseDto {
|
|
3
3
|
static success<T>(data: T, message?: string, statusCode?: number, meta?: Partial<ApiMeta>): ApiSuccessResponse<T>;
|
|
4
4
|
static created<T>(data: T, message?: string, meta?: Partial<ApiMeta>): ApiSuccessResponse<T>;
|
|
5
|
+
static accepted<T>(data: T, message?: string, meta?: Partial<ApiMeta>): ApiSuccessResponse<T>;
|
|
5
6
|
static noContent(message?: string, meta?: Partial<ApiMeta>): ApiSuccessResponse<null>;
|
|
6
7
|
static paginated<T>(data: T[], total: number, page: number, limit: number, message?: string, meta?: Partial<ApiMeta>): ApiPaginatedResponse<T>;
|
|
7
8
|
static error(message: string, statusCode: number, error: ApiErrorPayload, meta?: Partial<ApiMeta>): ApiErrorResponse;
|
|
@@ -17,6 +17,9 @@ class ApiResponseDto {
|
|
|
17
17
|
static created(data, message = 'Resource created successfully', meta = {}) {
|
|
18
18
|
return ApiResponseDto.success(data, message, 201, meta);
|
|
19
19
|
}
|
|
20
|
+
static accepted(data, message = 'Request accepted for processing', meta = {}) {
|
|
21
|
+
return ApiResponseDto.success(data, message, 202, meta);
|
|
22
|
+
}
|
|
20
23
|
static noContent(message = 'No content', meta = {}) {
|
|
21
24
|
return ApiResponseDto.success(null, message, 204, meta);
|
|
22
25
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-response.dto.js","sourceRoot":"","sources":["../../src/dto/api-response.dto.ts"],"names":[],"mappings":";;;AASA,MAAa,cAAc;IACzB,MAAM,CAAC,OAAO,CACZ,IAAO,EACP,OAAO,GAAG,oBAAoB,EAC9B,UAAU,GAAG,GAAG,EAChB,OAAyB,EAAE;QAE3B,OAAO;YACL,OAAO,EAAE,IAAI;YACb,UAAU;YACV,OAAO;YACP,IAAI;YACJ,IAAI,EAAE;gBACJ,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;gBACnC,GAAG,IAAI;aACR;SACF,CAAC;IACJ,CAAC;IAED,MAAM,CAAC,OAAO,CACZ,IAAO,EACP,OAAO,GAAG,+BAA+B,EACzC,OAAyB,EAAE;QAE3B,OAAO,cAAc,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;IAC1D,CAAC;IAED,MAAM,CAAC,SAAS,CACd,OAAO,GAAG,YAAY,EACtB,OAAyB,EAAE;QAE3B,OAAO,cAAc,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;IAC1D,CAAC;IAED,MAAM,CAAC,SAAS,CACd,IAAS,EACT,KAAa,EACb,IAAY,EACZ,KAAa,EACb,OAAO,GAAG,2BAA2B,EACrC,OAAyB,EAAE;QAE3B,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC;QAC5C,MAAM,UAAU,GAAmB;YACjC,KAAK;YACL,IAAI;YACJ,KAAK;YACL,UAAU;YACV,WAAW,EAAE,IAAI,GAAG,UAAU;YAC9B,WAAW,EAAE,IAAI,GAAG,CAAC;SACtB,CAAC;QAEF,OAAO;YACL,OAAO,EAAE,IAAI;YACb,UAAU,EAAE,GAAG;YACf,OAAO;YACP,IAAI;YACJ,UAAU;YACV,IAAI,EAAE;gBACJ,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;gBACnC,GAAG,IAAI;aACR;SACF,CAAC;IACJ,CAAC;IAED,MAAM,CAAC,KAAK,CACV,OAAe,EACf,UAAkB,EAClB,KAAsB,EACtB,OAAyB,EAAE;QAE3B,OAAO;YACL,OAAO,EAAE,KAAK;YACd,UAAU;YACV,OAAO;YACP,KAAK;YACL,IAAI,EAAE;gBACJ,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;gBACnC,GAAG,IAAI;aACR;SACF,CAAC;IACJ,CAAC;CACF;
|
|
1
|
+
{"version":3,"file":"api-response.dto.js","sourceRoot":"","sources":["../../src/dto/api-response.dto.ts"],"names":[],"mappings":";;;AASA,MAAa,cAAc;IACzB,MAAM,CAAC,OAAO,CACZ,IAAO,EACP,OAAO,GAAG,oBAAoB,EAC9B,UAAU,GAAG,GAAG,EAChB,OAAyB,EAAE;QAE3B,OAAO;YACL,OAAO,EAAE,IAAI;YACb,UAAU;YACV,OAAO;YACP,IAAI;YACJ,IAAI,EAAE;gBACJ,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;gBACnC,GAAG,IAAI;aACR;SACF,CAAC;IACJ,CAAC;IAED,MAAM,CAAC,OAAO,CACZ,IAAO,EACP,OAAO,GAAG,+BAA+B,EACzC,OAAyB,EAAE;QAE3B,OAAO,cAAc,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;IAC1D,CAAC;IAED,MAAM,CAAC,QAAQ,CACb,IAAO,EACP,OAAO,GAAG,iCAAiC,EAC3C,OAAyB,EAAE;QAE3B,OAAO,cAAc,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;IAC1D,CAAC;IAED,MAAM,CAAC,SAAS,CACd,OAAO,GAAG,YAAY,EACtB,OAAyB,EAAE;QAE3B,OAAO,cAAc,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;IAC1D,CAAC;IAED,MAAM,CAAC,SAAS,CACd,IAAS,EACT,KAAa,EACb,IAAY,EACZ,KAAa,EACb,OAAO,GAAG,2BAA2B,EACrC,OAAyB,EAAE;QAE3B,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC;QAC5C,MAAM,UAAU,GAAmB;YACjC,KAAK;YACL,IAAI;YACJ,KAAK;YACL,UAAU;YACV,WAAW,EAAE,IAAI,GAAG,UAAU;YAC9B,WAAW,EAAE,IAAI,GAAG,CAAC;SACtB,CAAC;QAEF,OAAO;YACL,OAAO,EAAE,IAAI;YACb,UAAU,EAAE,GAAG;YACf,OAAO;YACP,IAAI;YACJ,UAAU;YACV,IAAI,EAAE;gBACJ,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;gBACnC,GAAG,IAAI;aACR;SACF,CAAC;IACJ,CAAC;IAED,MAAM,CAAC,KAAK,CACV,OAAe,EACf,UAAkB,EAClB,KAAsB,EACtB,OAAyB,EAAE;QAE3B,OAAO;YACL,OAAO,EAAE,KAAK;YACd,UAAU;YACV,OAAO;YACP,KAAK;YACL,IAAI,EAAE;gBACJ,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;gBACnC,GAAG,IAAI;aACR;SACF,CAAC;IACJ,CAAC;CACF;AA1FD,wCA0FC"}
|