@erangamadhushan/express-error-handle-middleware 1.6.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 +345 -0
- package/dist/index.cjs +309 -0
- package/dist/index.d.cts +126 -0
- package/dist/index.d.ts +126 -0
- package/dist/index.js +263 -0
- package/package.json +71 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Eranga Madhushan (EM956)
|
|
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,345 @@
|
|
|
1
|
+
# @erangamadhushan/express-error-handle-middleware
|
|
2
|
+
|
|
3
|
+
Advanced TypeScript-based error handling middleware for Express.js.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## ✨ Features
|
|
13
|
+
|
|
14
|
+
- Async handler wrapper
|
|
15
|
+
- Custom `ApiError` class
|
|
16
|
+
- Predefined error classes (NotFound, BadRequest, etc.)
|
|
17
|
+
- `createError` helper for clean DX
|
|
18
|
+
- Global error middleware
|
|
19
|
+
- 404 Not Found middleware
|
|
20
|
+
- Logger integration (Pino, Winston, custom)
|
|
21
|
+
- Request ID and correlation metadata
|
|
22
|
+
- Structured logger context
|
|
23
|
+
- Custom error adapters
|
|
24
|
+
- Reusable adapter registry
|
|
25
|
+
- MongoDB duplicate key smart parsing
|
|
26
|
+
- Zod validation error formatting
|
|
27
|
+
- Standardized error response structure
|
|
28
|
+
- RFC 9457-style problem details
|
|
29
|
+
- Custom response serializers
|
|
30
|
+
- Configurable message exposure
|
|
31
|
+
- Production-safe stack handling
|
|
32
|
+
- ESM + CommonJS support
|
|
33
|
+
- Full TypeScript support
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 📦 Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install @erangamadhushan/express-error-handle-middleware
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Compatibility
|
|
44
|
+
|
|
45
|
+
- Node.js 18.18 or newer
|
|
46
|
+
- Express 4.18 or newer, including Express 5
|
|
47
|
+
- Zod 4 is optional and only required when using Zod validation errors
|
|
48
|
+
|
|
49
|
+
Express is a peer dependency because the middleware uses the host application's Express runtime. The package does not bundle Express, Zod, or other runtime dependencies.
|
|
50
|
+
|
|
51
|
+
## 🚀 Quick Start
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import express from "express";
|
|
55
|
+
import {
|
|
56
|
+
asyncHandler,
|
|
57
|
+
createError,
|
|
58
|
+
notFoundMiddleware,
|
|
59
|
+
errorMiddleware,
|
|
60
|
+
requestIdMiddleware,
|
|
61
|
+
} from "@erangamadhushan/express-error-handle-middleware";
|
|
62
|
+
|
|
63
|
+
const app = express();
|
|
64
|
+
app.use(express.json());
|
|
65
|
+
app.use(requestIdMiddleware());
|
|
66
|
+
|
|
67
|
+
app.get(
|
|
68
|
+
"/users/:id",
|
|
69
|
+
asyncHandler(async (req, res) => {
|
|
70
|
+
if (req.params.id !== "1") {
|
|
71
|
+
throw createError.notFound("User not found");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
res.json({ id: 1, name: "John" });
|
|
75
|
+
}),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
app.use(notFoundMiddleware);
|
|
79
|
+
app.use(errorMiddleware());
|
|
80
|
+
|
|
81
|
+
app.listen(5000);
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## 🧠 Recommended Workflow
|
|
85
|
+
|
|
86
|
+
- Wrap all controllers using asyncHandler.
|
|
87
|
+
- Throw errors using createError.* (recommended) or ApiError.
|
|
88
|
+
- Add requestIdMiddleware near the start of the application.
|
|
89
|
+
- Use global errorMiddleware.
|
|
90
|
+
- Integrate a logger in production.
|
|
91
|
+
- Use problem details or a custom serializer when an API-specific response contract is required.
|
|
92
|
+
|
|
93
|
+
## 🧩 Error Creation Options
|
|
94
|
+
|
|
95
|
+
### Using createError (Recommended)
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
throw createError.badRequest("Invalid input");
|
|
99
|
+
throw createError.notFound("User not found");
|
|
100
|
+
throw createError.unauthorized();
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Using ApiError
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
throw new ApiError("User not found", 404, "USER_NOT_FOUND");
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Using Predefined Classes
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
import { NotFoundError } from "...";
|
|
113
|
+
|
|
114
|
+
throw new NotFoundError("User not found");
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## 📤 Response Format
|
|
118
|
+
|
|
119
|
+
All errors follow a consistent structure:
|
|
120
|
+
|
|
121
|
+
```json
|
|
122
|
+
{
|
|
123
|
+
"success": false,
|
|
124
|
+
"statusCode": 404,
|
|
125
|
+
"message": "User not found",
|
|
126
|
+
"error": "NotFoundError",
|
|
127
|
+
"code": "NOT_FOUND"
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The legacy response format is the default for compatibility. Enable RFC 9457-style responses with `responseFormat: "problem"`:
|
|
132
|
+
|
|
133
|
+
```json
|
|
134
|
+
{
|
|
135
|
+
"type": "urn:express-error-kit:NOT_FOUND",
|
|
136
|
+
"title": "NotFoundError",
|
|
137
|
+
"status": 404,
|
|
138
|
+
"detail": "User not found",
|
|
139
|
+
"instance": "/users/42",
|
|
140
|
+
"code": "NOT_FOUND",
|
|
141
|
+
"requestId": "request-123"
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## 🧠 Smart MongoDB Error Handling
|
|
146
|
+
|
|
147
|
+
### Duplicate key errors are automatically formatted
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
// Mongo duplicate key error
|
|
151
|
+
{
|
|
152
|
+
code: 11000,
|
|
153
|
+
keyValue: { email: "test@example.com" }
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Response:
|
|
158
|
+
|
|
159
|
+
```json
|
|
160
|
+
{
|
|
161
|
+
"success": false,
|
|
162
|
+
"message": "email already exists",
|
|
163
|
+
"code": "DUPLICATE_FIELD"
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## 🧾 Zod Validation Formatting
|
|
168
|
+
|
|
169
|
+
If using Zod:
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
throw new ZodError([...]);
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Response:
|
|
176
|
+
|
|
177
|
+
```json
|
|
178
|
+
{
|
|
179
|
+
"success": false,
|
|
180
|
+
"message": "email: Expected string",
|
|
181
|
+
"code": "VALIDATION_ERROR"
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## 🪵 Logger Integration
|
|
186
|
+
|
|
187
|
+
Use any logger:
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
import pino from "pino";
|
|
191
|
+
|
|
192
|
+
const logger = pino();
|
|
193
|
+
|
|
194
|
+
app.use(
|
|
195
|
+
errorMiddleware({
|
|
196
|
+
logger: (error, context) => logger.error({ error, ...context }),
|
|
197
|
+
showStack: false,
|
|
198
|
+
}),
|
|
199
|
+
);
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
The logger receives the normalized error and structured request context containing `requestId`, `method`, `url`, `path`, `statusCode`, `code`, and `errorName`.
|
|
203
|
+
|
|
204
|
+
## 📚 Middleware Order (Important)
|
|
205
|
+
|
|
206
|
+
```js
|
|
207
|
+
app.use(routes);
|
|
208
|
+
|
|
209
|
+
app.use(notFoundMiddleware);
|
|
210
|
+
app.use(errorMiddleware());
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## 🧩 Creating Custom Errors
|
|
214
|
+
|
|
215
|
+
### Using createError (Recommended)
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
import { createError } from "@erangamadhushan/express-error-handle-middleware";
|
|
219
|
+
|
|
220
|
+
throw createError.notFound("User not found");
|
|
221
|
+
throw createError.badRequest("Invalid input");
|
|
222
|
+
throw createError.unauthorized();
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Using ApiError
|
|
226
|
+
|
|
227
|
+
```ts
|
|
228
|
+
import { ApiError } from "@erangamadhushan/express-error-handle-middleware";
|
|
229
|
+
|
|
230
|
+
throw new ApiError("User not found", 404, "USER_NOT_FOUND");
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
You can extend it like this:
|
|
234
|
+
|
|
235
|
+
```ts
|
|
236
|
+
import { ApiError } from "@erangamadhushan/express-error-handle-middleware";
|
|
237
|
+
|
|
238
|
+
class ValidationError extends ApiError {
|
|
239
|
+
constructor(message: string) {
|
|
240
|
+
super(message, 400, "VALIDATION_ERROR");
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## ⚙️ Configuration Options
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
errorMiddleware(options?: {
|
|
249
|
+
logger?: (error: unknown, context: ErrorLogContext) => void;
|
|
250
|
+
showStack?: boolean;
|
|
251
|
+
expose?: boolean | ((error: ApiError, context: ErrorRequestContext) => boolean);
|
|
252
|
+
responseFormat?: "legacy" | "problem";
|
|
253
|
+
serializer?: (error: ApiError, context: ErrorSerializationContext) => unknown;
|
|
254
|
+
requestId?: {
|
|
255
|
+
headerName?: string;
|
|
256
|
+
generator?: () => string;
|
|
257
|
+
};
|
|
258
|
+
adapters?: readonly ErrorAdapter[] | ErrorAdapterRegistry;
|
|
259
|
+
});
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
Request IDs are read from `x-request-id` by default, generated when absent, returned in the response header, and exposed through the logger and serializer context. Use `requestIdMiddleware()` near the start of the application to correlate successful requests as well as failures. Set `responseFormat: "problem"` for an RFC 9457-style response with `application/problem+json` content type.
|
|
263
|
+
|
|
264
|
+
Use `expose` to control whether an error message is returned. In production, 500-level messages are hidden by default.
|
|
265
|
+
|
|
266
|
+
Custom adapters can map application or library errors to `ApiError` instances. They run before the built-in MongoDB and Zod adapters.
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
import {
|
|
270
|
+
ApiError,
|
|
271
|
+
errorMiddleware,
|
|
272
|
+
} from "@erangamadhushan/express-error-handle-middleware";
|
|
273
|
+
|
|
274
|
+
const domainErrorAdapter = (error: unknown) => {
|
|
275
|
+
if (
|
|
276
|
+
typeof error === "object" &&
|
|
277
|
+
error !== null &&
|
|
278
|
+
"type" in error &&
|
|
279
|
+
error.type === "domain_error"
|
|
280
|
+
) {
|
|
281
|
+
return new ApiError("The resource is in an invalid state", 409, "INVALID_STATE");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return undefined;
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
app.use(errorMiddleware({ adapters: [domainErrorAdapter] }));
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
For shared application configuration, register adapters once and reuse the registry:
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
const adapters = new ErrorAdapterRegistry()
|
|
294
|
+
.register(domainErrorAdapter);
|
|
295
|
+
|
|
296
|
+
app.use(errorMiddleware({ adapters }));
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## 🛡 Production Behavior
|
|
300
|
+
|
|
301
|
+
- Stack traces hidden automatically in production
|
|
302
|
+
- 500-level messages hidden automatically in production
|
|
303
|
+
- Request IDs returned through the response header
|
|
304
|
+
- Structured context available to loggers and serializers
|
|
305
|
+
- Clean legacy JSON or problem-details response formats
|
|
306
|
+
- Centralized error normalization and exposure control
|
|
307
|
+
|
|
308
|
+
## 🧪 Testing
|
|
309
|
+
|
|
310
|
+
```bash
|
|
311
|
+
npm ci
|
|
312
|
+
npm test -- --runInBand
|
|
313
|
+
npx tsc --noEmit
|
|
314
|
+
npm run build
|
|
315
|
+
npm run smoke:package
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
## Docker Validation
|
|
319
|
+
|
|
320
|
+
The repository Dockerfile is a reproducible validation image for contributors and CI. It runs the typecheck, test suite, package build, and packed-package smoke test; it is not a production runtime image because this project publishes an npm library rather than an Express application.
|
|
321
|
+
|
|
322
|
+
```bash
|
|
323
|
+
docker build --tag express-error-handle-kit-validation .
|
|
324
|
+
docker run --rm express-error-handle-kit-validation
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
## 🔄 Automated Releases
|
|
328
|
+
|
|
329
|
+
- Conventional commits
|
|
330
|
+
- semantic-release
|
|
331
|
+
- GitHub Actions CI
|
|
332
|
+
- Automatic versioning and changelog generation
|
|
333
|
+
|
|
334
|
+
## Contributing
|
|
335
|
+
|
|
336
|
+
Contributions are welcome!
|
|
337
|
+
|
|
338
|
+
Please read CONTRIBUTING.md before opening a pull request.
|
|
339
|
+
|
|
340
|
+
## Engineering Documents
|
|
341
|
+
|
|
342
|
+
- [Architecture](ARCHITECTURE.md)
|
|
343
|
+
- [Error contract ADR](ADRs/001-error-contract.md)
|
|
344
|
+
- [Adapter system ADR](ADRs/002-adapter-system.md)
|
|
345
|
+
- [Compatibility matrix](COMPATIBILITY.md)
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
ApiError: () => ApiError,
|
|
24
|
+
BadRequestError: () => BadRequestError,
|
|
25
|
+
ConflictError: () => ConflictError,
|
|
26
|
+
ErrorAdapterRegistry: () => ErrorAdapterRegistry,
|
|
27
|
+
ForbiddenError: () => ForbiddenError,
|
|
28
|
+
InternalServerError: () => InternalServerError,
|
|
29
|
+
NotFoundError: () => NotFoundError,
|
|
30
|
+
UnauthorizedError: () => UnauthorizedError,
|
|
31
|
+
ValidationError: () => ValidationError,
|
|
32
|
+
asyncHandler: () => asyncHandler,
|
|
33
|
+
createError: () => createError,
|
|
34
|
+
createRequestContext: () => createRequestContext,
|
|
35
|
+
errorMiddleware: () => errorMiddleware,
|
|
36
|
+
getOrCreateRequestContext: () => getOrCreateRequestContext,
|
|
37
|
+
mongoDuplicateKeyAdapter: () => mongoDuplicateKeyAdapter,
|
|
38
|
+
normalizeError: () => normalizeError,
|
|
39
|
+
notFoundMiddleware: () => notFoundMiddleware,
|
|
40
|
+
requestIdMiddleware: () => requestIdMiddleware,
|
|
41
|
+
serializeError: () => serializeError,
|
|
42
|
+
zodErrorAdapter: () => zodErrorAdapter
|
|
43
|
+
});
|
|
44
|
+
module.exports = __toCommonJS(index_exports);
|
|
45
|
+
|
|
46
|
+
// src/asyncHandler.ts
|
|
47
|
+
var asyncHandler = (fn) => {
|
|
48
|
+
return (req, res, next) => {
|
|
49
|
+
Promise.resolve().then(() => fn(req, res, next)).catch(next);
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// src/ApiError.ts
|
|
54
|
+
var ApiError = class extends Error {
|
|
55
|
+
constructor(message, statusCode, code) {
|
|
56
|
+
super(message);
|
|
57
|
+
this.statusCode = statusCode;
|
|
58
|
+
this.code = code;
|
|
59
|
+
this.isOperational = true;
|
|
60
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
61
|
+
Error.captureStackTrace(this);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// src/adapters/mongoDuplicateKey.ts
|
|
66
|
+
var isRecord = (value) => typeof value === "object" && value !== null;
|
|
67
|
+
var mongoDuplicateKeyAdapter = (error) => {
|
|
68
|
+
if (!isRecord(error) || error.code !== 11e3) {
|
|
69
|
+
return void 0;
|
|
70
|
+
}
|
|
71
|
+
const keyValue = isRecord(error.keyValue) ? error.keyValue : void 0;
|
|
72
|
+
const field = keyValue ? Object.keys(keyValue)[0] : void 0;
|
|
73
|
+
const message = field ? `${field} already exists` : "Duplicate value";
|
|
74
|
+
return new ApiError(message, 400, "DUPLICATE_FIELD");
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// src/adapters/zod.ts
|
|
78
|
+
var isRecord2 = (value) => typeof value === "object" && value !== null;
|
|
79
|
+
var zodErrorAdapter = (error) => {
|
|
80
|
+
if (!isRecord2(error) || error.name !== "ZodError" || !Array.isArray(error.issues)) {
|
|
81
|
+
return void 0;
|
|
82
|
+
}
|
|
83
|
+
const message = error.issues.map((issue) => {
|
|
84
|
+
if (!isRecord2(issue)) {
|
|
85
|
+
return String(issue);
|
|
86
|
+
}
|
|
87
|
+
const path = Array.isArray(issue.path) ? issue.path.join(".") : "";
|
|
88
|
+
return path ? `${path}: ${String(issue.message)}` : String(issue.message);
|
|
89
|
+
}).join(", ");
|
|
90
|
+
return new ApiError(message || "Validation error", 400, "VALIDATION_ERROR");
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// src/types.ts
|
|
94
|
+
var ErrorAdapterRegistry = class {
|
|
95
|
+
constructor(adapters = []) {
|
|
96
|
+
this.registeredAdapters = [...adapters];
|
|
97
|
+
}
|
|
98
|
+
register(adapter) {
|
|
99
|
+
this.registeredAdapters.push(adapter);
|
|
100
|
+
return this;
|
|
101
|
+
}
|
|
102
|
+
registerMany(adapters) {
|
|
103
|
+
this.registeredAdapters.push(...adapters);
|
|
104
|
+
return this;
|
|
105
|
+
}
|
|
106
|
+
getAdapters() {
|
|
107
|
+
return this.registeredAdapters;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// src/normalizeError.ts
|
|
112
|
+
var defaultAdapters = [
|
|
113
|
+
mongoDuplicateKeyAdapter,
|
|
114
|
+
zodErrorAdapter
|
|
115
|
+
];
|
|
116
|
+
var normalizeError = (error, adapters = []) => {
|
|
117
|
+
if (error instanceof ApiError) {
|
|
118
|
+
return error;
|
|
119
|
+
}
|
|
120
|
+
const registeredAdapters = adapters instanceof ErrorAdapterRegistry ? adapters.getAdapters() : adapters;
|
|
121
|
+
for (const adapter of [...registeredAdapters, ...defaultAdapters]) {
|
|
122
|
+
const normalizedError = adapter(error);
|
|
123
|
+
if (normalizedError) {
|
|
124
|
+
return normalizedError;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return new ApiError("Internal Server Error", 500, "INTERNAL_ERROR");
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// src/requestContext.ts
|
|
131
|
+
var import_node_crypto = require("crypto");
|
|
132
|
+
var defaultHeaderName = "x-request-id";
|
|
133
|
+
var createRequestContext = (req, res, options = {}) => {
|
|
134
|
+
var _a, _b, _c;
|
|
135
|
+
const headerName = (_a = options.headerName) != null ? _a : defaultHeaderName;
|
|
136
|
+
const incomingRequestId = (_b = req.get(headerName)) == null ? void 0 : _b.trim();
|
|
137
|
+
const requestId = incomingRequestId || ((_c = options.generator) != null ? _c : import_node_crypto.randomUUID)();
|
|
138
|
+
res.setHeader(headerName, requestId);
|
|
139
|
+
res.locals.requestId = requestId;
|
|
140
|
+
return {
|
|
141
|
+
requestId,
|
|
142
|
+
method: req.method,
|
|
143
|
+
url: req.originalUrl || req.url,
|
|
144
|
+
path: req.path
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
var requestIdMiddleware = (options = {}) => (req, res, next) => {
|
|
148
|
+
createRequestContext(req, res, options);
|
|
149
|
+
next();
|
|
150
|
+
};
|
|
151
|
+
var getOrCreateRequestContext = (req, res, options = {}) => {
|
|
152
|
+
const existingRequestId = res.locals.requestId;
|
|
153
|
+
if (typeof existingRequestId === "string" && existingRequestId.length > 0) {
|
|
154
|
+
return {
|
|
155
|
+
requestId: existingRequestId,
|
|
156
|
+
method: req.method,
|
|
157
|
+
url: req.originalUrl || req.url,
|
|
158
|
+
path: req.path
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
return createRequestContext(req, res, options);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// src/serializeError.ts
|
|
165
|
+
var serializeError = (error, options = {}) => {
|
|
166
|
+
var _a, _b, _c, _d;
|
|
167
|
+
const isProduction = (_a = options.isProduction) != null ? _a : process.env.NODE_ENV === "production";
|
|
168
|
+
const exposeMessage = (_b = options.exposeMessage) != null ? _b : !(isProduction && error.statusCode >= 500);
|
|
169
|
+
const message = exposeMessage ? error.message : "Internal Server Error";
|
|
170
|
+
const includeStack = options.showStack === true && !isProduction;
|
|
171
|
+
if (options.format === "problem") {
|
|
172
|
+
const problem = {
|
|
173
|
+
type: error.code ? `urn:express-error-kit:${error.code}` : "about:blank",
|
|
174
|
+
title: error.constructor.name,
|
|
175
|
+
status: error.statusCode,
|
|
176
|
+
...message && { detail: message },
|
|
177
|
+
...((_c = options.context) == null ? void 0 : _c.url) && { instance: options.context.url },
|
|
178
|
+
...error.code && { code: error.code },
|
|
179
|
+
...((_d = options.context) == null ? void 0 : _d.requestId) && { requestId: options.context.requestId }
|
|
180
|
+
};
|
|
181
|
+
return problem;
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
success: false,
|
|
185
|
+
statusCode: error.statusCode,
|
|
186
|
+
message,
|
|
187
|
+
error: error.constructor.name,
|
|
188
|
+
...error.code && { code: error.code },
|
|
189
|
+
...includeStack && error.stack ? { stack: error.stack } : {}
|
|
190
|
+
};
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// src/errorMiddleware.ts
|
|
194
|
+
var errorMiddleware = (options = {}) => (err, req, res, next) => {
|
|
195
|
+
var _a, _b;
|
|
196
|
+
if (res.headersSent) {
|
|
197
|
+
next(err);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const requestContext = getOrCreateRequestContext(req, res, options.requestId);
|
|
201
|
+
const processedError = normalizeError(err, (_a = options.adapters) != null ? _a : []);
|
|
202
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
203
|
+
const exposeMessage = typeof options.expose === "function" ? options.expose(processedError, requestContext) : (_b = options.expose) != null ? _b : !(isProduction && processedError.statusCode >= 500);
|
|
204
|
+
const serializationContext = {
|
|
205
|
+
...requestContext,
|
|
206
|
+
isProduction,
|
|
207
|
+
exposeMessage,
|
|
208
|
+
includeStack: options.showStack === true && !isProduction
|
|
209
|
+
};
|
|
210
|
+
const logContext = {
|
|
211
|
+
...requestContext,
|
|
212
|
+
statusCode: processedError.statusCode,
|
|
213
|
+
code: processedError.code,
|
|
214
|
+
errorName: processedError.constructor.name
|
|
215
|
+
};
|
|
216
|
+
if (options.logger) {
|
|
217
|
+
options.logger(processedError, logContext);
|
|
218
|
+
} else {
|
|
219
|
+
console.error({ error: processedError, context: logContext });
|
|
220
|
+
}
|
|
221
|
+
const response = options.serializer ? options.serializer(processedError, serializationContext) : serializeError(processedError, {
|
|
222
|
+
exposeMessage,
|
|
223
|
+
format: options.responseFormat,
|
|
224
|
+
showStack: options.showStack,
|
|
225
|
+
context: serializationContext
|
|
226
|
+
});
|
|
227
|
+
if (options.responseFormat === "problem") {
|
|
228
|
+
res.type("application/problem+json");
|
|
229
|
+
}
|
|
230
|
+
res.status(processedError.statusCode).json(response);
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// src/notFoundMiddleware.ts
|
|
234
|
+
var notFoundMiddleware = (req, res, next) => {
|
|
235
|
+
next(new ApiError("Route not found", 404, "NOT_FOUND"));
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// src/errors.ts
|
|
239
|
+
var NotFoundError = class extends ApiError {
|
|
240
|
+
constructor(message = "Resource not found") {
|
|
241
|
+
super(message, 404, "NOT_FOUND");
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
var BadRequestError = class extends ApiError {
|
|
245
|
+
constructor(message = "Bad request") {
|
|
246
|
+
super(message, 400, "BAD_REQUEST");
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
var UnauthorizedError = class extends ApiError {
|
|
250
|
+
constructor(message = "Unauthorized") {
|
|
251
|
+
super(message, 401, "UNAUTHORIZED");
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
var ForbiddenError = class extends ApiError {
|
|
255
|
+
constructor(message = "Forbidden") {
|
|
256
|
+
super(message, 403, "FORBIDDEN");
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
var ConflictError = class extends ApiError {
|
|
260
|
+
constructor(message = "Conflict") {
|
|
261
|
+
super(message, 409, "CONFLICT");
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
var ValidationError = class extends ApiError {
|
|
265
|
+
constructor(message = "Validation error") {
|
|
266
|
+
super(message, 400, "VALIDATION_ERROR");
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
var InternalServerError = class extends ApiError {
|
|
270
|
+
constructor(message = "Internal Server Error") {
|
|
271
|
+
super(message, 500, "INTERNAL_ERROR");
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
// src/createError.ts
|
|
276
|
+
var createError = {
|
|
277
|
+
badRequest: (message = "Bad request") => new BadRequestError(message),
|
|
278
|
+
unauthorized: (message = "Unauthorized") => new UnauthorizedError(message),
|
|
279
|
+
forbidden: (message = "Forbidden") => new ForbiddenError(message),
|
|
280
|
+
notFound: (message = "Resource not found") => new NotFoundError(message),
|
|
281
|
+
conflict: (message = "Conflict") => new ConflictError(message),
|
|
282
|
+
validation: (message = "Validation error") => new ValidationError(message),
|
|
283
|
+
internal: (message = "Internal Server Error") => new InternalServerError(message),
|
|
284
|
+
// Custom flexible error
|
|
285
|
+
custom: (message, statusCode, code) => new ApiError(message, statusCode, code)
|
|
286
|
+
};
|
|
287
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
288
|
+
0 && (module.exports = {
|
|
289
|
+
ApiError,
|
|
290
|
+
BadRequestError,
|
|
291
|
+
ConflictError,
|
|
292
|
+
ErrorAdapterRegistry,
|
|
293
|
+
ForbiddenError,
|
|
294
|
+
InternalServerError,
|
|
295
|
+
NotFoundError,
|
|
296
|
+
UnauthorizedError,
|
|
297
|
+
ValidationError,
|
|
298
|
+
asyncHandler,
|
|
299
|
+
createError,
|
|
300
|
+
createRequestContext,
|
|
301
|
+
errorMiddleware,
|
|
302
|
+
getOrCreateRequestContext,
|
|
303
|
+
mongoDuplicateKeyAdapter,
|
|
304
|
+
normalizeError,
|
|
305
|
+
notFoundMiddleware,
|
|
306
|
+
requestIdMiddleware,
|
|
307
|
+
serializeError,
|
|
308
|
+
zodErrorAdapter
|
|
309
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
|
|
3
|
+
declare const asyncHandler: (fn: (req: Request, res: Response, next: NextFunction) => Promise<unknown> | unknown) => (req: Request, res: Response, next: NextFunction) => void;
|
|
4
|
+
|
|
5
|
+
declare class ApiError extends Error {
|
|
6
|
+
statusCode: number;
|
|
7
|
+
code?: string;
|
|
8
|
+
isOperational: boolean;
|
|
9
|
+
constructor(message: string, statusCode: number, code?: string);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type ErrorAdapter = (error: unknown) => ApiError | undefined;
|
|
13
|
+
interface ErrorRequestContext {
|
|
14
|
+
requestId: string;
|
|
15
|
+
method: string;
|
|
16
|
+
url: string;
|
|
17
|
+
path: string;
|
|
18
|
+
}
|
|
19
|
+
interface ErrorLogContext extends ErrorRequestContext {
|
|
20
|
+
statusCode: number;
|
|
21
|
+
code?: string;
|
|
22
|
+
errorName: string;
|
|
23
|
+
}
|
|
24
|
+
interface ErrorSerializationContext extends ErrorRequestContext {
|
|
25
|
+
isProduction: boolean;
|
|
26
|
+
exposeMessage: boolean;
|
|
27
|
+
includeStack: boolean;
|
|
28
|
+
}
|
|
29
|
+
type ErrorExposurePolicy = boolean | ((error: ApiError, context: ErrorRequestContext) => boolean);
|
|
30
|
+
type ErrorResponseSerializer = (error: ApiError, context: ErrorSerializationContext) => unknown;
|
|
31
|
+
type ErrorResponseFormat = "legacy" | "problem";
|
|
32
|
+
interface RequestIdOptions {
|
|
33
|
+
headerName?: string;
|
|
34
|
+
generator?: () => string;
|
|
35
|
+
}
|
|
36
|
+
interface ErrorMiddlewareOptions {
|
|
37
|
+
logger?: (error: unknown, context: ErrorLogContext) => void;
|
|
38
|
+
showStack?: boolean;
|
|
39
|
+
expose?: ErrorExposurePolicy;
|
|
40
|
+
responseFormat?: ErrorResponseFormat;
|
|
41
|
+
serializer?: ErrorResponseSerializer;
|
|
42
|
+
requestId?: RequestIdOptions;
|
|
43
|
+
adapters?: readonly ErrorAdapter[] | ErrorAdapterRegistry;
|
|
44
|
+
}
|
|
45
|
+
declare class ErrorAdapterRegistry {
|
|
46
|
+
private readonly registeredAdapters;
|
|
47
|
+
constructor(adapters?: readonly ErrorAdapter[]);
|
|
48
|
+
register(adapter: ErrorAdapter): this;
|
|
49
|
+
registerMany(adapters: readonly ErrorAdapter[]): this;
|
|
50
|
+
getAdapters(): readonly ErrorAdapter[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
declare const errorMiddleware: (options?: ErrorMiddlewareOptions) => (err: unknown, req: Request, res: Response, next: NextFunction) => void;
|
|
54
|
+
|
|
55
|
+
declare const notFoundMiddleware: (req: Request, res: Response, next: NextFunction) => void;
|
|
56
|
+
|
|
57
|
+
declare class NotFoundError extends ApiError {
|
|
58
|
+
constructor(message?: string);
|
|
59
|
+
}
|
|
60
|
+
declare class BadRequestError extends ApiError {
|
|
61
|
+
constructor(message?: string);
|
|
62
|
+
}
|
|
63
|
+
declare class UnauthorizedError extends ApiError {
|
|
64
|
+
constructor(message?: string);
|
|
65
|
+
}
|
|
66
|
+
declare class ForbiddenError extends ApiError {
|
|
67
|
+
constructor(message?: string);
|
|
68
|
+
}
|
|
69
|
+
declare class ConflictError extends ApiError {
|
|
70
|
+
constructor(message?: string);
|
|
71
|
+
}
|
|
72
|
+
declare class ValidationError extends ApiError {
|
|
73
|
+
constructor(message?: string);
|
|
74
|
+
}
|
|
75
|
+
declare class InternalServerError extends ApiError {
|
|
76
|
+
constructor(message?: string);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
declare const createError: {
|
|
80
|
+
badRequest: (message?: string) => BadRequestError;
|
|
81
|
+
unauthorized: (message?: string) => UnauthorizedError;
|
|
82
|
+
forbidden: (message?: string) => ForbiddenError;
|
|
83
|
+
notFound: (message?: string) => NotFoundError;
|
|
84
|
+
conflict: (message?: string) => ConflictError;
|
|
85
|
+
validation: (message?: string) => ValidationError;
|
|
86
|
+
internal: (message?: string) => InternalServerError;
|
|
87
|
+
custom: (message: string, statusCode: number, code?: string) => ApiError;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
declare const normalizeError: (error: unknown, adapters?: readonly ErrorAdapter[] | ErrorAdapterRegistry) => ApiError;
|
|
91
|
+
|
|
92
|
+
interface SerializedError {
|
|
93
|
+
success: false;
|
|
94
|
+
statusCode: number;
|
|
95
|
+
message: string;
|
|
96
|
+
error: string;
|
|
97
|
+
code?: string;
|
|
98
|
+
stack?: string;
|
|
99
|
+
}
|
|
100
|
+
interface SerializeErrorOptions {
|
|
101
|
+
isProduction?: boolean;
|
|
102
|
+
showStack?: boolean;
|
|
103
|
+
exposeMessage?: boolean;
|
|
104
|
+
format?: ErrorResponseFormat;
|
|
105
|
+
context?: ErrorSerializationContext;
|
|
106
|
+
}
|
|
107
|
+
interface ProblemDetails {
|
|
108
|
+
type: string;
|
|
109
|
+
title: string;
|
|
110
|
+
status: number;
|
|
111
|
+
detail?: string;
|
|
112
|
+
instance?: string;
|
|
113
|
+
code?: string;
|
|
114
|
+
requestId?: string;
|
|
115
|
+
}
|
|
116
|
+
declare const serializeError: (error: ApiError, options?: SerializeErrorOptions) => SerializedError | ProblemDetails;
|
|
117
|
+
|
|
118
|
+
declare const mongoDuplicateKeyAdapter: ErrorAdapter;
|
|
119
|
+
|
|
120
|
+
declare const zodErrorAdapter: ErrorAdapter;
|
|
121
|
+
|
|
122
|
+
declare const createRequestContext: (req: Request, res: Response, options?: RequestIdOptions) => ErrorRequestContext;
|
|
123
|
+
declare const requestIdMiddleware: (options?: RequestIdOptions) => (req: Request, res: Response, next: NextFunction) => void;
|
|
124
|
+
declare const getOrCreateRequestContext: (req: Request, res: Response, options?: RequestIdOptions) => ErrorRequestContext;
|
|
125
|
+
|
|
126
|
+
export { ApiError, BadRequestError, ConflictError, type ErrorAdapter, ErrorAdapterRegistry, type ErrorExposurePolicy, type ErrorLogContext, type ErrorMiddlewareOptions, type ErrorRequestContext, type ErrorResponseFormat, type ErrorResponseSerializer, type ErrorSerializationContext, ForbiddenError, InternalServerError, NotFoundError, type ProblemDetails, type RequestIdOptions, type SerializeErrorOptions, type SerializedError, UnauthorizedError, ValidationError, asyncHandler, createError, createRequestContext, errorMiddleware, getOrCreateRequestContext, mongoDuplicateKeyAdapter, normalizeError, notFoundMiddleware, requestIdMiddleware, serializeError, zodErrorAdapter };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
|
|
3
|
+
declare const asyncHandler: (fn: (req: Request, res: Response, next: NextFunction) => Promise<unknown> | unknown) => (req: Request, res: Response, next: NextFunction) => void;
|
|
4
|
+
|
|
5
|
+
declare class ApiError extends Error {
|
|
6
|
+
statusCode: number;
|
|
7
|
+
code?: string;
|
|
8
|
+
isOperational: boolean;
|
|
9
|
+
constructor(message: string, statusCode: number, code?: string);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type ErrorAdapter = (error: unknown) => ApiError | undefined;
|
|
13
|
+
interface ErrorRequestContext {
|
|
14
|
+
requestId: string;
|
|
15
|
+
method: string;
|
|
16
|
+
url: string;
|
|
17
|
+
path: string;
|
|
18
|
+
}
|
|
19
|
+
interface ErrorLogContext extends ErrorRequestContext {
|
|
20
|
+
statusCode: number;
|
|
21
|
+
code?: string;
|
|
22
|
+
errorName: string;
|
|
23
|
+
}
|
|
24
|
+
interface ErrorSerializationContext extends ErrorRequestContext {
|
|
25
|
+
isProduction: boolean;
|
|
26
|
+
exposeMessage: boolean;
|
|
27
|
+
includeStack: boolean;
|
|
28
|
+
}
|
|
29
|
+
type ErrorExposurePolicy = boolean | ((error: ApiError, context: ErrorRequestContext) => boolean);
|
|
30
|
+
type ErrorResponseSerializer = (error: ApiError, context: ErrorSerializationContext) => unknown;
|
|
31
|
+
type ErrorResponseFormat = "legacy" | "problem";
|
|
32
|
+
interface RequestIdOptions {
|
|
33
|
+
headerName?: string;
|
|
34
|
+
generator?: () => string;
|
|
35
|
+
}
|
|
36
|
+
interface ErrorMiddlewareOptions {
|
|
37
|
+
logger?: (error: unknown, context: ErrorLogContext) => void;
|
|
38
|
+
showStack?: boolean;
|
|
39
|
+
expose?: ErrorExposurePolicy;
|
|
40
|
+
responseFormat?: ErrorResponseFormat;
|
|
41
|
+
serializer?: ErrorResponseSerializer;
|
|
42
|
+
requestId?: RequestIdOptions;
|
|
43
|
+
adapters?: readonly ErrorAdapter[] | ErrorAdapterRegistry;
|
|
44
|
+
}
|
|
45
|
+
declare class ErrorAdapterRegistry {
|
|
46
|
+
private readonly registeredAdapters;
|
|
47
|
+
constructor(adapters?: readonly ErrorAdapter[]);
|
|
48
|
+
register(adapter: ErrorAdapter): this;
|
|
49
|
+
registerMany(adapters: readonly ErrorAdapter[]): this;
|
|
50
|
+
getAdapters(): readonly ErrorAdapter[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
declare const errorMiddleware: (options?: ErrorMiddlewareOptions) => (err: unknown, req: Request, res: Response, next: NextFunction) => void;
|
|
54
|
+
|
|
55
|
+
declare const notFoundMiddleware: (req: Request, res: Response, next: NextFunction) => void;
|
|
56
|
+
|
|
57
|
+
declare class NotFoundError extends ApiError {
|
|
58
|
+
constructor(message?: string);
|
|
59
|
+
}
|
|
60
|
+
declare class BadRequestError extends ApiError {
|
|
61
|
+
constructor(message?: string);
|
|
62
|
+
}
|
|
63
|
+
declare class UnauthorizedError extends ApiError {
|
|
64
|
+
constructor(message?: string);
|
|
65
|
+
}
|
|
66
|
+
declare class ForbiddenError extends ApiError {
|
|
67
|
+
constructor(message?: string);
|
|
68
|
+
}
|
|
69
|
+
declare class ConflictError extends ApiError {
|
|
70
|
+
constructor(message?: string);
|
|
71
|
+
}
|
|
72
|
+
declare class ValidationError extends ApiError {
|
|
73
|
+
constructor(message?: string);
|
|
74
|
+
}
|
|
75
|
+
declare class InternalServerError extends ApiError {
|
|
76
|
+
constructor(message?: string);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
declare const createError: {
|
|
80
|
+
badRequest: (message?: string) => BadRequestError;
|
|
81
|
+
unauthorized: (message?: string) => UnauthorizedError;
|
|
82
|
+
forbidden: (message?: string) => ForbiddenError;
|
|
83
|
+
notFound: (message?: string) => NotFoundError;
|
|
84
|
+
conflict: (message?: string) => ConflictError;
|
|
85
|
+
validation: (message?: string) => ValidationError;
|
|
86
|
+
internal: (message?: string) => InternalServerError;
|
|
87
|
+
custom: (message: string, statusCode: number, code?: string) => ApiError;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
declare const normalizeError: (error: unknown, adapters?: readonly ErrorAdapter[] | ErrorAdapterRegistry) => ApiError;
|
|
91
|
+
|
|
92
|
+
interface SerializedError {
|
|
93
|
+
success: false;
|
|
94
|
+
statusCode: number;
|
|
95
|
+
message: string;
|
|
96
|
+
error: string;
|
|
97
|
+
code?: string;
|
|
98
|
+
stack?: string;
|
|
99
|
+
}
|
|
100
|
+
interface SerializeErrorOptions {
|
|
101
|
+
isProduction?: boolean;
|
|
102
|
+
showStack?: boolean;
|
|
103
|
+
exposeMessage?: boolean;
|
|
104
|
+
format?: ErrorResponseFormat;
|
|
105
|
+
context?: ErrorSerializationContext;
|
|
106
|
+
}
|
|
107
|
+
interface ProblemDetails {
|
|
108
|
+
type: string;
|
|
109
|
+
title: string;
|
|
110
|
+
status: number;
|
|
111
|
+
detail?: string;
|
|
112
|
+
instance?: string;
|
|
113
|
+
code?: string;
|
|
114
|
+
requestId?: string;
|
|
115
|
+
}
|
|
116
|
+
declare const serializeError: (error: ApiError, options?: SerializeErrorOptions) => SerializedError | ProblemDetails;
|
|
117
|
+
|
|
118
|
+
declare const mongoDuplicateKeyAdapter: ErrorAdapter;
|
|
119
|
+
|
|
120
|
+
declare const zodErrorAdapter: ErrorAdapter;
|
|
121
|
+
|
|
122
|
+
declare const createRequestContext: (req: Request, res: Response, options?: RequestIdOptions) => ErrorRequestContext;
|
|
123
|
+
declare const requestIdMiddleware: (options?: RequestIdOptions) => (req: Request, res: Response, next: NextFunction) => void;
|
|
124
|
+
declare const getOrCreateRequestContext: (req: Request, res: Response, options?: RequestIdOptions) => ErrorRequestContext;
|
|
125
|
+
|
|
126
|
+
export { ApiError, BadRequestError, ConflictError, type ErrorAdapter, ErrorAdapterRegistry, type ErrorExposurePolicy, type ErrorLogContext, type ErrorMiddlewareOptions, type ErrorRequestContext, type ErrorResponseFormat, type ErrorResponseSerializer, type ErrorSerializationContext, ForbiddenError, InternalServerError, NotFoundError, type ProblemDetails, type RequestIdOptions, type SerializeErrorOptions, type SerializedError, UnauthorizedError, ValidationError, asyncHandler, createError, createRequestContext, errorMiddleware, getOrCreateRequestContext, mongoDuplicateKeyAdapter, normalizeError, notFoundMiddleware, requestIdMiddleware, serializeError, zodErrorAdapter };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
// src/asyncHandler.ts
|
|
2
|
+
var asyncHandler = (fn) => {
|
|
3
|
+
return (req, res, next) => {
|
|
4
|
+
Promise.resolve().then(() => fn(req, res, next)).catch(next);
|
|
5
|
+
};
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
// src/ApiError.ts
|
|
9
|
+
var ApiError = class extends Error {
|
|
10
|
+
constructor(message, statusCode, code) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.statusCode = statusCode;
|
|
13
|
+
this.code = code;
|
|
14
|
+
this.isOperational = true;
|
|
15
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
16
|
+
Error.captureStackTrace(this);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// src/adapters/mongoDuplicateKey.ts
|
|
21
|
+
var isRecord = (value) => typeof value === "object" && value !== null;
|
|
22
|
+
var mongoDuplicateKeyAdapter = (error) => {
|
|
23
|
+
if (!isRecord(error) || error.code !== 11e3) {
|
|
24
|
+
return void 0;
|
|
25
|
+
}
|
|
26
|
+
const keyValue = isRecord(error.keyValue) ? error.keyValue : void 0;
|
|
27
|
+
const field = keyValue ? Object.keys(keyValue)[0] : void 0;
|
|
28
|
+
const message = field ? `${field} already exists` : "Duplicate value";
|
|
29
|
+
return new ApiError(message, 400, "DUPLICATE_FIELD");
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// src/adapters/zod.ts
|
|
33
|
+
var isRecord2 = (value) => typeof value === "object" && value !== null;
|
|
34
|
+
var zodErrorAdapter = (error) => {
|
|
35
|
+
if (!isRecord2(error) || error.name !== "ZodError" || !Array.isArray(error.issues)) {
|
|
36
|
+
return void 0;
|
|
37
|
+
}
|
|
38
|
+
const message = error.issues.map((issue) => {
|
|
39
|
+
if (!isRecord2(issue)) {
|
|
40
|
+
return String(issue);
|
|
41
|
+
}
|
|
42
|
+
const path = Array.isArray(issue.path) ? issue.path.join(".") : "";
|
|
43
|
+
return path ? `${path}: ${String(issue.message)}` : String(issue.message);
|
|
44
|
+
}).join(", ");
|
|
45
|
+
return new ApiError(message || "Validation error", 400, "VALIDATION_ERROR");
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// src/types.ts
|
|
49
|
+
var ErrorAdapterRegistry = class {
|
|
50
|
+
constructor(adapters = []) {
|
|
51
|
+
this.registeredAdapters = [...adapters];
|
|
52
|
+
}
|
|
53
|
+
register(adapter) {
|
|
54
|
+
this.registeredAdapters.push(adapter);
|
|
55
|
+
return this;
|
|
56
|
+
}
|
|
57
|
+
registerMany(adapters) {
|
|
58
|
+
this.registeredAdapters.push(...adapters);
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
getAdapters() {
|
|
62
|
+
return this.registeredAdapters;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// src/normalizeError.ts
|
|
67
|
+
var defaultAdapters = [
|
|
68
|
+
mongoDuplicateKeyAdapter,
|
|
69
|
+
zodErrorAdapter
|
|
70
|
+
];
|
|
71
|
+
var normalizeError = (error, adapters = []) => {
|
|
72
|
+
if (error instanceof ApiError) {
|
|
73
|
+
return error;
|
|
74
|
+
}
|
|
75
|
+
const registeredAdapters = adapters instanceof ErrorAdapterRegistry ? adapters.getAdapters() : adapters;
|
|
76
|
+
for (const adapter of [...registeredAdapters, ...defaultAdapters]) {
|
|
77
|
+
const normalizedError = adapter(error);
|
|
78
|
+
if (normalizedError) {
|
|
79
|
+
return normalizedError;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return new ApiError("Internal Server Error", 500, "INTERNAL_ERROR");
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// src/requestContext.ts
|
|
86
|
+
import { randomUUID } from "crypto";
|
|
87
|
+
var defaultHeaderName = "x-request-id";
|
|
88
|
+
var createRequestContext = (req, res, options = {}) => {
|
|
89
|
+
var _a, _b, _c;
|
|
90
|
+
const headerName = (_a = options.headerName) != null ? _a : defaultHeaderName;
|
|
91
|
+
const incomingRequestId = (_b = req.get(headerName)) == null ? void 0 : _b.trim();
|
|
92
|
+
const requestId = incomingRequestId || ((_c = options.generator) != null ? _c : randomUUID)();
|
|
93
|
+
res.setHeader(headerName, requestId);
|
|
94
|
+
res.locals.requestId = requestId;
|
|
95
|
+
return {
|
|
96
|
+
requestId,
|
|
97
|
+
method: req.method,
|
|
98
|
+
url: req.originalUrl || req.url,
|
|
99
|
+
path: req.path
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
var requestIdMiddleware = (options = {}) => (req, res, next) => {
|
|
103
|
+
createRequestContext(req, res, options);
|
|
104
|
+
next();
|
|
105
|
+
};
|
|
106
|
+
var getOrCreateRequestContext = (req, res, options = {}) => {
|
|
107
|
+
const existingRequestId = res.locals.requestId;
|
|
108
|
+
if (typeof existingRequestId === "string" && existingRequestId.length > 0) {
|
|
109
|
+
return {
|
|
110
|
+
requestId: existingRequestId,
|
|
111
|
+
method: req.method,
|
|
112
|
+
url: req.originalUrl || req.url,
|
|
113
|
+
path: req.path
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return createRequestContext(req, res, options);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// src/serializeError.ts
|
|
120
|
+
var serializeError = (error, options = {}) => {
|
|
121
|
+
var _a, _b, _c, _d;
|
|
122
|
+
const isProduction = (_a = options.isProduction) != null ? _a : process.env.NODE_ENV === "production";
|
|
123
|
+
const exposeMessage = (_b = options.exposeMessage) != null ? _b : !(isProduction && error.statusCode >= 500);
|
|
124
|
+
const message = exposeMessage ? error.message : "Internal Server Error";
|
|
125
|
+
const includeStack = options.showStack === true && !isProduction;
|
|
126
|
+
if (options.format === "problem") {
|
|
127
|
+
const problem = {
|
|
128
|
+
type: error.code ? `urn:express-error-kit:${error.code}` : "about:blank",
|
|
129
|
+
title: error.constructor.name,
|
|
130
|
+
status: error.statusCode,
|
|
131
|
+
...message && { detail: message },
|
|
132
|
+
...((_c = options.context) == null ? void 0 : _c.url) && { instance: options.context.url },
|
|
133
|
+
...error.code && { code: error.code },
|
|
134
|
+
...((_d = options.context) == null ? void 0 : _d.requestId) && { requestId: options.context.requestId }
|
|
135
|
+
};
|
|
136
|
+
return problem;
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
success: false,
|
|
140
|
+
statusCode: error.statusCode,
|
|
141
|
+
message,
|
|
142
|
+
error: error.constructor.name,
|
|
143
|
+
...error.code && { code: error.code },
|
|
144
|
+
...includeStack && error.stack ? { stack: error.stack } : {}
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// src/errorMiddleware.ts
|
|
149
|
+
var errorMiddleware = (options = {}) => (err, req, res, next) => {
|
|
150
|
+
var _a, _b;
|
|
151
|
+
if (res.headersSent) {
|
|
152
|
+
next(err);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const requestContext = getOrCreateRequestContext(req, res, options.requestId);
|
|
156
|
+
const processedError = normalizeError(err, (_a = options.adapters) != null ? _a : []);
|
|
157
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
158
|
+
const exposeMessage = typeof options.expose === "function" ? options.expose(processedError, requestContext) : (_b = options.expose) != null ? _b : !(isProduction && processedError.statusCode >= 500);
|
|
159
|
+
const serializationContext = {
|
|
160
|
+
...requestContext,
|
|
161
|
+
isProduction,
|
|
162
|
+
exposeMessage,
|
|
163
|
+
includeStack: options.showStack === true && !isProduction
|
|
164
|
+
};
|
|
165
|
+
const logContext = {
|
|
166
|
+
...requestContext,
|
|
167
|
+
statusCode: processedError.statusCode,
|
|
168
|
+
code: processedError.code,
|
|
169
|
+
errorName: processedError.constructor.name
|
|
170
|
+
};
|
|
171
|
+
if (options.logger) {
|
|
172
|
+
options.logger(processedError, logContext);
|
|
173
|
+
} else {
|
|
174
|
+
console.error({ error: processedError, context: logContext });
|
|
175
|
+
}
|
|
176
|
+
const response = options.serializer ? options.serializer(processedError, serializationContext) : serializeError(processedError, {
|
|
177
|
+
exposeMessage,
|
|
178
|
+
format: options.responseFormat,
|
|
179
|
+
showStack: options.showStack,
|
|
180
|
+
context: serializationContext
|
|
181
|
+
});
|
|
182
|
+
if (options.responseFormat === "problem") {
|
|
183
|
+
res.type("application/problem+json");
|
|
184
|
+
}
|
|
185
|
+
res.status(processedError.statusCode).json(response);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
// src/notFoundMiddleware.ts
|
|
189
|
+
var notFoundMiddleware = (req, res, next) => {
|
|
190
|
+
next(new ApiError("Route not found", 404, "NOT_FOUND"));
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// src/errors.ts
|
|
194
|
+
var NotFoundError = class extends ApiError {
|
|
195
|
+
constructor(message = "Resource not found") {
|
|
196
|
+
super(message, 404, "NOT_FOUND");
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
var BadRequestError = class extends ApiError {
|
|
200
|
+
constructor(message = "Bad request") {
|
|
201
|
+
super(message, 400, "BAD_REQUEST");
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
var UnauthorizedError = class extends ApiError {
|
|
205
|
+
constructor(message = "Unauthorized") {
|
|
206
|
+
super(message, 401, "UNAUTHORIZED");
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
var ForbiddenError = class extends ApiError {
|
|
210
|
+
constructor(message = "Forbidden") {
|
|
211
|
+
super(message, 403, "FORBIDDEN");
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
var ConflictError = class extends ApiError {
|
|
215
|
+
constructor(message = "Conflict") {
|
|
216
|
+
super(message, 409, "CONFLICT");
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
var ValidationError = class extends ApiError {
|
|
220
|
+
constructor(message = "Validation error") {
|
|
221
|
+
super(message, 400, "VALIDATION_ERROR");
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
var InternalServerError = class extends ApiError {
|
|
225
|
+
constructor(message = "Internal Server Error") {
|
|
226
|
+
super(message, 500, "INTERNAL_ERROR");
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
// src/createError.ts
|
|
231
|
+
var createError = {
|
|
232
|
+
badRequest: (message = "Bad request") => new BadRequestError(message),
|
|
233
|
+
unauthorized: (message = "Unauthorized") => new UnauthorizedError(message),
|
|
234
|
+
forbidden: (message = "Forbidden") => new ForbiddenError(message),
|
|
235
|
+
notFound: (message = "Resource not found") => new NotFoundError(message),
|
|
236
|
+
conflict: (message = "Conflict") => new ConflictError(message),
|
|
237
|
+
validation: (message = "Validation error") => new ValidationError(message),
|
|
238
|
+
internal: (message = "Internal Server Error") => new InternalServerError(message),
|
|
239
|
+
// Custom flexible error
|
|
240
|
+
custom: (message, statusCode, code) => new ApiError(message, statusCode, code)
|
|
241
|
+
};
|
|
242
|
+
export {
|
|
243
|
+
ApiError,
|
|
244
|
+
BadRequestError,
|
|
245
|
+
ConflictError,
|
|
246
|
+
ErrorAdapterRegistry,
|
|
247
|
+
ForbiddenError,
|
|
248
|
+
InternalServerError,
|
|
249
|
+
NotFoundError,
|
|
250
|
+
UnauthorizedError,
|
|
251
|
+
ValidationError,
|
|
252
|
+
asyncHandler,
|
|
253
|
+
createError,
|
|
254
|
+
createRequestContext,
|
|
255
|
+
errorMiddleware,
|
|
256
|
+
getOrCreateRequestContext,
|
|
257
|
+
mongoDuplicateKeyAdapter,
|
|
258
|
+
normalizeError,
|
|
259
|
+
notFoundMiddleware,
|
|
260
|
+
requestIdMiddleware,
|
|
261
|
+
serializeError,
|
|
262
|
+
zodErrorAdapter
|
|
263
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@erangamadhushan/express-error-handle-middleware",
|
|
3
|
+
"version": "1.6.0",
|
|
4
|
+
"description": "Advanced TypeScript error handling middleware for Express applications.",
|
|
5
|
+
"main": "./dist/index.cjs",
|
|
6
|
+
"module": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"require": "./dist/index.cjs"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsup src/index.ts --format cjs,esm --dts",
|
|
20
|
+
"smoke:package": "npm run build && node scripts/verify-package.mjs",
|
|
21
|
+
"prepublishOnly": "npm run build",
|
|
22
|
+
"test": "jest --coverage"
|
|
23
|
+
},
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/Erangamadhushan/express-error-handle-middleware.git"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"express",
|
|
30
|
+
"middleware",
|
|
31
|
+
"error-handler",
|
|
32
|
+
"typescript"
|
|
33
|
+
],
|
|
34
|
+
"author": "Eranga Madhushan",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"type": "module",
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/Erangamadhushan/express-error-handle-middleware/issues"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://github.com/Erangamadhushan/express-error-handle-middleware#readme",
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=18.18"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"express": "^4.18.0 || ^5.0.0",
|
|
46
|
+
"zod": "^4.3.6"
|
|
47
|
+
},
|
|
48
|
+
"peerDependenciesMeta": {
|
|
49
|
+
"zod": {
|
|
50
|
+
"optional": true
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
55
|
+
"@semantic-release/git": "^10.0.1",
|
|
56
|
+
"@semantic-release/github": "^12.0.6",
|
|
57
|
+
"@semantic-release/npm": "^13.1.4",
|
|
58
|
+
"@types/express": "^5.0.6",
|
|
59
|
+
"@types/jest": "^30.0.0",
|
|
60
|
+
"@types/supertest": "^6.0.3",
|
|
61
|
+
"conventional-changelog-conventionalcommits": "^9.1.0",
|
|
62
|
+
"express": "^5.2.1",
|
|
63
|
+
"jest": "^30.2.0",
|
|
64
|
+
"semantic-release": "^25.0.3",
|
|
65
|
+
"supertest": "^7.2.2",
|
|
66
|
+
"ts-jest": "^29.4.6",
|
|
67
|
+
"tsup": "^8.5.1",
|
|
68
|
+
"typescript": "^5.9.3",
|
|
69
|
+
"zod": "^4.3.6"
|
|
70
|
+
}
|
|
71
|
+
}
|