@web-ts-toolkit/express-response-handler 0.1.0 → 0.3.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 CHANGED
@@ -1,4 +1,3 @@
1
-
2
1
  Apache License
3
2
  Version 2.0, January 2004
4
3
  http://www.apache.org/licenses/
@@ -187,7 +186,7 @@
187
186
  same "printed page" as the copyright notice for easier
188
187
  identification within third-party archives.
189
188
 
190
- Copyright © 2017, Province of British Columbia, Canada.
189
+ Copyright Junmin Ahn
191
190
 
192
191
  Licensed under the Apache License, Version 2.0 (the "License");
193
192
  you may not use this file except in compliance with the License.
package/README.md CHANGED
@@ -10,50 +10,12 @@ Instead of calling `res.json(...)` in every route, return a value. This package
10
10
  npm install @web-ts-toolkit/express-response-handler
11
11
  ```
12
12
 
13
- ## Philosophy
13
+ ## Documentation
14
14
 
15
- Express is usually imperative:
15
+ - Full package documentation lives in `website/docs/packages/express-response-handler.md`.
16
+ - Use the Docusaurus site in `website` for examples, hooks, and structured error format details.
16
17
 
17
- ```ts
18
- app.get('/users/:id', async (req, res, next) => {
19
- try {
20
- const user = await getUser(req.params.id);
21
-
22
- if (!user) {
23
- return res.status(404).json({ message: 'user not found' });
24
- }
25
-
26
- return res.json(user);
27
- } catch (err) {
28
- next(err);
29
- }
30
- });
31
- ```
32
-
33
- This package lets you write the same route in a more declarative style:
34
-
35
- ```ts
36
- app.get(
37
- '/users/:id',
38
- handleResponse(async (req) => {
39
- const user = await getUser(req.params.id);
40
-
41
- if (!user) {
42
- throw new NotFoundError('user not found');
43
- }
44
-
45
- return user;
46
- }),
47
- );
48
- ```
49
-
50
- The main rule is simple:
51
-
52
- - return a plain value for a `200 OK` JSON response
53
- - return an explicit response wrapper for a custom status or format
54
- - throw an error for failures
55
-
56
- ## Quick Start
18
+ ## Minimal Example
57
19
 
58
20
  ```ts
59
21
  import express from 'express';
@@ -93,230 +55,3 @@ app.post(
93
55
  }),
94
56
  );
