@darksheep/express 2.1.0 → 2.1.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.1.2](https://github.com/DarkSheepSoftware/node-packages/compare/express-v2.1.1...express-v2.1.2) (2026-04-17)
4
+
5
+
6
+ ### 🩹 Fixes
7
+
8
+ * **express:** make @darksheep/logger optional, always log errors ([#836](https://github.com/DarkSheepSoftware/node-packages/issues/836)) ([f29fef9](https://github.com/DarkSheepSoftware/node-packages/commit/f29fef9453b78ef03b6903bcc0b1ccd3af4e2baa))
9
+
10
+
11
+ ### 📚 Documentation
12
+
13
+ * Update the READMEs ([#824](https://github.com/DarkSheepSoftware/node-packages/issues/824)) ([9d282b7](https://github.com/DarkSheepSoftware/node-packages/commit/9d282b724fe056ec275578088bf495985e22c25f))
14
+
15
+
16
+ ### Dependencies
17
+
18
+ * The following workspace dependencies were updated
19
+ * dependencies
20
+ * @darksheep/errors bumped from 1.2.0 to 1.2.1
21
+ * devDependencies
22
+ * @darksheep/json-schema bumped to 0.1.2
23
+ * @darksheep/logger bumped from 1.3.0 to 1.3.1
24
+ * peerDependencies
25
+ * @darksheep/json-schema bumped to 0.1.2
26
+ * @darksheep/logger bumped from >=1.0.10 to >=1.3.1
27
+
28
+ ## [2.1.1](https://github.com/DarkSheepSoftware/node-packages/compare/express-v2.1.0...express-v2.1.1) (2025-12-04)
29
+
30
+
31
+ ### 📚 Documentation
32
+
33
+ * fix issue in example ([#797](https://github.com/DarkSheepSoftware/node-packages/issues/797)) ([c889a98](https://github.com/DarkSheepSoftware/node-packages/commit/c889a980ea4b6cbe970eac7627d8229becddbe83))
34
+
3
35
  ## [2.1.0](https://github.com/DarkSheepSoftware/node-packages/compare/express-v2.0.1...express-v2.1.0) (2025-10-09)
4
36
 
5
37
 
package/README.md CHANGED
@@ -1,2 +1,109 @@
1
1
  # @darksheep/express
2
- Automatically log your express requests.
2
+ Express middleware for the stuff you end up wiring up in every service.
3
+
4
+ ## Installation
5
+ ```sh
6
+ yarn add @darksheep/express express
7
+ ```
8
+
9
+ ## Example
10
+ ```js
11
+ import express from 'express';
12
+ import {
13
+ createRequestIdMiddleware,
14
+ createRequestLoggerMiddleware,
15
+ errorHandlerMiddleware,
16
+ notFoundMiddleware,
17
+ HttpErrorHandler,
18
+ } from '@darksheep/express';
19
+
20
+ const app = express();
21
+
22
+ app.use(createRequestIdMiddleware());
23
+ app.use(createRequestLoggerMiddleware());
24
+
25
+ // your routes here
26
+
27
+ app.use(notFoundMiddleware);
28
+ app.errorHandlers = [ HttpErrorHandler ];
29
+ app.use(errorHandlerMiddleware);
30
+ ```
31
+
32
+ ## Detailed Docs
33
+
34
+ ### Middleware
35
+
36
+ ```js
37
+ /**
38
+ * Adds a unique request ID to req.id and the X-Request-Id response header.
39
+ */
40
+ createRequestIdMiddleware(options?)
41
+
42
+ /**
43
+ * Logs each request. Requires @darksheep/logger.
44
+ */
45
+ createRequestLoggerMiddleware(options?)
46
+
47
+ /**
48
+ * Returns 404 JSON for any unmatched route.
49
+ */
50
+ notFoundMiddleware
51
+
52
+ /**
53
+ * Catches errors, sets the status code from error.statusCode, and returns JSON.
54
+ * Checks app.errorHandlers to format the error body.
55
+ */
56
+ errorHandlerMiddleware
57
+ ```
58
+
59
+ ### Content type
60
+
61
+ `createContentTypeMiddleware` checks the incoming `Content-Type` header and either continues or throws `UnsupportedMediaType`:
62
+
63
+ ```js
64
+ import { createContentTypeMiddleware } from '@darksheep/express';
65
+
66
+ // Only allow JSON bodies on this route
67
+ app.post('/data', createContentTypeMiddleware('application/json'), (req, res) => {
68
+ res.json({ ok: true });
69
+ });
70
+
71
+ // Allow multiple content types
72
+ app.post('/upload', createContentTypeMiddleware(['application/json', 'multipart/form-data']), handler);
73
+
74
+ // Pass false to skip to the next route instead of throwing
75
+ app.post('/maybe', createContentTypeMiddleware('application/json', false), jsonHandler);
76
+ ```
77
+
78
+ `createContentTypeRouter` is a shorthand that builds a sub-router dispatching by `Content-Type`:
79
+
80
+ ```js
81
+ import { createContentTypeRouter } from '@darksheep/express';
82
+
83
+ app.post('/data', createContentTypeRouter({
84
+ 'application/json': jsonHandler,
85
+ 'multipart/form-data': formHandler,
86
+ }));
87
+ // Throws UnsupportedMediaType for anything else
88
+ ```
89
+
90
+ ### Error handlers
91
+ `app.errorHandlers` is an array of `{ test, format }` objects. The first matching handler formats the error response.
92
+
93
+ ```js
94
+ /**
95
+ * Handles @darksheep/errors HttpError instances.
96
+ * Returns the error name and message.
97
+ */
98
+ HttpErrorHandler
99
+
100
+ /**
101
+ * Handles @darksheep/json-schema validation errors.
102
+ */
103
+ SchemaErrorHandler
104
+
105
+ /**
106
+ * Handles Zod validation errors.
107
+ */
108
+ ZodErrorHandler
109
+ ```
package/express.d.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  /* eslint @typescript-eslint/consistent-type-definitions: 0 */
2
- import { type Logger } from '@darksheep/logger';
2
+ import type { Logger } from '@darksheep/logger';
3
3
 
4
- export type ApiError = {
4
+ export interface ApiError {
5
5
  message?: string;
6
6
  name: string;
7
7
  path?: (number | string)[] | string;
8
- };
8
+ }
9
9
 
10
10
  export type ErrorHandler<E extends Error = Error> = {
11
11
  format(error: E): ApiError[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@darksheep/express",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
4
4
  "description": "express utilites and other bits",
5
5
  "license": "UNLICENCED",
6
6
  "type": "module",
@@ -22,16 +22,16 @@
22
22
  "test:watch": "node --watch --test src/**/*.test.js"
23
23
  },
24
24
  "dependencies": {
25
- "@darksheep/errors": "1.2.0",
25
+ "@darksheep/errors": "1.2.1",
26
26
  "@darksheep/prune": "1.1.0",
27
27
  "on-finished": "2.4.1"
28
28
  },
29
29
  "devDependencies": {
30
30
  "@darksheep/json-schema": "",
31
- "@darksheep/logger": "1.3.0",
31
+ "@darksheep/logger": "1.3.1",
32
32
  "@types/express": "~5.0.0",
33
33
  "@types/on-finished": "^2",
34
- "express": "~5.1.0"
34
+ "express": "~5.2.0"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "@darksheep/json-schema": "",
@@ -22,14 +22,15 @@ export function createContentTypeMiddleware(targetContentTypes, shouldThrow = tr
22
22
  provided != null &&
23
23
  contentTypes.has(provided)
24
24
  ) {
25
- return next();
25
+ next();
26
+ return;
26
27
  }
27
28
 
28
29
  if (shouldThrow === true) {
29
30
  return unsupportedMediaType(request, response, next);
30
31
  }
31
32
 
32
- return next('route');
33
+ next('route');
33
34
  };
34
35
  }
35
36
 
@@ -8,7 +8,9 @@
8
8
  * @property {Record<string, string> | undefined} headers - Additional headers to send.
9
9
  * @property {number} statusCode - The response status code.
10
10
  */
11
- /** @typedef {Error & HttpErrorExtra} HttpError */
11
+ /**
12
+ * @typedef {Error & HttpErrorExtra} HttpError
13
+ */
12
14
 
13
15
  /** @type {ErrorHandler<HttpError>} */
14
16
  export const HttpErrorHandler = {
@@ -1,10 +1,11 @@
1
1
  /**
2
+ * @import * as JsonSchema from '@darksheep/json-schema';
2
3
  * @import { ErrorHandler } from '../../express.js';
3
4
  * @import { RequestSchemaError, ResponseSchemaError } from '@darksheep/json-schema';
4
5
  */
5
6
 
6
7
  /**
7
- * @type {import('@darksheep/json-schema')}
8
+ * @type {JsonSchema}
8
9
  */
9
10
  let schema;
10
11
  try {
@@ -33,8 +34,9 @@ export const SchemaErrorHandler = {
33
34
  format(error) {
34
35
  const base = HttpErrorHandler.format(error);
35
36
 
36
- // @ts-ignore
37
- base.push(...error.errors);
37
+ for (const child of error.errors) {
38
+ base.push(Object.assign(child, { name: child.keyword }));
39
+ }
38
40
 
39
41
  return base;
40
42
  },
@@ -8,7 +8,9 @@
8
8
  * @property {string} code - The zod error code text.
9
9
  * @property {string} path - The path to the error in zod.
10
10
  */
11
- /** @typedef {Error & { errors: ZodIssue[] }} ZodError */
11
+ /**
12
+ * @typedef {Error & { errors: ZodIssue[] }} ZodError
13
+ */
12
14
 
13
15
  /** @type {ErrorHandler<ZodError>} */
14
16
  export const ZodErrorHandler = {
@@ -5,14 +5,20 @@ const fallbackFilters = [
5
5
  /^\/live(?:ness)?z?$/u,
6
6
  ];
7
7
 
8
- /** @typedef {string|RegExp} Filter */
8
+ /**
9
+ * @typedef {string|RegExp} Filter
10
+ */
9
11
 
10
12
  /**
11
- * @param {string} requestPath - The path to test.
13
+ * @param {string | undefined | null} [requestPath] - The path to test.
12
14
  * @param {Filter[]} [filters] - A list of paths to filter out.
13
15
  * @returns {boolean}
14
16
  */
15
17
  export function shouldFilterOutRequest(requestPath, filters = fallbackFilters) {
18
+ if (requestPath == null) {
19
+ return false;
20
+ }
21
+
16
22
  for (const filter of filters) {
17
23
  if (filter instanceof RegExp && filter.test(requestPath)) {
18
24
  return true;
@@ -1,18 +1,32 @@
1
1
  /**
2
2
  * @import { Socket } from 'node:net'
3
+ * @import { Logger } from '@darksheep/logger'
3
4
  * @import { Handler } from '../../express.js'
4
5
  * @import { Filter } from './filter.js'
5
6
  */
6
7
 
7
- import { Logger } from '@darksheep/logger';
8
8
  import { pruneShallow } from '@darksheep/prune';
9
9
  import onFinished from 'on-finished';
10
10
 
11
11
  import { shouldFilterOutRequest } from './filter.js';
12
12
 
13
+ async function getLogger() {
14
+ try {
15
+ const { Logger } = await import('@darksheep/logger');
16
+
17
+ return Logger;
18
+ } catch {
19
+ // optional peer dependency
20
+ }
21
+ }
22
+
23
+ const Logger = await getLogger();
24
+
13
25
  /** @type {WeakMap<Socket, number>} */
14
26
  const sockets = new WeakMap();
15
27
 
28
+ const wrap = Logger?.wrap ?? ((_ctx, fn) => fn());
29
+
16
30
  /**
17
31
  * @param {Logger} logger - A callback that can be bound to the request.
18
32
  * @param {Filter[]} [filters] - A list of requests to not log.
@@ -25,24 +39,25 @@ export function createRequestLoggerMiddleware(
25
39
  return (request, response, next) => {
26
40
  request.logger = logger;
27
41
 
28
- Logger.wrap({
42
+ wrap({
29
43
  request: request,
30
44
  trail: request.trail,
31
45
  }, () => {
32
- const [ requestPath ] = request.originalUrl.split('?', 1);
33
- if (
34
- requestPath == null ||
35
- shouldFilterOutRequest(requestPath, filters)
36
- ) {
37
- return next();
38
- }
39
-
40
46
  const startBytesRead = sockets.get(request.socket) ?? 0;
41
47
  const startBytesWritten = request.socket.bytesWritten;
42
48
 
43
49
  const requestStartMs = Date.now();
44
50
  onFinished(response, (error) => {
45
51
  sockets.set(request.socket, request.socket.bytesRead);
52
+ const [ requestPath ] = request.originalUrl.split('?', 1);
53
+
54
+ if (
55
+ shouldFilterOutRequest(requestPath, filters) === true &&
56
+ response.statusCode < 400 &&
57
+ error == null
58
+ ) {
59
+ return;
60
+ }
46
61
 
47
62
  const message = `${request.method} ${requestPath} ${response.statusCode}`;
48
63
  const context = pruneShallow({
@@ -61,14 +76,16 @@ export function createRequestLoggerMiddleware(
61
76
  });
62
77
 
63
78
  if (response.statusCode < 400) {
64
- return request.logger.info(message, context);
79
+ request.logger.info(message, context);
80
+ return;
65
81
  }
66
82
 
67
83
  if (response.statusCode < 500) {
68
- return request.logger.warning(message, context);
84
+ request.logger.warning(message, context);
85
+ return;
69
86
  }
70
87
 
71
- return request.logger.error(message, context);
88
+ request.logger.error(message, context);
72
89
  });
73
90
 
74
91
  next();
@@ -1,11 +1,17 @@
1
1
  /**
2
- * @import { Handler } from '../../express.js'
2
+ * @import { NextFunction } from 'express'
3
+ * @import { Request, Response } from '../../express.js'
3
4
  */
4
5
  import { NotFound } from '@darksheep/errors';
5
6
 
6
- /** @type {Handler} */
7
+ /**
8
+ * @param {Request} request - The express request.
9
+ * @param {Response} _response - The express response.
10
+ * @param {NextFunction} next - The next handler.
11
+ * @returns {void}
12
+ */
7
13
  export function notFoundMiddleware(request, _response, next) {
8
- return next(
14
+ next(
9
15
  new NotFound(`${request.method} ${request.originalUrl}`),
10
16
  );
11
17
  }
@@ -29,6 +29,6 @@ export function createRequestIdMiddleware(headerName) {
29
29
  return (request, response, next) => {
30
30
  request.trail ??= getTrail(request, headerName);
31
31
  response.trail = request.trail;
32
- return next();
32
+ next();
33
33
  };
34
34
  }
@@ -4,5 +4,5 @@
4
4
  * @returns {Handler}
5
5
  */
6
6
  export function createContentTypeMiddleware(targetContentTypes: string | string[], shouldThrow?: boolean): Handler;
7
- export function unsupportedMediaType(req: import(".store/@types-express-serve-static-core-npm-5.0.7-4a816a369e/package").Request<import(".store/@types-express-serve-static-core-npm-5.0.7-4a816a369e/package").ParamsDictionary, any, any, import(".store/@types-qs-npm-6.14.0-e27d9ca044/package").ParsedQs, Record<string, any>>, res: import(".store/@types-express-serve-static-core-npm-5.0.7-4a816a369e/package").Response<any, Record<string, any>, number>, next: import(".store/@types-express-serve-static-core-npm-5.0.7-4a816a369e/package").NextFunction): unknown;
7
+ export function unsupportedMediaType(req: import("express-serve-static-core").Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>, res: import("express-serve-static-core").Response<any, Record<string, any>, number>, next: import("express-serve-static-core").NextFunction): unknown;
8
8
  import type { Handler } from '../../express.js';
@@ -7,7 +7,9 @@
7
7
  * @property {Record<string, string> | undefined} headers - Additional headers to send.
8
8
  * @property {number} statusCode - The response status code.
9
9
  */
10
- /** @typedef {Error & HttpErrorExtra} HttpError */
10
+ /**
11
+ * @typedef {Error & HttpErrorExtra} HttpError
12
+ */
11
13
  /** @type {ErrorHandler<HttpError>} */
12
14
  export const HttpErrorHandler: ErrorHandler<HttpError>;
13
15
  export type HttpErrorExtra = {
@@ -1,5 +1,4 @@
1
1
  /** @type {ErrorHandler<RequestSchemaError | ResponseSchemaError>} */
2
- export const SchemaErrorHandler: ErrorHandler<RequestSchemaError | ResponseSchemaError>;
3
- import type { RequestSchemaError } from '@darksheep/json-schema';
4
- import type { ResponseSchemaError } from '@darksheep/json-schema';
2
+ export const SchemaErrorHandler: ErrorHandler<JsonSchema.RequestSchemaError | JsonSchema.ResponseSchemaError>;
3
+ import type * as JsonSchema from '@darksheep/json-schema';
5
4
  import type { ErrorHandler } from '../../express.js';
@@ -1 +1 @@
1
- export function errorHandlerMiddleware(err: any, req: import(".store/@types-express-serve-static-core-npm-5.0.7-4a816a369e/package").Request<import(".store/@types-express-serve-static-core-npm-5.0.7-4a816a369e/package").ParamsDictionary, any, any, import(".store/@types-qs-npm-6.14.0-e27d9ca044/package").ParsedQs, Record<string, any>>, res: import(".store/@types-express-serve-static-core-npm-5.0.7-4a816a369e/package").Response<any, Record<string, any>, number>, next: import(".store/@types-express-serve-static-core-npm-5.0.7-4a816a369e/package").NextFunction): unknown;
1
+ export function errorHandlerMiddleware(err: any, req: import("express-serve-static-core").Request<import("express-serve-static-core").ParamsDictionary, any, any, import("qs").ParsedQs, Record<string, any>>, res: import("express-serve-static-core").Response<any, Record<string, any>, number>, next: import("express-serve-static-core").NextFunction): unknown;
@@ -7,7 +7,9 @@
7
7
  * @property {string} code - The zod error code text.
8
8
  * @property {string} path - The path to the error in zod.
9
9
  */
10
- /** @typedef {Error & { errors: ZodIssue[] }} ZodError */
10
+ /**
11
+ * @typedef {Error & { errors: ZodIssue[] }} ZodError
12
+ */
11
13
  /** @type {ErrorHandler<ZodError>} */
12
14
  export const ZodErrorHandler: ErrorHandler<ZodError>;
13
15
  export type ZodIssue = {
@@ -1,8 +1,10 @@
1
- /** @typedef {string|RegExp} Filter */
2
1
  /**
3
- * @param {string} requestPath - The path to test.
2
+ * @typedef {string|RegExp} Filter
3
+ */
4
+ /**
5
+ * @param {string | undefined | null} [requestPath] - The path to test.
4
6
  * @param {Filter[]} [filters] - A list of paths to filter out.
5
7
  * @returns {boolean}
6
8
  */
7
- export function shouldFilterOutRequest(requestPath: string, filters?: Filter[]): boolean;
9
+ export function shouldFilterOutRequest(requestPath?: string | undefined | null, filters?: Filter[]): boolean;
8
10
  export type Filter = string | RegExp;
@@ -4,6 +4,8 @@
4
4
  * @returns {Handler}
5
5
  */
6
6
  export function createRequestLoggerMiddleware(logger: Logger, filters?: Filter[]): Handler;
7
- import { Logger } from '@darksheep/logger';
7
+ declare const Logger: typeof Logger | undefined;
8
+ import type { Logger } from '@darksheep/logger';
8
9
  import type { Filter } from './filter.js';
9
10
  import type { Handler } from '../../express.js';
11
+ export {};
@@ -1 +1,10 @@
1
- export function notFoundMiddleware(req: import(".store/@types-express-serve-static-core-npm-5.0.7-4a816a369e/package").Request<import(".store/@types-express-serve-static-core-npm-5.0.7-4a816a369e/package").ParamsDictionary, any, any, import(".store/@types-qs-npm-6.14.0-e27d9ca044/package").ParsedQs, Record<string, any>>, res: import(".store/@types-express-serve-static-core-npm-5.0.7-4a816a369e/package").Response<any, Record<string, any>, number>, next: import(".store/@types-express-serve-static-core-npm-5.0.7-4a816a369e/package").NextFunction): unknown;
1
+ /**
2
+ * @param {Request} request - The express request.
3
+ * @param {Response} _response - The express response.
4
+ * @param {NextFunction} next - The next handler.
5
+ * @returns {void}
6
+ */
7
+ export function notFoundMiddleware(request: Request, _response: Response, next: NextFunction): void;
8
+ import type { Request } from '../../express.js';
9
+ import type { Response } from '../../express.js';
10
+ import type { NextFunction } from 'express';