@tongei/express-response-envelop 0.0.1
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 +480 -0
- package/dist/builder/envelope.builder.d.ts +30 -0
- package/dist/builder/envelope.builder.js +33 -0
- package/dist/builder/envelope.builder.js.map +1 -0
- package/dist/error-handler/envelop-error-handler.d.ts +7 -0
- package/dist/error-handler/envelop-error-handler.js +41 -0
- package/dist/error-handler/envelop-error-handler.js.map +1 -0
- package/dist/exceptions/built-in.exceptions.d.ts +58 -0
- package/dist/exceptions/built-in.exceptions.js +119 -0
- package/dist/exceptions/built-in.exceptions.js.map +1 -0
- package/dist/exceptions/http.exception.d.ts +5 -0
- package/dist/exceptions/http.exception.js +18 -0
- package/dist/exceptions/http.exception.js.map +1 -0
- package/dist/exceptions/index.d.ts +2 -0
- package/dist/exceptions/index.js +19 -0
- package/dist/exceptions/index.js.map +1 -0
- package/dist/filters/exception-filter.type.d.ts +5 -0
- package/dist/filters/exception-filter.type.js +3 -0
- package/dist/filters/exception-filter.type.js.map +1 -0
- package/dist/helpers/send-envelop.d.ts +8 -0
- package/dist/helpers/send-envelop.js +27 -0
- package/dist/helpers/send-envelop.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/response-envelop.middleware.d.ts +3 -0
- package/dist/middleware/response-envelop.middleware.js +26 -0
- package/dist/middleware/response-envelop.middleware.js.map +1 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/dist/types/response-envelop-option.type.d.ts +10 -0
- package/dist/types/response-envelop-option.type.js +3 -0
- package/dist/types/response-envelop-option.type.js.map +1 -0
- package/package.json +73 -0
package/README.md
ADDED
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
# @tongei/express-response-envelop
|
|
2
|
+
|
|
3
|
+
A lightweight Express.js library for centralizing and standardizing API responses with a consistent envelope structure.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @tongei/express-response-envelop
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Response Shape
|
|
12
|
+
|
|
13
|
+
**Success:**
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"success": true,
|
|
17
|
+
"message": "Users fetched successfully",
|
|
18
|
+
"data": [...],
|
|
19
|
+
"optional": {
|
|
20
|
+
"apiVersion": "1.0.0",
|
|
21
|
+
"timestamp": "2025-01-01T00:00:00.000Z"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**Error:**
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"success": false,
|
|
30
|
+
"message": null,
|
|
31
|
+
"error": {
|
|
32
|
+
"status": 404,
|
|
33
|
+
"message": "User not found",
|
|
34
|
+
"details": { "field": "id" }
|
|
35
|
+
},
|
|
36
|
+
"optional": {
|
|
37
|
+
"apiVersion": "1.0.0",
|
|
38
|
+
"timestamp": "2025-01-01T00:00:00.000Z"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Usage
|
|
46
|
+
|
|
47
|
+
There are two ways to use this library. You can use them together or independently.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
### Option A — Global Middleware (Opt-out model)
|
|
52
|
+
|
|
53
|
+
Register the middleware once and **every `res.json()` call** is automatically wrapped in a success envelope.
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import express from 'express';
|
|
57
|
+
import {
|
|
58
|
+
responseEnvelopMiddleware,
|
|
59
|
+
envelopErrorHandler,
|
|
60
|
+
} from '@tongei/express-response-envelop';
|
|
61
|
+
|
|
62
|
+
const app = express();
|
|
63
|
+
app.use(express.json());
|
|
64
|
+
|
|
65
|
+
// 1. Register the global response middleware
|
|
66
|
+
app.use(
|
|
67
|
+
responseEnvelopMiddleware({
|
|
68
|
+
defaultApiVersion: '1.0.0', // optional
|
|
69
|
+
includeTimestamp: true, // default: true
|
|
70
|
+
}),
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// Your routes — no changes needed
|
|
74
|
+
app.get('/users', (req, res) => {
|
|
75
|
+
res.json([{ id: 1, name: 'Alice' }]);
|
|
76
|
+
// Response: { success: true, message: null, data: [...], optional: { ... } }
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// 2. Register the error handler LAST (after all routes)
|
|
80
|
+
app.use(
|
|
81
|
+
envelopErrorHandler({
|
|
82
|
+
defaultApiVersion: '1.0.0',
|
|
83
|
+
}),
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
app.listen(3000);
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
#### Opt-out per route
|
|
90
|
+
|
|
91
|
+
If a specific route should skip the envelope (e.g., a health-check or file download), set `res.locals.skipEnvelop = true` before calling `res.json()`:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
app.get('/health', (req, res) => {
|
|
95
|
+
res.locals.skipEnvelop = true;
|
|
96
|
+
res.json({ status: 'ok' }); // returned as-is, not wrapped
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
### Option B — Route-level Helpers (Explicit / Opt-in model)
|
|
103
|
+
|
|
104
|
+
Use the `sendEnvelop` and `sendErrorEnvelop` helpers for fine-grained control on specific routes, without using the global middleware.
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import express from 'express';
|
|
108
|
+
import {
|
|
109
|
+
sendEnvelop,
|
|
110
|
+
sendErrorEnvelop,
|
|
111
|
+
envelopErrorHandler,
|
|
112
|
+
} from '@tongei/express-response-envelop';
|
|
113
|
+
|
|
114
|
+
const app = express();
|
|
115
|
+
app.use(express.json());
|
|
116
|
+
|
|
117
|
+
// Success — static message
|
|
118
|
+
app.get('/users', (req, res) => {
|
|
119
|
+
const users = [{ id: 1, name: 'Alice' }];
|
|
120
|
+
sendEnvelop(res, users, { message: 'Users fetched successfully' });
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Success — dynamic message based on the data
|
|
124
|
+
app.get('/users/:id', (req, res) => {
|
|
125
|
+
const user = { id: 1, name: 'Alice' };
|
|
126
|
+
sendEnvelop(res, user, {
|
|
127
|
+
message: (data) => `User ${data.name} retrieved`,
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Success — with custom status and API version
|
|
132
|
+
app.post('/users', (req, res) => {
|
|
133
|
+
const newUser = { id: 2, name: 'Bob' };
|
|
134
|
+
sendEnvelop(res, newUser, {
|
|
135
|
+
status: 201,
|
|
136
|
+
message: 'User created',
|
|
137
|
+
apiVersion: '1.0.0',
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Error — explicit
|
|
142
|
+
app.get('/users/:id', (req, res) => {
|
|
143
|
+
sendErrorEnvelop(res, 404, 'User not found');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// Error — with details (e.g. validation)
|
|
147
|
+
app.post('/users', (req, res) => {
|
|
148
|
+
sendErrorEnvelop(res, 400, 'Validation failed', {
|
|
149
|
+
details: [{ field: 'email', message: 'Required' }],
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Register error handler last
|
|
154
|
+
app.use(envelopErrorHandler());
|
|
155
|
+
|
|
156
|
+
app.listen(3000);
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
### Mixing Both Approaches
|
|
162
|
+
|
|
163
|
+
You can use the global middleware AND the route-level helpers together. The helpers automatically set `res.locals.skipEnvelop = true` to prevent double-wrapping.
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
app.use(responseEnvelopMiddleware({ defaultApiVersion: '1.0.0' }));
|
|
167
|
+
|
|
168
|
+
// This route uses the global middleware — res.json is auto-wrapped
|
|
169
|
+
app.get('/products', (req, res) => {
|
|
170
|
+
res.json([{ id: 1 }]);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// This route uses explicit helpers — no double-wrap
|
|
174
|
+
app.post('/products', (req, res) => {
|
|
175
|
+
sendEnvelop(res, { id: 2 }, { status: 201, message: 'Created' });
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
app.use(envelopErrorHandler({ defaultApiVersion: '1.0.0' }));
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## API Reference
|
|
184
|
+
|
|
185
|
+
### `responseEnvelopMiddleware(options?)`
|
|
186
|
+
|
|
187
|
+
| Option | Type | Default | Description |
|
|
188
|
+
|---|---|---|---|
|
|
189
|
+
| `defaultApiVersion` | `string` | `undefined` | Included in `optional.apiVersion` of every response |
|
|
190
|
+
| `includeTimestamp` | `boolean` | `true` | Whether to include `optional.timestamp` |
|
|
191
|
+
|
|
192
|
+
### `envelopErrorHandler(options?)`
|
|
193
|
+
|
|
194
|
+
Same options as `responseEnvelopMiddleware`. Must be the **last** middleware registered.
|
|
195
|
+
|
|
196
|
+
Automatically detects `err.status`, `err.statusCode`, `err.errors`, and `err.details`.
|
|
197
|
+
|
|
198
|
+
### `sendEnvelop(res, data, options?)`
|
|
199
|
+
|
|
200
|
+
| Option | Type | Description |
|
|
201
|
+
|---|---|---|
|
|
202
|
+
| `message` | `string \| (data) => string` | Static or dynamic message |
|
|
203
|
+
| `status` | `number` | HTTP status code (default: `200`) |
|
|
204
|
+
| `apiVersion` | `string` | Overrides the `optional.apiVersion` |
|
|
205
|
+
| `includeTimestamp` | `boolean` | Whether to include timestamp |
|
|
206
|
+
|
|
207
|
+
### `sendErrorEnvelop(res, status, message, options?)`
|
|
208
|
+
|
|
209
|
+
| Option | Type | Description |
|
|
210
|
+
|---|---|---|
|
|
211
|
+
| `details` | `unknown` | Additional error details (e.g. validation errors) |
|
|
212
|
+
| `apiVersion` | `string` | Overrides the `optional.apiVersion` |
|
|
213
|
+
| `includeTimestamp` | `boolean` | Whether to include timestamp |
|
|
214
|
+
|
|
215
|
+
### Builder utilities (low-level)
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
import {
|
|
219
|
+
buildSuccessEnvelope,
|
|
220
|
+
buildErrorEnvelope,
|
|
221
|
+
} from '@tongei/express-response-envelop';
|
|
222
|
+
|
|
223
|
+
const success = buildSuccessEnvelope(data, { message: 'Done', apiVersion: '1.0' });
|
|
224
|
+
const error = buildErrorEnvelope(500, 'Internal error', { details: { ... } });
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## NestJS-style Exceptions
|
|
230
|
+
|
|
231
|
+
The package ships typed HTTP exception classes — exactly like NestJS — so you can `throw` them directly from any route handler, service, or middleware. The `envelopErrorHandler` catches them automatically.
|
|
232
|
+
|
|
233
|
+
### Available exceptions
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
import {
|
|
237
|
+
// 4xx
|
|
238
|
+
BadRequestException, // 400
|
|
239
|
+
UnauthorizedException, // 401
|
|
240
|
+
PaymentRequiredException, // 402
|
|
241
|
+
ForbiddenException, // 403
|
|
242
|
+
NotFoundException, // 404
|
|
243
|
+
MethodNotAllowedException, // 405
|
|
244
|
+
NotAcceptableException, // 406
|
|
245
|
+
RequestTimeoutException, // 408
|
|
246
|
+
ConflictException, // 409
|
|
247
|
+
GoneException, // 410
|
|
248
|
+
PayloadTooLargeException, // 413
|
|
249
|
+
UnsupportedMediaTypeException, // 415
|
|
250
|
+
UnprocessableEntityException,// 422
|
|
251
|
+
TooManyRequestsException, // 429
|
|
252
|
+
// 5xx
|
|
253
|
+
InternalServerErrorException,// 500
|
|
254
|
+
NotImplementedException, // 501
|
|
255
|
+
BadGatewayException, // 502
|
|
256
|
+
ServiceUnavailableException, // 503
|
|
257
|
+
GatewayTimeoutException, // 504
|
|
258
|
+
} from '@tongei/express-response-envelop';
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Throwing exceptions
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
import express from 'express';
|
|
265
|
+
import {
|
|
266
|
+
responseEnvelopMiddleware,
|
|
267
|
+
envelopErrorHandler,
|
|
268
|
+
NotFoundException,
|
|
269
|
+
ConflictException,
|
|
270
|
+
ForbiddenException,
|
|
271
|
+
UnprocessableEntityException,
|
|
272
|
+
} from '@tongei/express-response-envelop';
|
|
273
|
+
|
|
274
|
+
const app = express();
|
|
275
|
+
app.use(express.json());
|
|
276
|
+
app.use(responseEnvelopMiddleware({ defaultApiVersion: '1.0.0' }));
|
|
277
|
+
|
|
278
|
+
app.get('/users/:id', (req, res) => {
|
|
279
|
+
const user = db.findUser(req.params.id);
|
|
280
|
+
|
|
281
|
+
if (!user) {
|
|
282
|
+
throw new NotFoundException(`User ${req.params.id} not found`);
|
|
283
|
+
// { success: false, error: { status: 404, message: 'User 42 not found' } }
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
res.json(user);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
app.post('/users', (req, res) => {
|
|
290
|
+
if (db.emailExists(req.body.email)) {
|
|
291
|
+
throw new ConflictException('Email is already in use');
|
|
292
|
+
// { success: false, error: { status: 409, message: 'Email is already in use' } }
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (!req.user?.isAdmin) {
|
|
296
|
+
throw new ForbiddenException('Admin access required');
|
|
297
|
+
// { success: false, error: { status: 403, message: 'Admin access required' } }
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
res.json(db.createUser(req.body));
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// Register the error handler LAST — it catches all thrown exceptions
|
|
304
|
+
app.use(envelopErrorHandler({ defaultApiVersion: '1.0.0' }));
|
|
305
|
+
app.listen(3000);
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
> [!NOTE]
|
|
309
|
+
> The exceptions work with both `throw` and `next(err)`. You can throw them synchronously or pass them to `next()` in async routes.
|
|
310
|
+
|
|
311
|
+
### Throwing with validation details
|
|
312
|
+
|
|
313
|
+
Pass a second argument to attach structured detail to the error response:
|
|
314
|
+
|
|
315
|
+
```ts
|
|
316
|
+
app.post('/register', (req, res) => {
|
|
317
|
+
const errors = validate(req.body);
|
|
318
|
+
|
|
319
|
+
if (errors.length > 0) {
|
|
320
|
+
throw new UnprocessableEntityException('Validation failed', errors);
|
|
321
|
+
// {
|
|
322
|
+
// "success": false,
|
|
323
|
+
// "error": {
|
|
324
|
+
// "status": 422,
|
|
325
|
+
// "message": "Validation failed",
|
|
326
|
+
// "details": [{ "field": "email", "message": "Required" }]
|
|
327
|
+
// }
|
|
328
|
+
// }
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
res.json(db.register(req.body));
|
|
332
|
+
});
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### Custom domain exceptions
|
|
336
|
+
|
|
337
|
+
Extend `HttpException` for domain-specific errors:
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
import { HttpException } from '@tongei/express-response-envelop';
|
|
341
|
+
|
|
342
|
+
export class EntityNotFoundException extends HttpException {
|
|
343
|
+
constructor(entity: string, id: string | number) {
|
|
344
|
+
super(404, `${entity} with id "${id}" was not found`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export class SubscriptionExpiredException extends HttpException {
|
|
349
|
+
constructor() {
|
|
350
|
+
super(402, 'Your subscription has expired');
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Usage:
|
|
355
|
+
throw new EntityNotFoundException('Product', req.params.id);
|
|
356
|
+
throw new SubscriptionExpiredException();
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
## Custom Exception Filters
|
|
362
|
+
|
|
363
|
+
Register class-specific handlers in `envelopErrorHandler` — just like NestJS's `@Catch()` decorator. Filters are evaluated in order; the first match wins. Unmatched errors fall through to the default handler.
|
|
364
|
+
|
|
365
|
+
```ts
|
|
366
|
+
import {
|
|
367
|
+
envelopErrorHandler,
|
|
368
|
+
sendErrorEnvelop,
|
|
369
|
+
EnvelopExceptionFilter,
|
|
370
|
+
} from '@tongei/express-response-envelop';
|
|
371
|
+
|
|
372
|
+
// 1. Define your domain exception
|
|
373
|
+
class DatabaseException extends Error {
|
|
374
|
+
constructor(message = 'Database error') {
|
|
375
|
+
super(message);
|
|
376
|
+
this.name = 'DatabaseException';
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
class RateLimitException extends Error {
|
|
381
|
+
retryAfter: number;
|
|
382
|
+
constructor(retryAfter = 60) {
|
|
383
|
+
super('Too many requests');
|
|
384
|
+
this.retryAfter = retryAfter;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// 2. Create filters
|
|
389
|
+
const dbFilter: EnvelopExceptionFilter<DatabaseException> = {
|
|
390
|
+
exceptionType: DatabaseException,
|
|
391
|
+
handle(err, req, res) {
|
|
392
|
+
sendErrorEnvelop(res, 503, 'Database unavailable', {
|
|
393
|
+
details: { reason: err.message },
|
|
394
|
+
});
|
|
395
|
+
},
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
const rateLimitFilter: EnvelopExceptionFilter<RateLimitException> = {
|
|
399
|
+
exceptionType: RateLimitException,
|
|
400
|
+
handle(err, req, res) {
|
|
401
|
+
res.setHeader('Retry-After', err.retryAfter);
|
|
402
|
+
sendErrorEnvelop(res, 429, 'Too many requests', {
|
|
403
|
+
details: { retryAfter: err.retryAfter },
|
|
404
|
+
});
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
// 3. Register filters — first match wins
|
|
409
|
+
app.use(
|
|
410
|
+
envelopErrorHandler({
|
|
411
|
+
defaultApiVersion: '1.0.0',
|
|
412
|
+
filters: [dbFilter, rateLimitFilter],
|
|
413
|
+
}),
|
|
414
|
+
);
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
---
|
|
418
|
+
|
|
419
|
+
## Throwing Errors (plain Express style)
|
|
420
|
+
|
|
421
|
+
If you prefer not to use the typed exceptions, you can still use the classic Express pattern of attaching `status` to a plain error and passing it to `next()`:
|
|
422
|
+
|
|
423
|
+
```ts
|
|
424
|
+
app.get('/users/:id', (req, res, next) => {
|
|
425
|
+
const user = db.findUser(req.params.id);
|
|
426
|
+
|
|
427
|
+
if (!user) {
|
|
428
|
+
const err: any = new Error('User not found');
|
|
429
|
+
err.status = 404;
|
|
430
|
+
return next(err);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
res.json(user);
|
|
434
|
+
});
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
Works seamlessly with [`http-errors`](https://github.com/jshttp/http-errors):
|
|
438
|
+
|
|
439
|
+
```bash
|
|
440
|
+
npm install http-errors && npm install -D @types/http-errors
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
```ts
|
|
444
|
+
import createError from 'http-errors';
|
|
445
|
+
|
|
446
|
+
app.get('/protected', (req, res, next) => {
|
|
447
|
+
if (!req.headers.authorization) {
|
|
448
|
+
return next(createError(401, 'Authorization header missing'));
|
|
449
|
+
}
|
|
450
|
+
res.json({ secret: 'data' });
|
|
451
|
+
});
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
### Global 500 fallback
|
|
455
|
+
|
|
456
|
+
Any error without a `status` property (including uncaught `throw new Error(...)`) defaults to `500 Internal Server Error`:
|
|
457
|
+
|
|
458
|
+
```ts
|
|
459
|
+
app.get('/risky', async (req, res, next) => {
|
|
460
|
+
try {
|
|
461
|
+
const data = await someFlakyOperation();
|
|
462
|
+
res.json(data);
|
|
463
|
+
} catch (err) {
|
|
464
|
+
next(err);
|
|
465
|
+
// { success: false, error: { status: 500, message: '...' } }
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// Or with the typed exception:
|
|
470
|
+
app.get('/risky2', (req, res) => {
|
|
471
|
+
throw new InternalServerErrorException('Something went wrong internally');
|
|
472
|
+
});
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
---
|
|
476
|
+
|
|
477
|
+
## License
|
|
478
|
+
|
|
479
|
+
MIT
|
|
480
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface EnvelopeOptionalMeta {
|
|
2
|
+
apiVersion?: string;
|
|
3
|
+
timestamp?: string;
|
|
4
|
+
}
|
|
5
|
+
export interface SuccessEnvelope<T> {
|
|
6
|
+
success: true;
|
|
7
|
+
message: string | null;
|
|
8
|
+
data: T;
|
|
9
|
+
optional?: EnvelopeOptionalMeta;
|
|
10
|
+
}
|
|
11
|
+
export interface ErrorEnvelope {
|
|
12
|
+
success: false;
|
|
13
|
+
message: string | null;
|
|
14
|
+
error?: {
|
|
15
|
+
status: number;
|
|
16
|
+
message: string;
|
|
17
|
+
details?: unknown;
|
|
18
|
+
};
|
|
19
|
+
optional?: EnvelopeOptionalMeta;
|
|
20
|
+
}
|
|
21
|
+
export declare function buildSuccessEnvelope<T>(data: T, opts?: {
|
|
22
|
+
message?: string | null;
|
|
23
|
+
apiVersion?: string;
|
|
24
|
+
includeTimestamp?: boolean;
|
|
25
|
+
}): SuccessEnvelope<T>;
|
|
26
|
+
export declare function buildErrorEnvelope(status: number, message: string, opts?: {
|
|
27
|
+
apiVersion?: string;
|
|
28
|
+
includeTimestamp?: boolean;
|
|
29
|
+
details?: unknown;
|
|
30
|
+
}): ErrorEnvelope;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildSuccessEnvelope = buildSuccessEnvelope;
|
|
4
|
+
exports.buildErrorEnvelope = buildErrorEnvelope;
|
|
5
|
+
function buildOptional(apiVersion, includeTimestamp = true) {
|
|
6
|
+
const optional = {};
|
|
7
|
+
if (apiVersion !== undefined)
|
|
8
|
+
optional.apiVersion = apiVersion;
|
|
9
|
+
if (includeTimestamp)
|
|
10
|
+
optional.timestamp = new Date().toISOString();
|
|
11
|
+
return Object.keys(optional).length > 0 ? optional : undefined;
|
|
12
|
+
}
|
|
13
|
+
function buildSuccessEnvelope(data, opts = {}) {
|
|
14
|
+
return {
|
|
15
|
+
success: true,
|
|
16
|
+
message: opts.message ?? null,
|
|
17
|
+
data,
|
|
18
|
+
optional: buildOptional(opts.apiVersion, opts.includeTimestamp ?? true),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function buildErrorEnvelope(status, message, opts = {}) {
|
|
22
|
+
return {
|
|
23
|
+
success: false,
|
|
24
|
+
message: null,
|
|
25
|
+
error: {
|
|
26
|
+
status,
|
|
27
|
+
message,
|
|
28
|
+
...(opts.details ? { details: opts.details } : {}),
|
|
29
|
+
},
|
|
30
|
+
optional: buildOptional(opts.apiVersion, opts.includeTimestamp ?? true),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=envelope.builder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"envelope.builder.js","sourceRoot":"","sources":["../../src/builder/envelope.builder.ts"],"names":[],"mappings":";;AA8BA,oDAcC;AAED,gDAmBC;AA7CD,SAAS,aAAa,CACpB,UAAmB,EACnB,gBAAgB,GAAG,IAAI;IAEvB,MAAM,QAAQ,GAAyB,EAAE,CAAC;IAC1C,IAAI,UAAU,KAAK,SAAS;QAAE,QAAQ,CAAC,UAAU,GAAG,UAAU,CAAC;IAC/D,IAAI,gBAAgB;QAAE,QAAQ,CAAC,SAAS,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACpE,OAAO,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC;AACjE,CAAC;AAED,SAAgB,oBAAoB,CAClC,IAAO,EACP,OAII,EAAE;IAEN,OAAO;QACL,OAAO,EAAE,IAAI;QACb,OAAO,EAAE,IAAI,CAAC,OAAO,IAAI,IAAI;QAC7B,IAAI;QACJ,QAAQ,EAAE,aAAa,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,gBAAgB,IAAI,IAAI,CAAC;KACxE,CAAC;AACJ,CAAC;AAED,SAAgB,kBAAkB,CAChC,MAAc,EACd,OAAe,EACf,OAII,EAAE;IAEN,OAAO;QACL,OAAO,EAAE,KAAK;QACd,OAAO,EAAE,IAAI;QACb,KAAK,EAAE;YACL,MAAM;YACN,OAAO;YACP,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACnD;QACD,QAAQ,EAAE,aAAa,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,gBAAgB,IAAI,IAAI,CAAC;KACxE,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ErrorRequestHandler } from 'express';
|
|
2
|
+
import { EnvelopExceptionFilter } from '../filters/exception-filter.type';
|
|
3
|
+
import { EnvelopMiddlewareOptions } from '../types/response-envelop-option.type';
|
|
4
|
+
export interface EnvelopErrorHandlerOptions extends EnvelopMiddlewareOptions {
|
|
5
|
+
filters?: EnvelopExceptionFilter[];
|
|
6
|
+
}
|
|
7
|
+
export declare function envelopErrorHandler(options?: EnvelopErrorHandlerOptions): ErrorRequestHandler;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.envelopErrorHandler = envelopErrorHandler;
|
|
4
|
+
const envelope_builder_1 = require("../builder/envelope.builder");
|
|
5
|
+
const http_exception_1 = require("../exceptions/http.exception");
|
|
6
|
+
function envelopErrorHandler(options = {}) {
|
|
7
|
+
const { defaultApiVersion, includeTimestamp, filters = [] } = options;
|
|
8
|
+
return function errorHandler(err, req, res, next) {
|
|
9
|
+
for (const filter of filters) {
|
|
10
|
+
if (err instanceof filter.exceptionType) {
|
|
11
|
+
filter.handle(err, req, res, next);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
if (err instanceof http_exception_1.HttpException) {
|
|
16
|
+
const envelope = (0, envelope_builder_1.buildErrorEnvelope)(err.status, err.message, {
|
|
17
|
+
apiVersion: defaultApiVersion,
|
|
18
|
+
includeTimestamp,
|
|
19
|
+
details: err.details,
|
|
20
|
+
});
|
|
21
|
+
res.status(err.status).json(envelope);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const status = typeof err?.status === 'number'
|
|
25
|
+
? err.status
|
|
26
|
+
: typeof err?.statusCode === 'number'
|
|
27
|
+
? err.statusCode
|
|
28
|
+
: 500;
|
|
29
|
+
const message = typeof err?.message === 'string' && err.message
|
|
30
|
+
? err.message
|
|
31
|
+
: 'Internal Server Error';
|
|
32
|
+
const details = err?.errors ?? err?.details ?? (err instanceof Error ? undefined : err);
|
|
33
|
+
const envelope = (0, envelope_builder_1.buildErrorEnvelope)(status, message, {
|
|
34
|
+
apiVersion: defaultApiVersion,
|
|
35
|
+
includeTimestamp,
|
|
36
|
+
details,
|
|
37
|
+
});
|
|
38
|
+
res.status(status).json(envelope);
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=envelop-error-handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"envelop-error-handler.js","sourceRoot":"","sources":["../../src/error-handler/envelop-error-handler.ts"],"names":[],"mappings":";;AAyDA,kDAwDC;AA/GD,kEAAiE;AACjE,iEAA6D;AAsD7D,SAAgB,mBAAmB,CACjC,UAAsC,EAAE;IAExC,MAAM,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,OAAO,GAAG,EAAE,EAAE,GAAG,OAAO,CAAC;IAGtE,OAAO,SAAS,YAAY,CAC1B,GAAQ,EACR,GAAY,EACZ,GAAa,EACb,IAAkB;QAGlB,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;YAC7B,IAAI,GAAG,YAAY,MAAM,CAAC,aAAa,EAAE,CAAC;gBACxC,MAAM,CAAC,MAAM,CAAC,GAAU,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;gBAC1C,OAAO;YACT,CAAC;QACH,CAAC;QAGD,IAAI,GAAG,YAAY,8BAAa,EAAE,CAAC;YACjC,MAAM,QAAQ,GAAG,IAAA,qCAAkB,EAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,OAAO,EAAE;gBAC3D,UAAU,EAAE,iBAAiB;gBAC7B,gBAAgB;gBAChB,OAAO,EAAE,GAAG,CAAC,OAAO;aACrB,CAAC,CAAC;YACH,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YACtC,OAAO;QACT,CAAC;QAGD,MAAM,MAAM,GACV,OAAO,GAAG,EAAE,MAAM,KAAK,QAAQ;YAC7B,CAAC,CAAC,GAAG,CAAC,MAAM;YACZ,CAAC,CAAC,OAAO,GAAG,EAAE,UAAU,KAAK,QAAQ;gBACnC,CAAC,CAAC,GAAG,CAAC,UAAU;gBAChB,CAAC,CAAC,GAAG,CAAC;QAEZ,MAAM,OAAO,GACX,OAAO,GAAG,EAAE,OAAO,KAAK,QAAQ,IAAI,GAAG,CAAC,OAAO;YAC7C,CAAC,CAAC,GAAG,CAAC,OAAO;YACb,CAAC,CAAC,uBAAuB,CAAC;QAE9B,MAAM,OAAO,GACX,GAAG,EAAE,MAAM,IAAI,GAAG,EAAE,OAAO,IAAI,CAAC,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QAG1E,MAAM,QAAQ,GAAG,IAAA,qCAAkB,EAAC,MAAM,EAAE,OAAO,EAAE;YACnD,UAAU,EAAE,iBAAiB;YAC7B,gBAAgB;YAChB,OAAO;SACR,CAAC,CAAC;QAEH,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACpC,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { HttpException } from './http.exception';
|
|
2
|
+
export declare class BadRequestException extends HttpException {
|
|
3
|
+
constructor(message?: string, details?: unknown);
|
|
4
|
+
}
|
|
5
|
+
export declare class UnauthorizedException extends HttpException {
|
|
6
|
+
constructor(message?: string, details?: unknown);
|
|
7
|
+
}
|
|
8
|
+
export declare class PaymentRequiredException extends HttpException {
|
|
9
|
+
constructor(message?: string, details?: unknown);
|
|
10
|
+
}
|
|
11
|
+
export declare class ForbiddenException extends HttpException {
|
|
12
|
+
constructor(message?: string, details?: unknown);
|
|
13
|
+
}
|
|
14
|
+
export declare class NotFoundException extends HttpException {
|
|
15
|
+
constructor(message?: string, details?: unknown);
|
|
16
|
+
}
|
|
17
|
+
export declare class MethodNotAllowedException extends HttpException {
|
|
18
|
+
constructor(message?: string, details?: unknown);
|
|
19
|
+
}
|
|
20
|
+
export declare class NotAcceptableException extends HttpException {
|
|
21
|
+
constructor(message?: string, details?: unknown);
|
|
22
|
+
}
|
|
23
|
+
export declare class RequestTimeoutException extends HttpException {
|
|
24
|
+
constructor(message?: string, details?: unknown);
|
|
25
|
+
}
|
|
26
|
+
export declare class ConflictException extends HttpException {
|
|
27
|
+
constructor(message?: string, details?: unknown);
|
|
28
|
+
}
|
|
29
|
+
export declare class GoneException extends HttpException {
|
|
30
|
+
constructor(message?: string, details?: unknown);
|
|
31
|
+
}
|
|
32
|
+
export declare class PayloadTooLargeException extends HttpException {
|
|
33
|
+
constructor(message?: string, details?: unknown);
|
|
34
|
+
}
|
|
35
|
+
export declare class UnsupportedMediaTypeException extends HttpException {
|
|
36
|
+
constructor(message?: string, details?: unknown);
|
|
37
|
+
}
|
|
38
|
+
export declare class UnprocessableEntityException extends HttpException {
|
|
39
|
+
constructor(message?: string, details?: unknown);
|
|
40
|
+
}
|
|
41
|
+
export declare class TooManyRequestsException extends HttpException {
|
|
42
|
+
constructor(message?: string, details?: unknown);
|
|
43
|
+
}
|
|
44
|
+
export declare class InternalServerErrorException extends HttpException {
|
|
45
|
+
constructor(message?: string, details?: unknown);
|
|
46
|
+
}
|
|
47
|
+
export declare class NotImplementedException extends HttpException {
|
|
48
|
+
constructor(message?: string, details?: unknown);
|
|
49
|
+
}
|
|
50
|
+
export declare class BadGatewayException extends HttpException {
|
|
51
|
+
constructor(message?: string, details?: unknown);
|
|
52
|
+
}
|
|
53
|
+
export declare class ServiceUnavailableException extends HttpException {
|
|
54
|
+
constructor(message?: string, details?: unknown);
|
|
55
|
+
}
|
|
56
|
+
export declare class GatewayTimeoutException extends HttpException {
|
|
57
|
+
constructor(message?: string, details?: unknown);
|
|
58
|
+
}
|