@web-ts-toolkit/express-response-handler 0.2.0 → 0.4.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/README.md +4 -323
- package/package.json +3 -3
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
|
-
##
|
|
13
|
+
## Documentation
|
|
14
14
|
|
|
15
|
-
|
|
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
|
-
|
|
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,284 +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
|
-
import { ErrorFormats } from '@web-ts-toolkit/express-response-handler';
|
|
247
|
-
|
|
248
|
-
const structuredHandler = apiHandler.createHandler({
|
|
249
|
-
errorFormat: ErrorFormats.aip193,
|
|
250
|
-
errorDomain: 'api.example.com',
|
|
251
|
-
});
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
That mode returns errors in this shape:
|
|
255
|
-
|
|
256
|
-
```json
|
|
257
|
-
{
|
|
258
|
-
"error": {
|
|
259
|
-
"code": 404,
|
|
260
|
-
"status": "NOT_FOUND",
|
|
261
|
-
"message": "project not found",
|
|
262
|
-
"details": [
|
|
263
|
-
{
|
|
264
|
-
"type": "error_info",
|
|
265
|
-
"reason": "NOT_FOUND",
|
|
266
|
-
"domain": "api.example.com"
|
|
267
|
-
}
|
|
268
|
-
]
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
You can enrich HTTP errors with machine-readable fields:
|
|
274
|
-
|
|
275
|
-
```ts
|
|
276
|
-
import { BadRequestError } from '@web-ts-toolkit/http-errors';
|
|
277
|
-
|
|
278
|
-
app.get(
|
|
279
|
-
'/projects/:id',
|
|
280
|
-
structuredHandler.handleResponse(async () => {
|
|
281
|
-
throw new BadRequestError('invalid project id', {
|
|
282
|
-
reason: 'INVALID_PROJECT_ID',
|
|
283
|
-
metadata: { field: 'id' },
|
|
284
|
-
details: [
|
|
285
|
-
{
|
|
286
|
-
type: 'help',
|
|
287
|
-
links: [
|
|
288
|
-
{
|
|
289
|
-
description: 'Project ID format guide',
|
|
290
|
-
url: 'https://api.example.com/docs/errors/invalid-project-id',
|
|
291
|
-
},
|
|
292
|
-
],
|
|
293
|
-
},
|
|
294
|
-
],
|
|
295
|
-
});
|
|
296
|
-
}),
|
|
297
|
-
);
|
|
298
|
-
```
|
|
299
|
-
|
|
300
|
-
If you want RFC 9457 problem details instead, create a handler instance with `errorFormat: 'rfc9457'`:
|
|
301
|
-
|
|
302
|
-
```ts
|
|
303
|
-
import apiHandler from '@web-ts-toolkit/express-response-handler';
|
|
304
|
-
import { ErrorFormats } from '@web-ts-toolkit/express-response-handler';
|
|
305
|
-
|
|
306
|
-
const problemHandler = apiHandler.createHandler({
|
|
307
|
-
errorFormat: ErrorFormats.rfc9457,
|
|
308
|
-
errorDomain: 'api.example.com',
|
|
309
|
-
});
|
|
310
|
-
```
|
|
311
|
-
|
|
312
|
-
That mode returns `application/problem+json` payloads in this shape:
|
|
313
|
-
|
|
314
|
-
```json
|
|
315
|
-
{
|
|
316
|
-
"type": "https://api.example.com/problems/invalid-project-id",
|
|
317
|
-
"title": "Invalid project id",
|
|
318
|
-
"status": 400,
|
|
319
|
-
"detail": "invalid project id",
|
|
320
|
-
"instance": "/problems/invalid-project-id/123",
|
|
321
|
-
"errors": [
|
|
322
|
-
{
|
|
323
|
-
"detail": "must be a valid project id",
|
|
324
|
-
"pointer": "#/id"
|
|
325
|
-
}
|
|
326
|
-
]
|
|
327
|
-
}
|
|
328
|
-
```
|
|
329
|
-
|
|
330
|
-
You can enrich HTTP errors with problem detail fields:
|
|
331
|
-
|
|
332
|
-
```ts
|
|
333
|
-
import { BadRequestError } from '@web-ts-toolkit/http-errors';
|
|
334
|
-
|
|
335
|
-
app.get(
|
|
336
|
-
'/projects/:id',
|
|
337
|
-
problemHandler.handleResponse(async () => {
|
|
338
|
-
throw new BadRequestError('invalid project id', {
|
|
339
|
-
type: 'https://api.example.com/problems/invalid-project-id',
|
|
340
|
-
title: 'Invalid project id',
|
|
341
|
-
instance: '/problems/invalid-project-id/123',
|
|
342
|
-
errors: [
|
|
343
|
-
{
|
|
344
|
-
detail: 'must be a valid project id',
|
|
345
|
-
pointer: '#/id',
|
|
346
|
-
},
|
|
347
|
-
],
|
|
348
|
-
});
|
|
349
|
-
}),
|
|
350
|
-
);
|
|
351
|
-
```
|
|
352
|
-
|
|
353
|
-
## Isolated Instances
|
|
354
|
-
|
|
355
|
-
The default export is a ready-to-use singleton. If you want separate hook configuration per router or module, create an isolated instance:
|
|
356
|
-
|
|
357
|
-
```ts
|
|
358
|
-
import apiHandler from '@web-ts-toolkit/express-response-handler';
|
|
359
|
-
|
|
360
|
-
const adminHandler = apiHandler.createHandler();
|
|
361
|
-
const publicHandler = apiHandler.createHandler();
|
|
362
|
-
|
|
363
|
-
adminHandler.preError = async function (err) {
|
|
364
|
-
console.error('admin route failed', err);
|
|
365
|
-
};
|
|
366
|
-
```
|
|
367
|
-
|
|
368
|
-
## When To Use It
|
|
369
|
-
|
|
370
|
-
This package is a good fit when you want:
|
|
371
|
-
|
|
372
|
-
- Express routes that return values instead of calling `res.json(...)`
|
|
373
|
-
- a small abstraction rather than a full framework
|
|
374
|
-
- consistent JSON, error, and CSV response behavior
|
|
375
|
-
|
|
376
|
-
It is less useful if you want fully explicit low-level Express response control in every route.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@web-ts-toolkit/express-response-handler",
|
|
3
3
|
"description": "Express response handler",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.4.0",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"express",
|
|
7
7
|
"api",
|
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@fast-csv/format": "^5.0.5",
|
|
48
|
-
"@web-ts-toolkit/http-errors": "0.
|
|
49
|
-
"@web-ts-toolkit/utils": "0.
|
|
48
|
+
"@web-ts-toolkit/http-errors": "0.4.0",
|
|
49
|
+
"@web-ts-toolkit/utils": "0.4.0"
|
|
50
50
|
},
|
|
51
51
|
"author": "Junmin Ahn",
|
|
52
52
|
"bugs": {
|