95
57
  ```
96
-
97
- ## How It Works
98
-
99
- `handleResponse(...)` wraps one or more Express handlers.
100
-
101
- When a handler runs:
102
-
103
- - a plain returned value becomes `res.json(value)`
104
- - a returned `HttpResponse.*(...)` wrapper controls the status code
105
- - a returned `HttpResponse.csv(...)` streams CSV
106
- - a thrown error becomes an error response
107
- - a returned promise is awaited automatically
108
-
109
- Supported forms:
110
-
111
- - `handleResponse(fn)`
112
- - `handleResponse(fn1, fn2)`
113
- - `handleResponse([fn1, fn2])`
114
-
115
- ## Examples
116
-
117
- ### Return JSON with `200 OK`
118
-
119
- ```ts
120
- app.get(
121
- '/profile',
122
- handleResponse(async (req) => {
123
- return {
124
- id: req.user.id,
125
- email: req.user.email,
126
- };
127
- }),
128
- );
129
- ```
130
-
131
- ### Return a custom success status
132
-
133
- ```ts
134
- app.post(
135
- '/sessions',
136
- handleResponse(async (req) => {
137
- const session = await createSession(req.body);
138
- return HttpResponse.created(session);
139
- }),
140
- );
141
- ```
142
-
143
- ### Throw HTTP errors
144
-
145
- ```ts
146
- import { BadRequestError, NotFoundError } from '@web-ts-toolkit/http-errors';
147
-
148
- app.get(
149
- '/projects/:id',
150
- handleResponse(async (req) => {
151
- if (!req.params.id) {
152
- throw new BadRequestError('project id is required');
153
- }
154
-
155
- const project = await getProject(req.params.id);
156
-
157
- if (!project) {
158
- throw new NotFoundError('project not found');
159
- }
160
-
161
- return project;
162
- }),
163
- );
164
- ```
165
-
166
- ### Return CSV
167
-
168
- ```ts
169
- app.get(
170
- '/reports/users.csv',
171
- handleResponse(async () => {
172
- const rows = await getUserReportRows();
173
-
174
- return HttpResponse.csv(rows, {
175
- filename: 'users.csv',
176
- });
177
- }),
178
- );
179
- ```
180
-
181
- ### Use more than one Express handler
182
-
183
- ```ts
184
- app.get(
185
- '/me',
186
- handleResponse(requireAuth, async (req) => {
187
- return req.user;
188
- }),
189
- );
190
- ```
191
-
192
- If you call `next()` with no arguments, Express middleware flow continues normally.
193
-
194
- Do not use `next(value)` for successful responses. Return the value instead.
195
-
196
- ## Hooks
197
-
198
- Hooks let you observe or modify response flow without repeating code in every route.
199
-
200
- Available setters:
201
-
202
- - `apiHandler.preJson = fn`
203
- - `apiHandler.postJson = fn`
204
- - `apiHandler.preError = fn`
205
- - `apiHandler.postError = fn`
206
-
207
- Example:
208
-
209
- ```ts
210
- apiHandler.preJson = async function (data) {
211
- console.log('about to send json response', data);
212
- };
213
-
214
- apiHandler.preError = async function (err) {
215
- console.error('request failed', err);
216
- };
217
- ```
218
-
219
- ## Custom Error Messages
220
-
221
- Non-HTTP errors default to status `422` with a message resolved from the thrown value.
222
-
223
- You can customize that behavior:
224
-
225
- ```ts
226
- apiHandler.errorMessageProvider = function (err) {
227
- return {
228
- message: 'request failed',
229
- detail: err instanceof Error ? err.message : String(err),
230
- };
231
- };
232
- ```
233
-
234
- ## Structured Error Format
235
-
236
- The default error payload is intentionally small:
237
-
238
- ```json
239
- { "message": "project not found" }
240
- ```
241
-
242
- If you want an AIP-193-inspired error envelope, create a handler instance with `errorFormat: 'aip193'`:
243
-
244
- ```ts
245
- import apiHandler from '@web-ts-toolkit/express-response-handler';
246
-
247
- const structuredHandler = apiHandler.createExpressResponseHandler({
248
- errorFormat: 'aip193',
249
- errorDomain: 'api.example.com',
250
- });
251
- ```
252
-
253
- That mode returns errors in this shape:
254
-
255
- ```json
256
- {
257
- "error": {
258
- "code": 404,
259
- "status": "NOT_FOUND",
260
- "message": "project not found",
261
- "details": [
262
- {
263
- "type": "error_info",
264
- "reason": "NOT_FOUND",
265
- "domain": "api.example.com"
266
- }
267
- ]
268
- }
269
- }
270
- ```
271
-
272
- You can enrich HTTP errors with machine-readable fields:
273
-
274
- ```ts
275
- import { BadRequestError } from '@web-ts-toolkit/http-errors';
276
-
277
- app.get(
278
- '/projects/:id',
279
- structuredHandler.handleResponse(async () => {
280
- throw new BadRequestError('invalid project id', {
281
- reason: 'INVALID_PROJECT_ID',
282
- metadata: { field: 'id' },
283
- details: [
284
- {
285
- type: 'help',
286
- links: [
287
- {
288
- description: 'Project ID format guide',
289
- url: 'https://api.example.com/docs/errors/invalid-project-id',
290
- },
291
- ],
292
- },
293
- ],
294
- });
295
- }),
296
- );
297
- ```
298
-
299
- ## Isolated Instances
300
-
301
- The default export is a ready-to-use singleton. If you want separate hook configuration per router or module, create an isolated instance:
302
-
303
- ```ts
304
- import apiHandler from '@web-ts-toolkit/express-response-handler';
305
-
306
- const adminHandler = apiHandler.createExpressResponseHandler();
307
- const publicHandler = apiHandler.createExpressResponseHandler();
308
-
309
- adminHandler.preError = async function (err) {
310
- console.error('admin route failed', err);
311
- };
312
- ```
313
-
314
- ## When To Use It
315
-
316
- This package is a good fit when you want:
317
-
318
- - Express routes that return values instead of calling `res.json(...)`
319
- - a small abstraction rather than a full framework
320
- - consistent JSON, error, and CSV response behavior
321
-
322
- It is less useful if you want fully explicit low-level Express response control in every route.
@@ -4,7 +4,8 @@ import '@web-ts-toolkit/http-errors';
4
4
  import './responses/success.mjs';
5
5
  import './responses/index.mjs';
6
6
  import './responses/csv.mjs';
7
+ import './error-formats.mjs';
7
8
 
8
- declare function createExpressResponseHandler(options?: ExpressResponseHandlerOptions): ExpressResponseHandler;
9
+ declare function createHandler(options?: ExpressResponseHandlerOptions): ExpressResponseHandler;
9
10
 
10
- export { createExpressResponseHandler };
11
+ export { createHandler };
@@ -4,7 +4,8 @@ import '@web-ts-toolkit/http-errors';
4
4
  import './responses/success.js';
5
5
  import './responses/index.js';
6
6
  import './responses/csv.js';
7
+ import './error-formats.js';
7
8
 
8
- declare function createExpressResponseHandler(options?: ExpressResponseHandlerOptions): ExpressResponseHandler;
9
+ declare function createHandler(options?: ExpressResponseHandlerOptions): ExpressResponseHandler;
9
10
 
10
- export { createExpressResponseHandler };
11
+ export { createHandler };
@@ -26,25 +26,59 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
26
26
  mod
27
27
  ));
28
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
- var create_express_response_handler_exports = {};
30
- __export(create_express_response_handler_exports, {
31
- createExpressResponseHandler: () => createExpressResponseHandler
29
+ var create_handler_exports = {};
30
+ __export(create_handler_exports, {
31
+ createHandler: () => createHandler
32
32
  });
33
- module.exports = __toCommonJS(create_express_response_handler_exports);
33
+ module.exports = __toCommonJS(create_handler_exports);
34
34
  var import_assert = __toESM(require("assert"));
35
+ var import_utils = require("@web-ts-toolkit/utils");
35
36
  var import_csv = require("./responses/csv");
36
37
  var import_responses = require("./responses");
37
38
  var import_http_response = require("./http-response");
38
39
  var import_error_format = require("./error-format");
39
- const isFunction = (value) => typeof value === "function";
40
- const isPromise = (value) => Boolean(value) && isFunction(value.then);
40
+ var import_error_formats = require("./error-formats");
41
41
  const promisify = (fn) => (value) => Promise.resolve().then(() => fn(value));
42
- const { isArray } = Array;
43
42
  const invokePostHook = (hook, value) => {
44
43
  void hook(value).catch(() => void 0);
45
44
  };
45
+ const RFC_9457_CONTENT_TYPE = "application/problem+json";
46
+ const shouldSkipResponse = (res, event) => res.headersSent || event.canceled;
47
+ const sendProblemJson = (res, statusCode, payload, contentType) => {
48
+ res.status(statusCode);
49
+ res.set("Content-Type", contentType);
50
+ res.send(payload);
51
+ };
52
+ const sendHttpErrorByFormat = {
53
+ [import_error_formats.ErrorFormats.simple]: (res, error) => {
54
+ const payload = { message: error.message ?? "" };
55
+ if (error.errors !== void 0) {
56
+ payload.errors = error.errors;
57
+ }
58
+ res.status(error.statusCode ?? 500).send(payload);
59
+ },
60
+ [import_error_formats.ErrorFormats.aip193]: (res, error, domain) => {
61
+ res.status(error.statusCode ?? 500).send((0, import_error_format.toStructuredHttpErrorPayload)(error, domain));
62
+ },
63
+ [import_error_formats.ErrorFormats.rfc9457]: (res, error, domain) => {
64
+ sendProblemJson(res, error.statusCode ?? 500, (0, import_error_format.toRfc9457HttpErrorPayload)(error, domain), RFC_9457_CONTENT_TYPE);
65
+ }
66
+ };
67
+ const sendGenericErrorByFormat = {
68
+ [import_error_formats.ErrorFormats.simple]: (res, result) => {
69
+ res.status(422).send((0, import_error_format.toSimpleErrorPayload)(result));
70
+ },
71
+ [import_error_formats.ErrorFormats.aip193]: (res, result, domain) => {
72
+ const payload = (0, import_error_format.toStructuredGenericErrorPayload)(result, domain);
73
+ res.status(payload.error.code).send(payload);
74
+ },
75
+ [import_error_formats.ErrorFormats.rfc9457]: (res, result) => {
76
+ const payload = (0, import_error_format.toRfc9457GenericErrorPayload)(result);
77
+ sendProblemJson(res, payload.status ?? 422, payload, RFC_9457_CONTENT_TYPE);
78
+ }
79
+ };
46
80
  const assertMiddleware = (fn) => {
47
- import_assert.default.ok(isFunction(fn), "middleware handler must be a function");
81
+ import_assert.default.ok((0, import_utils.isFunction)(fn), "middleware handler must be a function");
48
82
  };
49
83
  const normalizeMiddlewareList = (fns) => {
50
84
  import_assert.default.ok(fns.length > 0, "at least one middleware handler is required");
@@ -52,7 +86,7 @@ const normalizeMiddlewareList = (fns) => {
52
86
  fns.forEach(assertMiddleware);
53
87
  return fns;
54
88
  }
55
- if (isArray(fns[0])) {
89
+ if ((0, import_utils.isArray)(fns[0])) {
56
90
  import_assert.default.ok(fns[0].length > 0, "at least one middleware handler is required");
57
91
  fns[0].forEach(assertMiddleware);
58
92
  return fns[0];
@@ -60,9 +94,10 @@ const normalizeMiddlewareList = (fns) => {
60
94
  assertMiddleware(fns[0]);
61
95
  return [fns[0]];
62
96
  };
63
- function createExpressResponseHandler(options = {}) {
64
- const errorFormat = options.errorFormat || "simple";
65
- const errorDomain = options.errorDomain || "express-response-handler";
97
+ function createHandler(options = {}) {
98
+ const errorFormat = options.errorFormat ?? import_error_formats.ErrorFormats.simple;
99
+ const errorDomain = options.errorDomain ?? "express-response-handler";
100
+ const rfc9457ContentType = options.rfc9457ContentType ?? RFC_9457_CONTENT_TYPE;
66
101
  let errorMessageProvider = import_error_format.defaultErrorMessageProvider;
67
102
  let preJson = null;
68
103
  let postJson = null;
@@ -72,8 +107,18 @@ function createExpressResponseHandler(options = {}) {
72
107
  let postJsonHook = null;
73
108
  let preErrorHook = null;
74
109
  let postErrorHook = null;
110
+ const updateHook = (fn, name, setState, rebuild) => {
111
+ if (fn === null) {
112
+ setState(null, null);
113
+ rebuild();
114
+ return;
115
+ }
116
+ import_assert.default.ok((0, import_utils.isFunction)(fn), `${name} hook must be a function`);
117
+ setState(fn, promisify(fn));
118
+ rebuild();
119
+ };
75
120
  const sendBaseJson = function(res, data, event) {
76
- if (res.headersSent || event.canceled) {
121
+ if (shouldSkipResponse(res, event)) {
77
122
  return;
78
123
  }
79
124
  if (data instanceof import_responses.Response) {
@@ -87,29 +132,30 @@ function createExpressResponseHandler(options = {}) {
87
132
  res.json(data);
88
133
  };
89
134
  const sendBaseError = function(res, err, event) {
90
- if (res.headersSent || event.canceled) {
135
+ if (shouldSkipResponse(res, event)) {
91
136
  return;
92
137
  }
93
138
  const error = err;
94
139
  if (error.statusCode) {
95
- if (errorFormat === "aip193") {
96
- res.status(error.statusCode).send((0, import_error_format.toStructuredHttpErrorPayload)(error, errorDomain));
140
+ if (errorFormat === import_error_formats.ErrorFormats.rfc9457) {
141
+ sendProblemJson(
142
+ res,
143
+ error.statusCode ?? 500,
144
+ (0, import_error_format.toRfc9457HttpErrorPayload)(error, errorDomain),
145
+ rfc9457ContentType
146
+ );
97
147
  return;
98
148
  }
99
- const payload = { message: error.message || "" };
100
- if (error.errors !== void 0) {
101
- payload.errors = error.errors;
102
- }
103
- res.status(error.statusCode).send(payload);
149
+ sendHttpErrorByFormat[errorFormat](res, error, errorDomain);
104
150
  return;
105
151
  }
106
152
  const result = errorMessageProvider(err);
107
- if (errorFormat === "aip193") {
108
- const payload = (0, import_error_format.toStructuredGenericErrorPayload)(result, errorDomain);
109
- res.status(payload.error.code).send(payload);
153
+ if (errorFormat === import_error_formats.ErrorFormats.rfc9457) {
154
+ const payload = (0, import_error_format.toRfc9457GenericErrorPayload)(result);
155
+ sendProblemJson(res, payload.status ?? 422, payload, rfc9457ContentType);
110
156
  return;
111
157
  }
112
- res.status(422).send((0, import_error_format.toSimpleErrorPayload)(result));
158
+ sendGenericErrorByFormat[errorFormat](res, result, errorDomain);
113
159
  };
114
160
  let sendJson = sendBaseJson;
115
161
  let sendError = sendBaseError;
@@ -157,7 +203,7 @@ function createExpressResponseHandler(options = {}) {
157
203
  };
158
204
  };
159
205
  const handlePromise = function(res, promise, event) {
160
- promise.then((data) => {
206
+ Promise.resolve(promise).then((data) => {
161
207
  if (event.nextError) {
162
208
  sendError(res, event.nextError, event);
163
209
  return;
@@ -166,14 +212,14 @@ function createExpressResponseHandler(options = {}) {
166
212
  }).catch((err) => sendError(res, err, event));
167
213
  };
168
214
  const handleResult = function(res, result, event) {
169
- if (res.headersSent || event.canceled) {
215
+ if (shouldSkipResponse(res, event)) {
170
216
  return;
171
217
  }
172
218
  if (event.nextError) {
173
219
  sendError(res, event.nextError, event);
174
220
  return;
175
221
  }
176
- if (isPromise(result)) {
222
+ if ((0, import_utils.isPromise)(result)) {
177
223
  handlePromise(res, result, event);
178
224
  return;
179
225
  }
@@ -216,77 +262,73 @@ function createExpressResponseHandler(options = {}) {
216
262
  handleResult,
217
263
  handlePromise,
218
264
  HttpResponse: import_http_response.HttpResponse,
219
- createExpressResponseHandler,
265
+ createHandler,
220
266
  get errorMessageProvider() {
221
267
  return errorMessageProvider;
222
268
  },
223
269
  set errorMessageProvider(fn) {
224
- import_assert.default.ok(isFunction(fn), "error message provider must be a function");
270
+ import_assert.default.ok((0, import_utils.isFunction)(fn), "error message provider must be a function");
225
271
  errorMessageProvider = fn;
226
272
  },
227
273
  get preJson() {
228
274
  return preJson;
229
275
  },
230
276
  set preJson(fn) {
231
- if (fn === null) {
232
- preJson = null;
233
- preJsonHook = null;
234
- rebuildSendJson();
235
- return;
236
- }
237
- import_assert.default.ok(isFunction(fn), "pre-json hook must be a function");
238
- preJson = fn;
239
- preJsonHook = promisify(fn);
240
- rebuildSendJson();
277
+ updateHook(
278
+ fn,
279
+ "pre-json",
280
+ (syncHook, asyncHook) => {
281
+ preJson = syncHook;
282
+ preJsonHook = asyncHook;
283
+ },
284
+ rebuildSendJson
285
+ );
241
286
  },
242
287
  get postJson() {
243
288
  return postJson;
244
289
  },
245
290
  set postJson(fn) {
246
- if (fn === null) {
247
- postJson = null;
248
- postJsonHook = null;
249
- rebuildSendJson();
250
- return;
251
- }
252
- import_assert.default.ok(isFunction(fn), "post-json hook must be a function");
253
- postJson = fn;
254
- postJsonHook = promisify(fn);
255
- rebuildSendJson();
291
+ updateHook(
292
+ fn,
293
+ "post-json",
294
+ (syncHook, asyncHook) => {
295
+ postJson = syncHook;
296
+ postJsonHook = asyncHook;
297
+ },
298
+ rebuildSendJson
299
+ );
256
300
  },
257
301
  get preError() {
258
302
  return preError;
259
303
  },
260
304
  set preError(fn) {
261
- if (fn === null) {
262
- preError = null;
263
- preErrorHook = null;
264
- rebuildSendError();
265
- return;
266
- }
267
- import_assert.default.ok(isFunction(fn), "pre-error hook must be a function");
268
- preError = fn;
269
- preErrorHook = promisify(fn);
270
- rebuildSendError();
305
+ updateHook(
306
+ fn,
307
+ "pre-error",
308
+ (syncHook, asyncHook) => {
309
+ preError = syncHook;
310
+ preErrorHook = asyncHook;
311
+ },
312
+ rebuildSendError
313
+ );
271
314
  },
272
315
  get postError() {
273
316
  return postError;
274
317
  },
275
318
  set postError(fn) {
276
- if (fn === null) {
277
- postError = null;
278
- postErrorHook = null;
279
- rebuildSendError();
280
- return;
281
- }
282
- import_assert.default.ok(isFunction(fn), "post-error hook must be a function");
283
- postError = fn;
284
- postErrorHook = promisify(fn);
285
- rebuildSendError();
319
+ updateHook(
320
+ fn,
321
+ "post-error",
322
+ (syncHook, asyncHook) => {
323
+ postError = syncHook;
324
+ postErrorHook = asyncHook;
325
+ },
326
+ rebuildSendError
327
+ );
286
328
  }
287
329
  };
288
330
  }
289
331
  // Annotate the CommonJS export names for ESM import in node:
290
332
  0 && (module.exports = {
291
- createExpressResponseHandler
333
+ createHandler
292
334
  });