@batkit/express-middleware 0.1.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ken Courville
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,331 @@
1
+ # @batkit/express-middleware
2
+
3
+ Express middleware for error handling with RFC 9457 Problem Details format.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @batkit/express-middleware express
9
+ ```
10
+
11
+ ## Overview
12
+
13
+ Production-ready Express middleware for error handling with automatic RFC 9457 Problem Details formatting.
14
+
15
+ ## Features
16
+
17
+ - ✅ RFC 9457 Problem Details error responses
18
+ - ✅ Handles common error types (AppError, Axios, Zod)
19
+ - ✅ Customizable error formatters
20
+ - ✅ Support for async route handlers
21
+ - ✅ TypeScript-first
22
+ - ✅ Production and development modes
23
+
24
+ ## Usage
25
+
26
+ ### Basic Setup
27
+
28
+ ```typescript
29
+ import { errorHandler } from '@batkit/express-middleware';
30
+ import express from 'express';
31
+
32
+ const app = express();
33
+
34
+ // Your routes
35
+ app.get('/users/:id', (req, res) => {
36
+ // ... your logic
37
+ res.json({ user: {} });
38
+ });
39
+
40
+ // Add error handler (must be LAST)
41
+ app.use(errorHandler());
42
+
43
+ app.listen(3000);
44
+ ```
45
+
46
+ ### Error Handling
47
+
48
+ ```typescript
49
+ import { asyncHandler, errorHandler } from "@batkit/express-middleware";
50
+ import { NotFoundError, ValidationError } from "@batkit/errors";
51
+ import express from "express";
52
+
53
+ const app = express();
54
+
55
+ // Async route handler with error handling
56
+ app.get(
57
+ "/users/:id",
58
+ asyncHandler(async (req, res) => {
59
+ const user = await db.users.findById(req.params.id);
60
+
61
+ if (!user) {
62
+ // Automatically converted to RFC 9457 format
63
+ throw new NotFoundError("User", req.params.id);
64
+ }
65
+
66
+ res.json(user);
67
+ }),
68
+ );
69
+
70
+ // Synchronous route
71
+ app.post("/users", (req, res) => {
72
+ if (!req.body.email) {
73
+ throw new ValidationError("Invalid user data", [
74
+ { field: "email", message: "Email is required" },
75
+ ]);
76
+ }
77
+
78
+ res.json({ created: true });
79
+ });
80
+
81
+ // Error handler converts all errors to RFC 9457
82
+ app.use(errorHandler());
83
+ ```
84
+
85
+ ### Async-local log context
86
+
87
+ `logContextMiddleware` wraps each request in [`runWithContext`](https://nodejs.org/api/async_context.html#asynchronous-context-tracking) from `@batkit/logger/async-local` so your code can call `mergeLogContext`, use `getLogContext`, and attach a [`ContextualLoggerProvider`](../logger/README.md)—without `req.logger` or Express-specific types inside the logger package.
88
+
89
+ **Background:** see the toolkit guide [Understanding AsyncLocalStorage](../../docs/async-local-storage.md).
90
+
91
+ ```typescript
92
+ import { logContextMiddleware } from "@batkit/express-middleware";
93
+ import { LoggerFacade } from "@batkit/logger";
94
+ import { ContextualLoggerProvider, mergeLogContext } from "@batkit/logger/async-local";
95
+ import { PinoLoggerProvider } from "@batkit/logger-pino";
96
+ import express from "express";
97
+ import { randomUUID } from "node:crypto";
98
+
99
+ LoggerFacade.setProvider(new ContextualLoggerProvider(new PinoLoggerProvider({ level: "info" })));
100
+
101
+ const app = express();
102
+ app.use(express.json());
103
+
104
+ app.use(
105
+ logContextMiddleware({
106
+ initialContext: (req) => ({
107
+ requestId: req.get("x-request-id") ?? randomUUID(),
108
+ }),
109
+ }),
110
+ );
111
+
112
+ app.post("/orders/:id/submit", (req, res) => {
113
+ mergeLogContext({ transactionId: req.get("x-transaction-id") ?? randomUUID() });
114
+ LoggerFacade.getLogger("orders").info("Submitting"); // includes requestId + transactionId in structured output
115
+ res.status(204).end();
116
+ });
117
+ ```
118
+
119
+ ### Custom Error Formatter
120
+
121
+ ```typescript
122
+ import { errorHandler, type ErrorFormatter } from "@batkit/express-middleware";
123
+ import type { ExtendedProblemDetails } from "@batkit/rfc9457";
124
+
125
+ class CustomErrorFormatter implements ErrorFormatter {
126
+ canFormat(error: unknown): boolean {
127
+ return error instanceof MyCustomError;
128
+ }
129
+
130
+ format(error: MyCustomError): ExtendedProblemDetails {
131
+ return {
132
+ type: "error:custom",
133
+ title: "Custom Error",
134
+ status: 400,
135
+ detail: error.message,
136
+ customField: error.customData,
137
+ };
138
+ }
139
+ }
140
+
141
+ app.use(
142
+ errorHandler({
143
+ formatters: [new CustomErrorFormatter()],
144
+ includeStack: true, // Include stack traces
145
+ logErrors: true,
146
+ }),
147
+ );
148
+ ```
149
+
150
+ ## API Reference
151
+
152
+ ### Log context middleware
153
+
154
+ #### `logContextMiddleware(options?): Middleware`
155
+
156
+ Runs `next()` inside `runWithContext(initialContext(req), …)` so nested async work can use `getLogContext` / `mergeLogContext` from `@batkit/logger/async-local`.
157
+
158
+ **Options:**
159
+
160
+ ```typescript
161
+ interface LogContextMiddlewareOptions {
162
+ /** Default: `() => ({})` */
163
+ initialContext?: (req: Request) => Record<string, import("@batkit/logger").LogValue>;
164
+ }
165
+ ```
166
+
167
+ **Returns:** Express middleware function. Mount it **early** (after body parsers if you need `req` fields).
168
+
169
+ **Note:** Prefer a synchronous call to `next()` inside the ALS scope. Avoid `async` middleware that `await`s before calling `next()` unless the entire downstream pipeline stays in the same async context.
170
+
171
+ ### Error Handler
172
+
173
+ #### `errorHandler(options?): ErrorMiddleware`
174
+
175
+ Creates error handling middleware that converts errors to RFC 9457 format.
176
+
177
+ **Options:**
178
+
179
+ ```typescript
180
+ interface ErrorHandlerOptions {
181
+ formatters?: ErrorFormatter[]; // Custom error formatters
182
+ includeStack?: boolean; // Include stack traces (default: only in dev)
183
+ logErrors?: boolean; // Whether to log errors (default: true)
184
+ onError?: (error: unknown, req: Request) => void; // Custom error logger
185
+ }
186
+ ```
187
+
188
+ **Returns:** Express error middleware function (must have 4 parameters)
189
+
190
+ #### `asyncHandler(fn): Middleware`
191
+
192
+ Wraps async route handlers to catch errors.
193
+
194
+ **Parameters:**
195
+
196
+ - `fn`: Async route handler function
197
+
198
+ **Returns:** Express middleware function
199
+
200
+ ### Types
201
+
202
+ #### `RequestContext`
203
+
204
+ ```typescript
205
+ interface RequestContext {
206
+ requestId: string;
207
+ logger: Logger;
208
+ userId?: string;
209
+ metadata: Record<string, unknown>;
210
+ }
211
+ ```
212
+
213
+ #### `ErrorFormatter`
214
+
215
+ ```typescript
216
+ interface ErrorFormatter {
217
+ canFormat(error: unknown): boolean;
218
+ format(error: unknown): ExtendedProblemDetails;
219
+ }
220
+ ```
221
+
222
+ ## Error Response Format
223
+
224
+ All errors are returned in RFC 9457 format:
225
+
226
+ ```json
227
+ {
228
+ "type": "error:not-found",
229
+ "title": "Resource Not Found",
230
+ "status": 404,
231
+ "detail": "User with id '123' was not found",
232
+ "instance": "/users/123",
233
+ "entityName": "User",
234
+ "entityId": "123"
235
+ }
236
+ ```
237
+
238
+ ## Built-in Error Support
239
+
240
+ The default error formatter handles:
241
+
242
+ - **@batkit/errors** - All AppError subclasses
243
+ - **Axios errors** - Formatted as upstream service errors
244
+ - **Zod errors** - Formatted as validation errors
245
+ - **Standard Error** - Formatted as internal server errors
246
+
247
+ ## Best Practices
248
+
249
+ 1. **Add `logContextMiddleware` early** in the middleware chain when using async-local logging
250
+ 2. **Add `errorHandler` last** after all routes
251
+ 3. **Use `asyncHandler`** for async routes to catch errors
252
+ 4. **Use `ContextualLoggerProvider`** with `LoggerFacade` (or your DI) so structured logs pick up ALS fields automatically
253
+ 5. **Throw `@batkit/errors`** for consistent error handling
254
+ 6. **Don't expose stack traces** in production (default behavior)
255
+
256
+ ## Example: Complete Setup
257
+
258
+ ```typescript
259
+ import { errorHandler, asyncHandler, logContextMiddleware } from "@batkit/express-middleware";
260
+ import { NotFoundError, ValidationError } from "@batkit/errors";
261
+ import { LoggerFacade } from "@batkit/logger";
262
+ import { ContextualLoggerProvider } from "@batkit/logger/async-local";
263
+ import { PinoLoggerProvider } from "@batkit/logger-pino";
264
+ import express from "express";
265
+ import { randomUUID } from "node:crypto";
266
+
267
+ LoggerFacade.setProvider(new ContextualLoggerProvider(new PinoLoggerProvider({ level: "info" })));
268
+
269
+ const app = express();
270
+ const logger = LoggerFacade.getLogger("server");
271
+
272
+ // Middleware
273
+ app.use(express.json());
274
+ app.use(
275
+ logContextMiddleware({
276
+ initialContext: (req) => ({ requestId: req.get("x-request-id") ?? randomUUID() }),
277
+ }),
278
+ );
279
+
280
+ // Routes
281
+ app.get("/health", (req, res) => {
282
+ res.json({ status: "ok" });
283
+ });
284
+
285
+ app.get(
286
+ "/users/:id",
287
+ asyncHandler(async (req, res) => {
288
+ LoggerFacade.getLogger("users").info("Fetching user", { userId: req.params.id });
289
+
290
+ const user = await db.users.findById(req.params.id);
291
+ if (!user) {
292
+ throw new NotFoundError("User", req.params.id);
293
+ }
294
+
295
+ res.json(user);
296
+ }),
297
+ );
298
+
299
+ app.post(
300
+ "/users",
301
+ asyncHandler(async (req, res) => {
302
+ const validation = validateUser(req.body);
303
+ if (!validation.success) {
304
+ throw new ValidationError("Invalid user data", validation.errors);
305
+ }
306
+
307
+ const user = await db.users.create(req.body);
308
+ res.status(201).json(user);
309
+ }),
310
+ );
311
+
312
+ // Error handler (MUST be last)
313
+ app.use(
314
+ errorHandler({
315
+ includeStack: true,
316
+ }),
317
+ );
318
+
319
+ const PORT = process.env.PORT || 3000;
320
+ app.listen(PORT, () => {
321
+ logger.info(`Server listening on port ${PORT}`);
322
+ });
323
+ ```
324
+
325
+ ## TypeScript
326
+
327
+ `logContextMiddleware` uses standard Express typings. Use `getLogContext()` from `@batkit/logger/async-local` when you need the current bag of log fields in a handler or service.
328
+
329
+ ## License
330
+
331
+ MIT © Ken Courville
@@ -0,0 +1,23 @@
1
+ import { type ExtendedProblemDetails } from "@batkit/rfc9457";
2
+ import type { NextFunction, Request, Response } from "express";
3
+ export interface ErrorFormatter {
4
+ canFormat(error: unknown): boolean;
5
+ format(error: unknown): ExtendedProblemDetails;
6
+ }
7
+ export declare class DefaultErrorFormatter implements ErrorFormatter {
8
+ canFormat(error: unknown): boolean;
9
+ format(error: unknown): ExtendedProblemDetails;
10
+ private isAxiosError;
11
+ private isZodError;
12
+ private formatAxiosError;
13
+ private formatZodError;
14
+ }
15
+ export interface ErrorHandlerOptions {
16
+ formatters?: ErrorFormatter[];
17
+ includeStack?: boolean;
18
+ logErrors?: boolean;
19
+ onError?: (error: unknown, req: Request) => void;
20
+ }
21
+ export declare function errorHandler(options?: ErrorHandlerOptions): (error: Error, req: Request, res: Response, next: NextFunction) => void;
22
+ export declare function asyncHandler(fn: (req: Request, res: Response, next: NextFunction) => Promise<unknown>): (req: Request, res: Response, next: NextFunction) => void;
23
+ //# sourceMappingURL=error-handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-handler.d.ts","sourceRoot":"","sources":["../src/error-handler.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,sBAAsB,EAG5B,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAM/D,MAAM,WAAW,cAAc;IAI7B,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC;IAKnC,MAAM,CAAC,KAAK,EAAE,OAAO,GAAG,sBAAsB,CAAC;CAChD;AAKD,qBAAa,qBAAsB,YAAW,cAAc;IAC1D,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO;IASlC,MAAM,CAAC,KAAK,EAAE,OAAO,GAAG,sBAAsB;IAmC9C,OAAO,CAAC,YAAY;IASpB,OAAO,CAAC,UAAU;IASlB,OAAO,CAAC,gBAAgB;IAaxB,OAAO,CAAC,cAAc;CAevB;AAKD,MAAM,WAAW,mBAAmB;IAIlC,UAAU,CAAC,EAAE,cAAc,EAAE,CAAC;IAK9B,YAAY,CAAC,EAAE,OAAO,CAAC;IAKvB,SAAS,CAAC,EAAE,OAAO,CAAC;IAKpB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;CAClD;AAwBD,wBAAgB,YAAY,CAC1B,OAAO,GAAE,mBAAwB,GAChC,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,KAAK,IAAI,CAwEzE;AAsBD,wBAAgB,YAAY,CAC1B,EAAE,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,KAAK,OAAO,CAAC,OAAO,CAAC,GACxE,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,KAAK,IAAI,CAI3D"}
@@ -0,0 +1,138 @@
1
+ import { isAppError } from "@batkit/errors";
2
+ import { LoggerFacade } from "@batkit/logger";
3
+ import { PROBLEM_DETAILS_CONTENT_TYPE, createExtendedProblemDetails, } from "@batkit/rfc9457";
4
+ const logger = LoggerFacade.getLogger("error-handler");
5
+ export class DefaultErrorFormatter {
6
+ canFormat(error) {
7
+ return (isAppError(error) ||
8
+ error instanceof Error ||
9
+ this.isAxiosError(error) ||
10
+ this.isZodError(error));
11
+ }
12
+ format(error) {
13
+ if (isAppError(error)) {
14
+ return error.toRFC9457();
15
+ }
16
+ if (this.isAxiosError(error)) {
17
+ return this.formatAxiosError(error);
18
+ }
19
+ if (this.isZodError(error)) {
20
+ return this.formatZodError(error);
21
+ }
22
+ if (error instanceof Error) {
23
+ return createExtendedProblemDetails({
24
+ type: "error:internal",
25
+ title: "Internal Server Error",
26
+ status: 500,
27
+ detail: error.message,
28
+ });
29
+ }
30
+ return createExtendedProblemDetails({
31
+ type: "error:unknown",
32
+ title: "Unknown Error",
33
+ status: 500,
34
+ detail: "An unknown error occurred",
35
+ });
36
+ }
37
+ isAxiosError(error) {
38
+ return (typeof error === "object" &&
39
+ error !== null &&
40
+ "isAxiosError" in error &&
41
+ error.isAxiosError === true);
42
+ }
43
+ isZodError(error) {
44
+ return (typeof error === "object" &&
45
+ error !== null &&
46
+ "issues" in error &&
47
+ Array.isArray(error.issues));
48
+ }
49
+ formatAxiosError(error) {
50
+ const status = error.response?.status || 502;
51
+ return createExtendedProblemDetails({
52
+ type: "error:upstream",
53
+ title: "Upstream Service Error",
54
+ status,
55
+ detail: error.message,
56
+ upstreamUrl: error.config?.url,
57
+ upstreamMethod: error.config?.method?.toUpperCase(),
58
+ upstreamStatus: error.response?.status,
59
+ });
60
+ }
61
+ formatZodError(error) {
62
+ const validationErrors = error.issues.map((issue) => ({
63
+ field: issue.path.join("."),
64
+ message: issue.message,
65
+ code: issue.code,
66
+ }));
67
+ return createExtendedProblemDetails({
68
+ type: "error:validation",
69
+ title: "Validation Error",
70
+ status: 400,
71
+ detail: "Request validation failed",
72
+ validationErrors,
73
+ });
74
+ }
75
+ }
76
+ export function errorHandler(options = {}) {
77
+ const { formatters = [new DefaultErrorFormatter()], includeStack = true, logErrors = true, onError, } = options;
78
+ return (error, req, res, next) => {
79
+ if (res.headersSent) {
80
+ next(error);
81
+ return;
82
+ }
83
+ if (logErrors) {
84
+ if (onError) {
85
+ onError(error, req);
86
+ }
87
+ else {
88
+ if (isAppError(error) && error.isOperational) {
89
+ logger.warn("Operational error occurred", {
90
+ error: error.message,
91
+ code: error.code,
92
+ statusCode: error.statusCode,
93
+ path: req.path,
94
+ method: req.method,
95
+ });
96
+ }
97
+ else {
98
+ logger.error(error, {
99
+ path: req.path,
100
+ method: req.method,
101
+ querd: req.method,
102
+ consoley: req.query,
103
+ body: req.body,
104
+ });
105
+ }
106
+ }
107
+ }
108
+ let problemDetails;
109
+ const formatter = formatters.find((f) => f.canFormat(error));
110
+ if (formatter) {
111
+ problemDetails = formatter.format(error);
112
+ }
113
+ else {
114
+ problemDetails = createExtendedProblemDetails({
115
+ type: "error:internal",
116
+ title: "Internal Server Error",
117
+ status: 500,
118
+ detail: "An unexpected error occurred",
119
+ });
120
+ }
121
+ if (includeStack && error.stack) {
122
+ problemDetails.stack = error.stack.split("\n").map((line) => line.trim());
123
+ }
124
+ if (!problemDetails.instance) {
125
+ problemDetails.instance = req.path;
126
+ }
127
+ res
128
+ .status(problemDetails.status || 500)
129
+ .set("Content-Type", PROBLEM_DETAILS_CONTENT_TYPE)
130
+ .json(problemDetails);
131
+ };
132
+ }
133
+ export function asyncHandler(fn) {
134
+ return (req, res, next) => {
135
+ Promise.resolve(fn(req, res, next)).catch(next);
136
+ };
137
+ }
138
+ //# sourceMappingURL=error-handler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-handler.js","sourceRoot":"","sources":["../src/error-handler.ts"],"names":[],"mappings":"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"}
package/dist/index.cjs ADDED
@@ -0,0 +1,169 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _batkit_errors = require("@batkit/errors");
3
+ let _batkit_logger = require("@batkit/logger");
4
+ let _batkit_rfc9457 = require("@batkit/rfc9457");
5
+ let _batkit_logger_async_local = require("@batkit/logger/async-local");
6
+ //#region src/error-handler.ts
7
+ const logger = _batkit_logger.LoggerFacade.getLogger("error-handler");
8
+ /**
9
+ * Default error formatter that handles common error types
10
+ */
11
+ var DefaultErrorFormatter = class {
12
+ canFormat(error) {
13
+ return (0, _batkit_errors.isAppError)(error) || error instanceof Error || this.isAxiosError(error) || this.isZodError(error);
14
+ }
15
+ format(error) {
16
+ if ((0, _batkit_errors.isAppError)(error)) return error.toRFC9457();
17
+ if (this.isAxiosError(error)) return this.formatAxiosError(error);
18
+ if (this.isZodError(error)) return this.formatZodError(error);
19
+ if (error instanceof Error) return (0, _batkit_rfc9457.createExtendedProblemDetails)({
20
+ type: "error:internal",
21
+ title: "Internal Server Error",
22
+ status: 500,
23
+ detail: error.message
24
+ });
25
+ return (0, _batkit_rfc9457.createExtendedProblemDetails)({
26
+ type: "error:unknown",
27
+ title: "Unknown Error",
28
+ status: 500,
29
+ detail: "An unknown error occurred"
30
+ });
31
+ }
32
+ isAxiosError(error) {
33
+ return typeof error === "object" && error !== null && "isAxiosError" in error && error.isAxiosError === true;
34
+ }
35
+ isZodError(error) {
36
+ return typeof error === "object" && error !== null && "issues" in error && Array.isArray(error.issues);
37
+ }
38
+ formatAxiosError(error) {
39
+ return (0, _batkit_rfc9457.createExtendedProblemDetails)({
40
+ type: "error:upstream",
41
+ title: "Upstream Service Error",
42
+ status: error.response?.status || 502,
43
+ detail: error.message,
44
+ upstreamUrl: error.config?.url,
45
+ upstreamMethod: (error.config?.method)?.toUpperCase(),
46
+ upstreamStatus: error.response?.status
47
+ });
48
+ }
49
+ formatZodError(error) {
50
+ return (0, _batkit_rfc9457.createExtendedProblemDetails)({
51
+ type: "error:validation",
52
+ title: "Validation Error",
53
+ status: 400,
54
+ detail: "Request validation failed",
55
+ validationErrors: error.issues.map((issue) => ({
56
+ field: issue.path.join("."),
57
+ message: issue.message,
58
+ code: issue.code
59
+ }))
60
+ });
61
+ }
62
+ };
63
+ /**
64
+ * Express error handling middleware that converts errors to RFC 9457 format
65
+ *
66
+ * @param options - Configuration options
67
+ * @returns Express error middleware function
68
+ *
69
+ * @example
70
+ * ```ts
71
+ * import { errorHandler } from '@batkit/express-middleware';
72
+ * import { NotFoundError } from '@batkit/errors';
73
+ * import express from 'express';
74
+ *
75
+ * const app = express();
76
+ *
77
+ * app.get('/users/:id', (req, res) => {
78
+ * throw new NotFoundError('User', req.params.id);
79
+ * });
80
+ *
81
+ * // Must be added AFTER all routes
82
+ * app.use(errorHandler());
83
+ * ```
84
+ */
85
+ function errorHandler(options = {}) {
86
+ const { formatters = [new DefaultErrorFormatter()], includeStack = true, logErrors = true, onError } = options;
87
+ return (error, req, res, next) => {
88
+ if (res.headersSent) {
89
+ next(error);
90
+ return;
91
+ }
92
+ if (logErrors) if (onError) onError(error, req);
93
+ else if ((0, _batkit_errors.isAppError)(error) && error.isOperational) logger.warn("Operational error occurred", {
94
+ error: error.message,
95
+ code: error.code,
96
+ statusCode: error.statusCode,
97
+ path: req.path,
98
+ method: req.method
99
+ });
100
+ else logger.error(error, {
101
+ path: req.path,
102
+ method: req.method,
103
+ querd: req.method,
104
+ consoley: req.query,
105
+ body: req.body
106
+ });
107
+ let problemDetails;
108
+ const formatter = formatters.find((f) => f.canFormat(error));
109
+ if (formatter) problemDetails = formatter.format(error);
110
+ else problemDetails = (0, _batkit_rfc9457.createExtendedProblemDetails)({
111
+ type: "error:internal",
112
+ title: "Internal Server Error",
113
+ status: 500,
114
+ detail: "An unexpected error occurred"
115
+ });
116
+ if (includeStack && error.stack) problemDetails.stack = error.stack.split("\n").map((line) => line.trim());
117
+ if (!problemDetails.instance) problemDetails.instance = req.path;
118
+ res.status(problemDetails.status || 500).set("Content-Type", _batkit_rfc9457.PROBLEM_DETAILS_CONTENT_TYPE).json(problemDetails);
119
+ };
120
+ }
121
+ /**
122
+ * Express middleware to catch async errors
123
+ * Wraps async route handlers to ensure errors are passed to error middleware
124
+ *
125
+ * @param fn - Async route handler
126
+ * @returns Wrapped route handler
127
+ *
128
+ * @example
129
+ * ```ts
130
+ * import { asyncHandler } from '@batkit/express-middleware';
131
+ *
132
+ * app.get('/users/:id', asyncHandler(async (req, res) => {
133
+ * const user = await db.users.findById(req.params.id);
134
+ * if (!user) {
135
+ * throw new NotFoundError('User', req.params.id);
136
+ * }
137
+ * res.json(user);
138
+ * }));
139
+ * ```
140
+ */
141
+ function asyncHandler(fn) {
142
+ return (req, res, next) => {
143
+ Promise.resolve(fn(req, res, next)).catch(next);
144
+ };
145
+ }
146
+ //#endregion
147
+ //#region src/log-context-middleware.ts
148
+ /**
149
+ * Runs each request inside {@link runWithContext} so later code can use
150
+ * {@link mergeLogContext} and a {@link ContextualLoggerProvider} without
151
+ * depending on Express (`req`) deep in the stack.
152
+ *
153
+ * Mount this **early** in the middleware chain. Use synchronous `next()` inside
154
+ * the callback; avoid `async` middleware that calls `next` after an `await`
155
+ * without keeping the rest of the work in the same async context.
156
+ */
157
+ function logContextMiddleware(options = {}) {
158
+ const initialContext = options.initialContext ?? (() => ({}));
159
+ return (req, _res, next) => {
160
+ (0, _batkit_logger_async_local.runWithContext)(initialContext(req), () => next());
161
+ };
162
+ }
163
+ //#endregion
164
+ exports.DefaultErrorFormatter = DefaultErrorFormatter;
165
+ exports.asyncHandler = asyncHandler;
166
+ exports.errorHandler = errorHandler;
167
+ exports.logContextMiddleware = logContextMiddleware;
168
+
169
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","names":["LoggerFacade","PROBLEM_DETAILS_CONTENT_TYPE"],"sources":["../src/error-handler.ts","../src/log-context-middleware.ts"],"sourcesContent":["import { isAppError } from \"@batkit/errors\";\nimport { LoggerFacade } from \"@batkit/logger\";\nimport {\n type ExtendedProblemDetails,\n PROBLEM_DETAILS_CONTENT_TYPE,\n createExtendedProblemDetails,\n} from \"@batkit/rfc9457\";\nimport type { NextFunction, Request, Response } from \"express\";\n\nconst logger = LoggerFacade.getLogger(\"error-handler\");\n/**\n * Interface for custom error formatters\n */\nexport interface ErrorFormatter {\n /**\n * Check if this formatter can handle the error\n */\n canFormat(error: unknown): boolean;\n\n /**\n * Format the error as RFC 9457 Problem Details\n */\n format(error: unknown): ExtendedProblemDetails;\n}\n\n/**\n * Default error formatter that handles common error types\n */\nexport class DefaultErrorFormatter implements ErrorFormatter {\n canFormat(error: unknown): boolean {\n return (\n isAppError(error) ||\n error instanceof Error ||\n this.isAxiosError(error) ||\n this.isZodError(error)\n );\n }\n\n format(error: unknown): ExtendedProblemDetails {\n // Handle AppError (from @batkit/errors)\n if (isAppError(error)) {\n return error.toRFC9457();\n }\n\n // Handle Axios errors\n if (this.isAxiosError(error)) {\n return this.formatAxiosError(error as Record<string, unknown>);\n }\n\n // Handle Zod validation errors\n if (this.isZodError(error)) {\n return this.formatZodError(error as Record<string, unknown>);\n }\n\n // Handle generic Error\n if (error instanceof Error) {\n return createExtendedProblemDetails({\n type: \"error:internal\",\n title: \"Internal Server Error\",\n status: 500,\n detail: error.message,\n });\n }\n\n // Unknown error type\n return createExtendedProblemDetails({\n type: \"error:unknown\",\n title: \"Unknown Error\",\n status: 500,\n detail: \"An unknown error occurred\",\n });\n }\n\n private isAxiosError(error: unknown): boolean {\n return (\n typeof error === \"object\" &&\n error !== null &&\n \"isAxiosError\" in error &&\n error.isAxiosError === true\n );\n }\n\n private isZodError(error: unknown): boolean {\n return (\n typeof error === \"object\" &&\n error !== null &&\n \"issues\" in error &&\n Array.isArray((error as Record<string, unknown>).issues)\n );\n }\n\n private formatAxiosError(error: Record<string, unknown>): ExtendedProblemDetails {\n const status = ((error.response as Record<string, unknown>)?.status as number) || 502;\n return createExtendedProblemDetails({\n type: \"error:upstream\",\n title: \"Upstream Service Error\",\n status,\n detail: error.message as string,\n upstreamUrl: (error.config as Record<string, unknown>)?.url as string | undefined,\n upstreamMethod: ((error.config as Record<string, unknown>)?.method as string)?.toUpperCase(),\n upstreamStatus: (error.response as Record<string, unknown>)?.status as number | undefined,\n });\n }\n\n private formatZodError(error: Record<string, unknown>): ExtendedProblemDetails {\n const validationErrors = (error.issues as Array<Record<string, unknown>>).map((issue) => ({\n field: (issue.path as unknown[]).join(\".\"),\n message: issue.message as string,\n code: issue.code as string,\n }));\n\n return createExtendedProblemDetails({\n type: \"error:validation\",\n title: \"Validation Error\",\n status: 400,\n detail: \"Request validation failed\",\n validationErrors,\n });\n }\n}\n\n/**\n * Options for error handler middleware\n */\nexport interface ErrorHandlerOptions {\n /**\n * Custom error formatters (in order of precedence)\n */\n formatters?: ErrorFormatter[];\n\n /**\n * Whether to include stack traces in responses (default: only in non-production)\n */\n includeStack?: boolean;\n\n /**\n * Whether to log errors (default: true)\n */\n logErrors?: boolean;\n\n /**\n * Custom error logger function\n */\n onError?: (error: unknown, req: Request) => void;\n}\n\n/**\n * Express error handling middleware that converts errors to RFC 9457 format\n *\n * @param options - Configuration options\n * @returns Express error middleware function\n *\n * @example\n * ```ts\n * import { errorHandler } from '@batkit/express-middleware';\n * import { NotFoundError } from '@batkit/errors';\n * import express from 'express';\n *\n * const app = express();\n *\n * app.get('/users/:id', (req, res) => {\n * throw new NotFoundError('User', req.params.id);\n * });\n *\n * // Must be added AFTER all routes\n * app.use(errorHandler());\n * ```\n */\nexport function errorHandler(\n options: ErrorHandlerOptions = {},\n): (error: Error, req: Request, res: Response, next: NextFunction) => void {\n const {\n formatters = [new DefaultErrorFormatter()],\n includeStack = true,\n logErrors = true,\n onError,\n } = options;\n\n return (error: Error, req: Request, res: Response, next: NextFunction): void => {\n // If headers already sent, delegate to default Express error handler\n if (res.headersSent) {\n next(error);\n return;\n }\n\n // Log the error\n if (logErrors) {\n if (onError) {\n onError(error, req);\n } else {\n if (isAppError(error) && error.isOperational) {\n logger.warn(\"Operational error occurred\", {\n error: error.message,\n code: error.code,\n statusCode: error.statusCode,\n path: req.path,\n method: req.method,\n });\n } else {\n logger.error(error, {\n path: req.path,\n method: req.method,\n querd: req.method,\n consoley: req.query,\n body: req.body,\n });\n }\n }\n }\n\n // Format the error\n let problemDetails: ExtendedProblemDetails;\n\n const formatter = formatters.find((f) => f.canFormat(error));\n if (formatter) {\n problemDetails = formatter.format(error);\n } else {\n // Fallback to generic error\n problemDetails = createExtendedProblemDetails({\n type: \"error:internal\",\n title: \"Internal Server Error\",\n status: 500,\n detail: \"An unexpected error occurred\",\n });\n }\n\n // Add stack trace if enabled\n if (includeStack && error.stack) {\n problemDetails.stack = error.stack.split(\"\\n\").map((line) => line.trim());\n }\n\n // Add instance (request path)\n if (!problemDetails.instance) {\n problemDetails.instance = req.path;\n }\n\n // Send RFC 9457 response\n res\n .status(problemDetails.status || 500)\n .set(\"Content-Type\", PROBLEM_DETAILS_CONTENT_TYPE)\n .json(problemDetails);\n };\n}\n\n/**\n * Express middleware to catch async errors\n * Wraps async route handlers to ensure errors are passed to error middleware\n *\n * @param fn - Async route handler\n * @returns Wrapped route handler\n *\n * @example\n * ```ts\n * import { asyncHandler } from '@batkit/express-middleware';\n *\n * app.get('/users/:id', asyncHandler(async (req, res) => {\n * const user = await db.users.findById(req.params.id);\n * if (!user) {\n * throw new NotFoundError('User', req.params.id);\n * }\n * res.json(user);\n * }));\n * ```\n */\nexport function asyncHandler(\n fn: (req: Request, res: Response, next: NextFunction) => Promise<unknown>,\n): (req: Request, res: Response, next: NextFunction) => void {\n return (req: Request, res: Response, next: NextFunction): void => {\n Promise.resolve(fn(req, res, next)).catch(next);\n };\n}\n","import type { LogValue } from \"@batkit/logger\";\nimport { runWithContext } from \"@batkit/logger/async-local\";\nimport type { NextFunction, Request, Response } from \"express\";\n\nexport type LogContextInitializer = (req: Request) => Record<string, LogValue>;\n\nexport interface LogContextMiddlewareOptions {\n /**\n * Build initial async-local log fields for this HTTP request.\n * @default () => ({})\n */\n initialContext?: LogContextInitializer;\n}\n\n/**\n * Runs each request inside {@link runWithContext} so later code can use\n * {@link mergeLogContext} and a {@link ContextualLoggerProvider} without\n * depending on Express (`req`) deep in the stack.\n *\n * Mount this **early** in the middleware chain. Use synchronous `next()` inside\n * the callback; avoid `async` middleware that calls `next` after an `await`\n * without keeping the rest of the work in the same async context.\n */\nexport function logContextMiddleware(\n options: LogContextMiddlewareOptions = {},\n): (req: Request, res: Response, next: NextFunction) => void {\n const initialContext = options.initialContext ?? (() => ({}));\n\n return (req: Request, _res: Response, next: NextFunction) => {\n runWithContext(initialContext(req), () => next());\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ import { ExtendedProblemDetails } from "@batkit/rfc9457";
2
+ import { NextFunction, Request, Response } from "express";
3
+ import { LogValue } from "@batkit/logger";
4
+
5
+ //#region src/error-handler.d.ts
6
+ interface ErrorFormatter {
7
+ canFormat(error: unknown): boolean;
8
+ format(error: unknown): ExtendedProblemDetails;
9
+ }
10
+ declare class DefaultErrorFormatter implements ErrorFormatter {
11
+ canFormat(error: unknown): boolean;
12
+ format(error: unknown): ExtendedProblemDetails;
13
+ private isAxiosError;
14
+ private isZodError;
15
+ private formatAxiosError;
16
+ private formatZodError;
17
+ }
18
+ interface ErrorHandlerOptions {
19
+ formatters?: ErrorFormatter[];
20
+ includeStack?: boolean;
21
+ logErrors?: boolean;
22
+ onError?: (error: unknown, req: Request) => void;
23
+ }
24
+ declare function errorHandler(options?: ErrorHandlerOptions): (error: Error, req: Request, res: Response, next: NextFunction) => void;
25
+ declare function asyncHandler(fn: (req: Request, res: Response, next: NextFunction) => Promise<unknown>): (req: Request, res: Response, next: NextFunction) => void;
26
+ //#endregion
27
+ //#region src/log-context-middleware.d.ts
28
+ type LogContextInitializer = (req: Request) => Record<string, LogValue>;
29
+ interface LogContextMiddlewareOptions {
30
+ initialContext?: LogContextInitializer;
31
+ }
32
+ declare function logContextMiddleware(options?: LogContextMiddlewareOptions): (req: Request, res: Response, next: NextFunction) => void;
33
+ //#endregion
34
+ export { DefaultErrorFormatter, type ErrorFormatter, type ErrorHandlerOptions, type LogContextInitializer, type LogContextMiddlewareOptions, asyncHandler, errorHandler, logContextMiddleware };
35
+ //# sourceMappingURL=index.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../src/error-handler.ts","../src/log-context-middleware.ts"],"mappings":";;;;;UAaiB,cAAA;EAIf,SAAA,CAAU,KAAA;EAKV,MAAA,CAAO,KAAA,YAAiB,sBAAsB;AAAA;AAAA,cAMnC,qBAAA,YAAiC,cAAc;EAC1D,SAAA,CAAU,KAAA;EASV,MAAA,CAAO,KAAA,YAAiB,sBAAA;EAAA,QAmChB,YAAA;EAAA,QASA,UAAA;EAAA,QASA,gBAAA;EAAA,QAaA,cAAA;AAAA;AAAA,UAoBO,mBAAA;EAIf,UAAA,GAAa,cAAA;EAKb,YAAA;EAKA,SAAA;EAKA,OAAA,IAAW,KAAA,WAAgB,GAAA,EAAK,OAAO;AAAA;AAAA,iBAyBzB,YAAA,CACd,OAAA,GAAS,mBAAA,IACP,KAAA,EAAO,KAAA,EAAO,GAAA,EAAK,OAAA,EAAS,GAAA,EAAK,QAAA,EAAU,IAAA,EAAM,YAAA;AAAA,iBA8FrC,YAAA,CACd,EAAA,GAAK,GAAA,EAAK,OAAA,EAAS,GAAA,EAAK,QAAA,EAAU,IAAA,EAAM,YAAA,KAAiB,OAAA,aACvD,GAAA,EAAK,OAAA,EAAS,GAAA,EAAK,QAAA,EAAU,IAAA,EAAM,YAAA;;;KCtQ3B,qBAAA,IAAyB,GAAA,EAAK,OAAA,KAAY,MAAA,SAAe,QAAA;AAAA,UAEpD,2BAAA;EAKf,cAAA,GAAiB,qBAAqB;AAAA;AAAA,iBAYxB,oBAAA,CACd,OAAA,GAAS,2BAAA,IACP,GAAA,EAAK,OAAA,EAAS,GAAA,EAAK,QAAA,EAAU,IAAA,EAAM,YAAA"}
@@ -0,0 +1,3 @@
1
+ export { DefaultErrorFormatter, asyncHandler, errorHandler, type ErrorFormatter, type ErrorHandlerOptions, } from "./error-handler.js";
2
+ export { logContextMiddleware, type LogContextInitializer, type LogContextMiddlewareOptions, } from "./log-context-middleware.js";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,qBAAqB,EACrB,YAAY,EACZ,YAAY,EACZ,KAAK,cAAc,EACnB,KAAK,mBAAmB,GACzB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,oBAAoB,EACpB,KAAK,qBAAqB,EAC1B,KAAK,2BAA2B,GACjC,MAAM,6BAA6B,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { DefaultErrorFormatter, asyncHandler, errorHandler, } from "./error-handler.js";
2
+ export { logContextMiddleware, } from "./log-context-middleware.js";
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,qBAAqB,EACrB,YAAY,EACZ,YAAY,GAGb,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,oBAAoB,GAGrB,MAAM,6BAA6B,CAAC"}
@@ -0,0 +1,8 @@
1
+ import type { LogValue } from "@batkit/logger";
2
+ import type { NextFunction, Request, Response } from "express";
3
+ export type LogContextInitializer = (req: Request) => Record<string, LogValue>;
4
+ export interface LogContextMiddlewareOptions {
5
+ initialContext?: LogContextInitializer;
6
+ }
7
+ export declare function logContextMiddleware(options?: LogContextMiddlewareOptions): (req: Request, res: Response, next: NextFunction) => void;
8
+ //# sourceMappingURL=log-context-middleware.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log-context-middleware.d.ts","sourceRoot":"","sources":["../src/log-context-middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAE/C,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAE/D,MAAM,MAAM,qBAAqB,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;AAE/E,MAAM,WAAW,2BAA2B;IAK1C,cAAc,CAAC,EAAE,qBAAqB,CAAC;CACxC;AAWD,wBAAgB,oBAAoB,CAClC,OAAO,GAAE,2BAAgC,GACxC,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,KAAK,IAAI,CAM3D"}
@@ -0,0 +1,8 @@
1
+ import { runWithContext } from "@batkit/logger/async-local";
2
+ export function logContextMiddleware(options = {}) {
3
+ const initialContext = options.initialContext ?? (() => ({}));
4
+ return (req, _res, next) => {
5
+ runWithContext(initialContext(req), () => next());
6
+ };
7
+ }
8
+ //# sourceMappingURL=log-context-middleware.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log-context-middleware.js","sourceRoot":"","sources":["../src/log-context-middleware.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAsB5D,MAAM,UAAU,oBAAoB,CAClC,UAAuC,EAAE;IAEzC,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAE9D,OAAO,CAAC,GAAY,EAAE,IAAc,EAAE,IAAkB,EAAE,EAAE;QAC1D,cAAc,CAAC,cAAc,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC;IACpD,CAAC,CAAC;AACJ,CAAC"}
package/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "@batkit/express-middleware",
3
+ "version": "0.1.0",
4
+ "description": "Express middleware for error handling and request context (Node.js only)",
5
+ "keywords": [
6
+ "error-handling",
7
+ "express",
8
+ "middleware",
9
+ "rfc9457",
10
+ "typescript"
11
+ ],
12
+ "homepage": "https://github.com/krcourville/better-application-toolkit/tree/main/packages/express-middleware#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/krcourville/better-application-toolkit/issues"
15
+ },
16
+ "license": "MIT",
17
+ "author": "Ken Courville",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/krcourville/better-application-toolkit.git",
21
+ "directory": "packages/express-middleware"
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "README.md",
26
+ "LICENSE"
27
+ ],
28
+ "type": "module",
29
+ "main": "./dist/index.cjs",
30
+ "module": "./dist/index.js",
31
+ "types": "./dist/index.d.ts",
32
+ "exports": {
33
+ ".": {
34
+ "import": {
35
+ "types": "./dist/index.d.ts",
36
+ "default": "./dist/index.js"
37
+ },
38
+ "require": {
39
+ "types": "./dist/index.d.cts",
40
+ "default": "./dist/index.cjs"
41
+ }
42
+ }
43
+ },
44
+ "publishConfig": {
45
+ "access": "public"
46
+ },
47
+ "scripts": {
48
+ "build": "pnpm clean && vp pack",
49
+ "dev": "vp pack -w",
50
+ "test": "vp test",
51
+ "test:watch": "vp test -w",
52
+ "typecheck": "tsc --noEmit",
53
+ "lint": "vp check .",
54
+ "clean": "rm -rf dist"
55
+ },
56
+ "dependencies": {
57
+ "@batkit/errors": "workspace:*",
58
+ "@batkit/logger": "workspace:*",
59
+ "@batkit/rfc9457": "workspace:*"
60
+ },
61
+ "devDependencies": {
62
+ "@types/express": "^5.0.0",
63
+ "express": "^4.21.2",
64
+ "typescript": "^5.7.2",
65
+ "vite-plus": "^0.1.14",
66
+ "vitest": "^4.1.2"
67
+ },
68
+ "peerDependencies": {
69
+ "express": "^4.18.0 || ^5.0.0"
70
+ },
71
+ "engines": {
72
+ "node": ">=22.0.0"
73
+ }
74
+ }