express-pro-toolkit 2.0.0
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/LICENSE +21 -0
- package/README.md +426 -0
- package/index.js +28 -0
- package/package.json +55 -0
- package/src/AppError.js +175 -0
- package/src/asyncHandler.js +48 -0
- package/src/correlationId.js +58 -0
- package/src/errorHandler.js +153 -0
- package/src/httpStatus.js +48 -0
- package/src/notFoundHandler.js +34 -0
- package/src/requestLogger.js +119 -0
- package/src/responseHelpers.js +108 -0
- package/types/index.d.ts +169 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
# express-pro-toolkit
|
|
2
|
+
|
|
3
|
+
> Enterprise-grade Express.js toolkit — async error handling, configurable error handler with logging hooks, request logger with correlation IDs, structured `AppError` class, pagination helpers, and consistent JSON responses. **Zero external dependencies.**
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/express-pro-toolkit)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
|
|
12
|
+
| Utility | Description |
|
|
13
|
+
| --- | --- |
|
|
14
|
+
| `asyncHandler(fn)` | Wraps async route/error handlers — catches rejected promises automatically |
|
|
15
|
+
| `errorHandler` | Drop-in global error handler with consistent JSON responses |
|
|
16
|
+
| `createErrorHandler(opts)` | Configurable error handler with logging hooks (Sentry, Datadog, etc.) |
|
|
17
|
+
| `requestLogger` | Colourised request logger with response time |
|
|
18
|
+
| `createRequestLogger(opts)` | Configurable logger — skip routes, custom output, correlation IDs |
|
|
19
|
+
| `correlationId(opts)` | Generates / propagates `x-request-id` for distributed tracing |
|
|
20
|
+
| `notFoundHandler(opts)` | 404 catch-all middleware |
|
|
21
|
+
| `AppError` | Structured error class with static factories, codes, and operational flag |
|
|
22
|
+
| `sendSuccess()` | Consistent JSON success response |
|
|
23
|
+
| `sendError()` | Consistent JSON error response |
|
|
24
|
+
| `sendPaginated()` | Paginated JSON response with metadata |
|
|
25
|
+
| `httpStatus` | Frozen enum of common HTTP status codes |
|
|
26
|
+
|
|
27
|
+
**Zero external dependencies** · Works with **Express 4 & 5** · **TypeScript declarations included**
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm install express-pro-toolkit
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
const express = require('express');
|
|
43
|
+
const {
|
|
44
|
+
asyncHandler,
|
|
45
|
+
createErrorHandler,
|
|
46
|
+
createRequestLogger,
|
|
47
|
+
correlationId,
|
|
48
|
+
notFoundHandler,
|
|
49
|
+
sendSuccess,
|
|
50
|
+
AppError,
|
|
51
|
+
httpStatus,
|
|
52
|
+
} = require('express-pro-toolkit');
|
|
53
|
+
|
|
54
|
+
const app = express();
|
|
55
|
+
|
|
56
|
+
app.use(express.json());
|
|
57
|
+
app.use(correlationId()); // x-request-id
|
|
58
|
+
app.use(createRequestLogger({ // skip health checks
|
|
59
|
+
skip: (req) => req.url === '/health',
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
app.get('/api/users', asyncHandler(async (req, res) => {
|
|
63
|
+
const users = await getUsersFromDB();
|
|
64
|
+
sendSuccess(res, users, 'Users fetched');
|
|
65
|
+
}));
|
|
66
|
+
|
|
67
|
+
app.post('/api/users', asyncHandler(async (req, res) => {
|
|
68
|
+
if (!req.body.email) {
|
|
69
|
+
throw AppError.validation([{ field: 'email', message: 'Required' }]);
|
|
70
|
+
}
|
|
71
|
+
const user = await createUser(req.body);
|
|
72
|
+
sendSuccess(res, user, 'Created', httpStatus.CREATED);
|
|
73
|
+
}));
|
|
74
|
+
|
|
75
|
+
app.use(notFoundHandler()); // 404 catch-all
|
|
76
|
+
app.use(createErrorHandler({ // global error handler
|
|
77
|
+
onError: ({ err, requestId }) => {
|
|
78
|
+
// Send to Sentry, Datadog, Pino, etc.
|
|
79
|
+
},
|
|
80
|
+
}));
|
|
81
|
+
|
|
82
|
+
app.listen(3000);
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## API Reference
|
|
88
|
+
|
|
89
|
+
### `asyncHandler(fn)`
|
|
90
|
+
|
|
91
|
+
Wraps an async `(req, res, next)` handler so rejected promises are forwarded to `next(err)`. Also supports 4-arg error-handling middleware.
|
|
92
|
+
|
|
93
|
+
```js
|
|
94
|
+
router.get('/items', asyncHandler(async (req, res) => {
|
|
95
|
+
const items = await Item.find();
|
|
96
|
+
res.json(items);
|
|
97
|
+
}));
|
|
98
|
+
|
|
99
|
+
// Also works with async error middleware
|
|
100
|
+
app.use(asyncHandler(async (err, req, res, next) => {
|
|
101
|
+
await logToExternalService(err);
|
|
102
|
+
next(err);
|
|
103
|
+
}));
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
### `AppError`
|
|
109
|
+
|
|
110
|
+
Structured error class with HTTP status codes, machine-readable codes, operational flags, and static factory methods.
|
|
111
|
+
|
|
112
|
+
```js
|
|
113
|
+
const { AppError } = require('express-pro-toolkit');
|
|
114
|
+
|
|
115
|
+
// Static factories (most common)
|
|
116
|
+
throw AppError.badRequest('Invalid input'); // 400
|
|
117
|
+
throw AppError.unauthorized('Login required'); // 401
|
|
118
|
+
throw AppError.forbidden('No access'); // 403
|
|
119
|
+
throw AppError.notFound('User not found'); // 404
|
|
120
|
+
throw AppError.conflict('Email already exists'); // 409
|
|
121
|
+
throw AppError.tooManyRequests('Slow down'); // 429
|
|
122
|
+
throw AppError.internal('Unexpected failure'); // 500 (isOperational: false)
|
|
123
|
+
|
|
124
|
+
// Validation with field-level details
|
|
125
|
+
throw AppError.validation([
|
|
126
|
+
{ field: 'email', message: 'Email is required' },
|
|
127
|
+
{ field: 'name', message: 'Name is required' },
|
|
128
|
+
]);
|
|
129
|
+
|
|
130
|
+
// Custom
|
|
131
|
+
throw new AppError('Payment failed', 402, {
|
|
132
|
+
code: 'PAYMENT_DECLINED',
|
|
133
|
+
errors: [{ provider: 'stripe', reason: 'insufficient_funds' }],
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**Operational vs Programmer errors:**
|
|
138
|
+
- `isOperational: true` (default) — expected failures, safe to expose to clients.
|
|
139
|
+
- `isOperational: false` — bugs; in production the message is replaced with "Internal Server Error".
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
### `errorHandler` / `createErrorHandler(options)`
|
|
144
|
+
|
|
145
|
+
| Option | Type | Default | Description |
|
|
146
|
+
| --- | --- | --- | --- |
|
|
147
|
+
| `includeStack` | `boolean` | `!production` | Include stack trace in response |
|
|
148
|
+
| `onError` | `function` | `console.error` | Logging hook — receives full payload |
|
|
149
|
+
| `defaultStatusCode` | `number` | `500` | Fallback HTTP status |
|
|
150
|
+
|
|
151
|
+
```js
|
|
152
|
+
// Drop-in (default config)
|
|
153
|
+
app.use(errorHandler);
|
|
154
|
+
|
|
155
|
+
// Enterprise — custom logging hook
|
|
156
|
+
app.use(createErrorHandler({
|
|
157
|
+
onError: ({ err, statusCode, method, url, requestId, timestamp }) => {
|
|
158
|
+
Sentry.captureException(err, { tags: { requestId } });
|
|
159
|
+
},
|
|
160
|
+
}));
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**Error response includes:**
|
|
164
|
+
- `success: false`
|
|
165
|
+
- `message` — safe message (sanitised in production for 5xx)
|
|
166
|
+
- `errors` — detail array (validation, etc.)
|
|
167
|
+
- `code` — machine-readable error code (if set on error)
|
|
168
|
+
- `requestId` — correlation ID (if `correlationId` middleware is active)
|
|
169
|
+
- `stack` — stack trace (dev only by default)
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
### `requestLogger` / `createRequestLogger(options)`
|
|
174
|
+
|
|
175
|
+
| Option | Type | Default | Description |
|
|
176
|
+
| --- | --- | --- | --- |
|
|
177
|
+
| `log` | `function` | Colourised console | Custom log function `(info) => void` |
|
|
178
|
+
| `skip` | `function` | none | `(req, res) => boolean` — skip certain requests |
|
|
179
|
+
|
|
180
|
+
```js
|
|
181
|
+
// Default (colourised, logs everything)
|
|
182
|
+
app.use(requestLogger);
|
|
183
|
+
|
|
184
|
+
// Skip health checks, pipe to Pino
|
|
185
|
+
app.use(createRequestLogger({
|
|
186
|
+
skip: (req) => req.url === '/health',
|
|
187
|
+
log: (info) => pino.info(info, 'http'),
|
|
188
|
+
}));
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
**Log info object:**
|
|
192
|
+
```js
|
|
193
|
+
{
|
|
194
|
+
method: 'GET',
|
|
195
|
+
url: '/api/users',
|
|
196
|
+
status: 200,
|
|
197
|
+
duration: 4.21, // ms
|
|
198
|
+
timestamp: '2026-03-01T12:00:00.000Z',
|
|
199
|
+
requestId: 'a1b2c3d4e5f6'
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
### `correlationId(options)`
|
|
206
|
+
|
|
207
|
+
Generates or propagates a unique request ID for distributed tracing.
|
|
208
|
+
|
|
209
|
+
| Option | Type | Default | Description |
|
|
210
|
+
| --- | --- | --- | --- |
|
|
211
|
+
| `header` | `string` | `'x-request-id'` | Header to read / echo |
|
|
212
|
+
| `generator` | `function` | 16-char hex | Custom ID generator |
|
|
213
|
+
| `setResponseHeader` | `boolean` | `true` | Echo ID back to client |
|
|
214
|
+
|
|
215
|
+
```js
|
|
216
|
+
app.use(correlationId());
|
|
217
|
+
|
|
218
|
+
// Custom header + UUID generator
|
|
219
|
+
app.use(correlationId({
|
|
220
|
+
header: 'x-correlation-id',
|
|
221
|
+
generator: () => crypto.randomUUID(),
|
|
222
|
+
}));
|
|
223
|
+
|
|
224
|
+
// Access in routes
|
|
225
|
+
app.get('/test', (req, res) => {
|
|
226
|
+
console.log(req.id); // 'a1b2c3d4e5f67890'
|
|
227
|
+
});
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
### `notFoundHandler(options)`
|
|
233
|
+
|
|
234
|
+
Catches unmatched routes and forwards a 404 `AppError` to the error handler.
|
|
235
|
+
|
|
236
|
+
```js
|
|
237
|
+
// After all routes, before errorHandler
|
|
238
|
+
app.use(notFoundHandler());
|
|
239
|
+
app.use(notFoundHandler({ message: 'Route does not exist' }));
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
### `sendSuccess(res, data, message?, statusCode?, meta?)`
|
|
245
|
+
|
|
246
|
+
| Parameter | Type | Default | Description |
|
|
247
|
+
| --- | --- | --- | --- |
|
|
248
|
+
| `res` | `Response` | — | Express response |
|
|
249
|
+
| `data` | `any` | `null` | Payload |
|
|
250
|
+
| `message` | `string` | `"Success"` | Human-readable message |
|
|
251
|
+
| `statusCode` | `number` | `200` | HTTP status code |
|
|
252
|
+
| `meta` | `object` | — | Optional metadata (pagination, etc.) |
|
|
253
|
+
|
|
254
|
+
```js
|
|
255
|
+
sendSuccess(res, user, 'User created', 201);
|
|
256
|
+
|
|
257
|
+
// With metadata
|
|
258
|
+
sendSuccess(res, users, 'Fetched', 200, { cached: true });
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
### `sendError(res, message?, statusCode?, errors?, code?)`
|
|
264
|
+
|
|
265
|
+
```js
|
|
266
|
+
sendError(res, 'Validation failed', 422, [
|
|
267
|
+
{ field: 'email', message: 'Required' },
|
|
268
|
+
], 'VALIDATION_ERROR');
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
### `sendPaginated(res, items, pagination, message?)`
|
|
274
|
+
|
|
275
|
+
```js
|
|
276
|
+
sendPaginated(res, users, { page: 2, limit: 25, total: 100 });
|
|
277
|
+
|
|
278
|
+
// Response:
|
|
279
|
+
// {
|
|
280
|
+
// "success": true,
|
|
281
|
+
// "message": "Success",
|
|
282
|
+
// "data": [...],
|
|
283
|
+
// "meta": {
|
|
284
|
+
// "page": 2,
|
|
285
|
+
// "limit": 25,
|
|
286
|
+
// "total": 100,
|
|
287
|
+
// "totalPages": 4,
|
|
288
|
+
// "hasNextPage": true,
|
|
289
|
+
// "hasPrevPage": true
|
|
290
|
+
// }
|
|
291
|
+
// }
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
### `httpStatus`
|
|
297
|
+
|
|
298
|
+
Frozen object of common HTTP status codes — eliminates magic numbers.
|
|
299
|
+
|
|
300
|
+
```js
|
|
301
|
+
const { httpStatus } = require('express-pro-toolkit');
|
|
302
|
+
|
|
303
|
+
res.status(httpStatus.CREATED).json(data); // 201
|
|
304
|
+
res.status(httpStatus.NO_CONTENT).end(); // 204
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
## Response Formats
|
|
310
|
+
|
|
311
|
+
### Success
|
|
312
|
+
|
|
313
|
+
```json
|
|
314
|
+
{
|
|
315
|
+
"success": true,
|
|
316
|
+
"message": "Message here",
|
|
317
|
+
"data": { }
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### Success (paginated)
|
|
322
|
+
|
|
323
|
+
```json
|
|
324
|
+
{
|
|
325
|
+
"success": true,
|
|
326
|
+
"message": "Message here",
|
|
327
|
+
"data": [ ],
|
|
328
|
+
"meta": {
|
|
329
|
+
"page": 1,
|
|
330
|
+
"limit": 25,
|
|
331
|
+
"total": 100,
|
|
332
|
+
"totalPages": 4,
|
|
333
|
+
"hasNextPage": true,
|
|
334
|
+
"hasPrevPage": false
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
### Error
|
|
340
|
+
|
|
341
|
+
```json
|
|
342
|
+
{
|
|
343
|
+
"success": false,
|
|
344
|
+
"message": "Error message",
|
|
345
|
+
"code": "ERROR_CODE",
|
|
346
|
+
"errors": [ ],
|
|
347
|
+
"requestId": "a1b2c3d4e5f67890"
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
---
|
|
352
|
+
|
|
353
|
+
## Example App
|
|
354
|
+
|
|
355
|
+
```bash
|
|
356
|
+
cd express-pro-toolkit
|
|
357
|
+
npm install express
|
|
358
|
+
node example/server.js
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
|
|
363
|
+
## Smoke Tests
|
|
364
|
+
|
|
365
|
+
```bash
|
|
366
|
+
npm test
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
## TypeScript
|
|
372
|
+
|
|
373
|
+
TypeScript declarations are bundled in `types/index.d.ts`. No `@types/` package needed.
|
|
374
|
+
|
|
375
|
+
```ts
|
|
376
|
+
import {
|
|
377
|
+
asyncHandler,
|
|
378
|
+
AppError,
|
|
379
|
+
createErrorHandler,
|
|
380
|
+
httpStatus,
|
|
381
|
+
} from 'express-pro-toolkit';
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
---
|
|
385
|
+
|
|
386
|
+
## Publishing to NPM
|
|
387
|
+
|
|
388
|
+
```bash
|
|
389
|
+
npm login
|
|
390
|
+
npm version patch # 2.0.0 → 2.0.1
|
|
391
|
+
npm publish
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
---
|
|
395
|
+
|
|
396
|
+
## Folder Structure
|
|
397
|
+
|
|
398
|
+
```
|
|
399
|
+
express-pro-toolkit/
|
|
400
|
+
├── example/
|
|
401
|
+
│ └── server.js
|
|
402
|
+
├── src/
|
|
403
|
+
│ ├── AppError.js
|
|
404
|
+
│ ├── asyncHandler.js
|
|
405
|
+
│ ├── correlationId.js
|
|
406
|
+
│ ├── errorHandler.js
|
|
407
|
+
│ ├── httpStatus.js
|
|
408
|
+
│ ├── notFoundHandler.js
|
|
409
|
+
│ ├── requestLogger.js
|
|
410
|
+
│ └── responseHelpers.js
|
|
411
|
+
├── test/
|
|
412
|
+
│ └── smoke.js
|
|
413
|
+
├── types/
|
|
414
|
+
│ └── index.d.ts
|
|
415
|
+
├── index.js
|
|
416
|
+
├── package.json
|
|
417
|
+
├── README.md
|
|
418
|
+
├── LICENSE
|
|
419
|
+
└── .npmignore
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
## License
|
|
425
|
+
|
|
426
|
+
[MIT](LICENSE)
|
package/index.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const asyncHandler = require('./src/asyncHandler');
|
|
4
|
+
const errorHandlerModule = require('./src/errorHandler');
|
|
5
|
+
const requestLoggerModule = require('./src/requestLogger');
|
|
6
|
+
const { sendSuccess, sendError, sendPaginated } = require('./src/responseHelpers');
|
|
7
|
+
const AppError = require('./src/AppError');
|
|
8
|
+
const correlationId = require('./src/correlationId');
|
|
9
|
+
const notFoundHandler = require('./src/notFoundHandler');
|
|
10
|
+
const httpStatus = require('./src/httpStatus');
|
|
11
|
+
|
|
12
|
+
module.exports = {
|
|
13
|
+
// Core (v1 backward-compatible)
|
|
14
|
+
asyncHandler,
|
|
15
|
+
errorHandler: errorHandlerModule,
|
|
16
|
+
requestLogger: requestLoggerModule,
|
|
17
|
+
sendSuccess,
|
|
18
|
+
sendError,
|
|
19
|
+
|
|
20
|
+
// Enterprise additions (v2)
|
|
21
|
+
createErrorHandler: errorHandlerModule.createErrorHandler,
|
|
22
|
+
createRequestLogger: requestLoggerModule.createRequestLogger,
|
|
23
|
+
sendPaginated,
|
|
24
|
+
AppError,
|
|
25
|
+
correlationId,
|
|
26
|
+
notFoundHandler,
|
|
27
|
+
httpStatus,
|
|
28
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "express-pro-toolkit",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Enterprise-grade Express.js toolkit — async error handling, configurable error handler, request logger with correlation IDs, structured AppError class, pagination helpers, and consistent JSON responses. Zero external dependencies.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"types": "types/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "jest --verbose",
|
|
9
|
+
"test:smoke": "node test/smoke.js",
|
|
10
|
+
"test:coverage": "jest --verbose --coverage",
|
|
11
|
+
"example": "node example/server.js",
|
|
12
|
+
"lint": "echo \"No linter configured yet\"",
|
|
13
|
+
"prepublishOnly": "jest"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"express",
|
|
17
|
+
"error-handler",
|
|
18
|
+
"async-handler",
|
|
19
|
+
"middleware",
|
|
20
|
+
"request-logger",
|
|
21
|
+
"json-response",
|
|
22
|
+
"express-middleware",
|
|
23
|
+
"error-handling",
|
|
24
|
+
"toolkit",
|
|
25
|
+
"correlation-id",
|
|
26
|
+
"request-id",
|
|
27
|
+
"app-error",
|
|
28
|
+
"pagination",
|
|
29
|
+
"enterprise",
|
|
30
|
+
"express-pro-toolkit"
|
|
31
|
+
],
|
|
32
|
+
"author": "Hadeed Ul Hassan <hadeed.hassan189@gmail.com>",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/hadiid1718/express-pro-toolkit.git"
|
|
37
|
+
},
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/hadiid1718/express-pro-toolkit/issues"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/hadiid1718/express-pro-toolkit#readme",
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=12.0.0"
|
|
44
|
+
},
|
|
45
|
+
"peerDependencies": {
|
|
46
|
+
"express": ">=4.0.0"
|
|
47
|
+
},
|
|
48
|
+
"files": [
|
|
49
|
+
"index.js",
|
|
50
|
+
"src/",
|
|
51
|
+
"types/",
|
|
52
|
+
"LICENSE",
|
|
53
|
+
"README.md"
|
|
54
|
+
]
|
|
55
|
+
}
|
package/src/AppError.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Custom application error class for express-pro-toolkit.
|
|
5
|
+
*
|
|
6
|
+
* Provides structured, operational errors with HTTP status codes,
|
|
7
|
+
* error codes, and detail arrays. Distinguishes operational errors
|
|
8
|
+
* (expected, safe to expose) from programmer errors (bugs).
|
|
9
|
+
*
|
|
10
|
+
* @extends Error
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* const { AppError } = require('express-pro-toolkit');
|
|
14
|
+
*
|
|
15
|
+
* // Simple
|
|
16
|
+
* throw new AppError('User not found', 404);
|
|
17
|
+
*
|
|
18
|
+
* // With error code
|
|
19
|
+
* throw new AppError('Token expired', 401, { code: 'AUTH_TOKEN_EXPIRED' });
|
|
20
|
+
*
|
|
21
|
+
* // With validation details
|
|
22
|
+
* throw new AppError('Validation failed', 422, {
|
|
23
|
+
* code: 'VALIDATION_ERROR',
|
|
24
|
+
* errors: [
|
|
25
|
+
* { field: 'email', message: 'Email is required' },
|
|
26
|
+
* ],
|
|
27
|
+
* });
|
|
28
|
+
*/
|
|
29
|
+
class AppError extends Error {
|
|
30
|
+
/**
|
|
31
|
+
* @param {string} message - Human-readable error message
|
|
32
|
+
* @param {number} [statusCode=500] - HTTP status code
|
|
33
|
+
* @param {object} [options] - Additional options
|
|
34
|
+
* @param {string} [options.code] - Machine-readable error code (e.g. 'VALIDATION_ERROR')
|
|
35
|
+
* @param {Array} [options.errors] - Array of detailed sub-errors
|
|
36
|
+
* @param {boolean} [options.isOperational=true] - Whether this is an operational (expected) error
|
|
37
|
+
*/
|
|
38
|
+
constructor(message, statusCode, options) {
|
|
39
|
+
super(message);
|
|
40
|
+
|
|
41
|
+
/** @type {string} */
|
|
42
|
+
this.name = 'AppError';
|
|
43
|
+
|
|
44
|
+
/** @type {number} */
|
|
45
|
+
this.statusCode = typeof statusCode === 'number' ? statusCode : 500;
|
|
46
|
+
|
|
47
|
+
const opts = options && typeof options === 'object' ? options : {};
|
|
48
|
+
|
|
49
|
+
/** @type {string|undefined} */
|
|
50
|
+
this.code = opts.code || undefined;
|
|
51
|
+
|
|
52
|
+
/** @type {Array|null} */
|
|
53
|
+
this.errors = Array.isArray(opts.errors) ? opts.errors : null;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Operational errors are expected failures (bad input, not found, etc.).
|
|
57
|
+
* Non-operational errors indicate programmer bugs and should trigger alerts.
|
|
58
|
+
* @type {boolean}
|
|
59
|
+
*/
|
|
60
|
+
this.isOperational = opts.isOperational !== false;
|
|
61
|
+
|
|
62
|
+
/** @type {number} */
|
|
63
|
+
this.timestamp = Date.now();
|
|
64
|
+
|
|
65
|
+
// Capture stack trace, excluding constructor call from it
|
|
66
|
+
if (Error.captureStackTrace) {
|
|
67
|
+
Error.captureStackTrace(this, AppError);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Create a 400 Bad Request error.
|
|
73
|
+
* @param {string} [message='Bad Request']
|
|
74
|
+
* @param {object} [options]
|
|
75
|
+
* @returns {AppError}
|
|
76
|
+
*/
|
|
77
|
+
static badRequest(message, options) {
|
|
78
|
+
return new AppError(message || 'Bad Request', 400, options);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Create a 401 Unauthorized error.
|
|
83
|
+
* @param {string} [message='Unauthorized']
|
|
84
|
+
* @param {object} [options]
|
|
85
|
+
* @returns {AppError}
|
|
86
|
+
*/
|
|
87
|
+
static unauthorized(message, options) {
|
|
88
|
+
return new AppError(message || 'Unauthorized', 401, options);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Create a 403 Forbidden error.
|
|
93
|
+
* @param {string} [message='Forbidden']
|
|
94
|
+
* @param {object} [options]
|
|
95
|
+
* @returns {AppError}
|
|
96
|
+
*/
|
|
97
|
+
static forbidden(message, options) {
|
|
98
|
+
return new AppError(message || 'Forbidden', 403, options);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Create a 404 Not Found error.
|
|
103
|
+
* @param {string} [message='Not Found']
|
|
104
|
+
* @param {object} [options]
|
|
105
|
+
* @returns {AppError}
|
|
106
|
+
*/
|
|
107
|
+
static notFound(message, options) {
|
|
108
|
+
return new AppError(message || 'Not Found', 404, options);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Create a 409 Conflict error.
|
|
113
|
+
* @param {string} [message='Conflict']
|
|
114
|
+
* @param {object} [options]
|
|
115
|
+
* @returns {AppError}
|
|
116
|
+
*/
|
|
117
|
+
static conflict(message, options) {
|
|
118
|
+
return new AppError(message || 'Conflict', 409, options);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Create a 422 Validation error with field-level details.
|
|
123
|
+
* @param {Array} errors - Array of { field, message } objects
|
|
124
|
+
* @param {string} [message='Validation Failed']
|
|
125
|
+
* @returns {AppError}
|
|
126
|
+
*/
|
|
127
|
+
static validation(errors, message) {
|
|
128
|
+
return new AppError(message || 'Validation Failed', 422, {
|
|
129
|
+
code: 'VALIDATION_ERROR',
|
|
130
|
+
errors: errors,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Create a 429 Too Many Requests error.
|
|
136
|
+
* @param {string} [message='Too Many Requests']
|
|
137
|
+
* @param {object} [options]
|
|
138
|
+
* @returns {AppError}
|
|
139
|
+
*/
|
|
140
|
+
static tooManyRequests(message, options) {
|
|
141
|
+
return new AppError(message || 'Too Many Requests', 429, options);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Create a 500 Internal Server Error.
|
|
146
|
+
* @param {string} [message='Internal Server Error']
|
|
147
|
+
* @param {object} [options]
|
|
148
|
+
* @returns {AppError}
|
|
149
|
+
*/
|
|
150
|
+
static internal(message, options) {
|
|
151
|
+
return new AppError(message || 'Internal Server Error', 500, {
|
|
152
|
+
...options,
|
|
153
|
+
isOperational: false,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Serialise the error to a plain object (useful for logging).
|
|
159
|
+
* @returns {object}
|
|
160
|
+
*/
|
|
161
|
+
toJSON() {
|
|
162
|
+
return {
|
|
163
|
+
name: this.name,
|
|
164
|
+
message: this.message,
|
|
165
|
+
statusCode: this.statusCode,
|
|
166
|
+
code: this.code,
|
|
167
|
+
errors: this.errors,
|
|
168
|
+
isOperational: this.isOperational,
|
|
169
|
+
timestamp: this.timestamp,
|
|
170
|
+
stack: this.stack,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
module.exports = AppError;
|