expediate 1.0.4 → 1.0.6
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 +138 -0
- package/CONTRIBUTING.md +150 -0
- package/LICENSE +16 -16
- package/README.md +330 -444
- package/dist/apis.d.ts +504 -27
- package/dist/apis.d.ts.map +1 -1
- package/dist/apis.js +618 -107
- package/dist/apis.js.map +1 -1
- package/dist/cjs/index.js +4066 -0
- package/dist/cjs/package.json +1 -0
- package/dist/git.d.ts +72 -9
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +129 -74
- package/dist/git.js.map +1 -1
- package/dist/http-objects.d.ts +26 -0
- package/dist/http-objects.d.ts.map +1 -0
- package/dist/http-objects.js +588 -0
- package/dist/http-objects.js.map +1 -0
- package/dist/index.d.ts +18 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -24
- package/dist/index.js.map +1 -1
- package/dist/jwt-auth.d.ts +158 -57
- package/dist/jwt-auth.d.ts.map +1 -1
- package/dist/jwt-auth.js +447 -207
- package/dist/jwt-auth.js.map +1 -1
- package/dist/middleware.d.ts +476 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +647 -0
- package/dist/middleware.js.map +1 -0
- package/dist/mimetypes.json +882 -1
- package/dist/misc.d.ts +268 -25
- package/dist/misc.d.ts.map +1 -1
- package/dist/misc.js +449 -168
- package/dist/misc.js.map +1 -1
- package/dist/openapi.d.ts +433 -0
- package/dist/openapi.d.ts.map +1 -0
- package/dist/openapi.js +624 -0
- package/dist/openapi.js.map +1 -0
- package/dist/router-types.d.ts +760 -0
- package/dist/router-types.d.ts.map +1 -0
- package/dist/router-types.js +23 -0
- package/dist/router-types.js.map +1 -0
- package/dist/router.d.ts +37 -201
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +502 -244
- package/dist/router.js.map +1 -1
- package/dist/static.d.ts +3 -3
- package/dist/static.d.ts.map +1 -1
- package/dist/static.js +164 -105
- package/dist/static.js.map +1 -1
- package/docs/THREAT_MODEL.md +52 -0
- package/docs/api-builder-v2-design.md +644 -0
- package/docs/api-builder-v3-design.md +397 -0
- package/docs/api-builder.md +454 -0
- package/docs/benchmark.md +27 -0
- package/docs/body-parsing.md +223 -0
- package/docs/errors.md +359 -0
- package/docs/expediate.png +0 -0
- package/docs/git.md +139 -0
- package/docs/jwt-auth.md +251 -0
- package/docs/logo.svg +12 -0
- package/docs/middleware.md +264 -0
- package/docs/openapi.md +180 -0
- package/docs/router.md +356 -0
- package/docs/static.md +128 -0
- package/docs/wiki.json +123 -0
- package/package.json +47 -8
- package/.npmignore +0 -16
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
# API Builder v3 — Response Control, Auth Escape Hatches, and Error Unification
|
|
2
|
+
|
|
3
|
+
**Status:** Draft / design discussion
|
|
4
|
+
**Date:** 2026-06-25
|
|
5
|
+
**Scope:** `src/apis.ts`, `src/http-objects.ts`, touch points with `src/router.ts`
|
|
6
|
+
**Compatibility:** additive except where noted; one optional breaking change is flagged separately
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 1. Context
|
|
11
|
+
|
|
12
|
+
A second field study — independent from the DevLab review behind
|
|
13
|
+
`api-builder-v2-design.md` — evaluated `apiBuilder` for a project built around
|
|
14
|
+
hand-rolled, cookie-based session authentication (`createSessionMiddleware`
|
|
15
|
+
reading `req.headers.cookie` / writing `res.setHeader('Set-Cookie', ...)` /
|
|
16
|
+
attaching `req.session`) and a class-based `AppError` hierarchy
|
|
17
|
+
(`.status`/`.code`, caught once via `instanceof AppError`). The project ended
|
|
18
|
+
up using plain `createRouter()` routers instead of `apiBuilder`, for three
|
|
19
|
+
reasons:
|
|
20
|
+
|
|
21
|
+
1. `ApiContext` carries no path to the response object, so nothing in the
|
|
22
|
+
`apiBuilder` pipeline can refresh a sliding-expiration session cookie or
|
|
23
|
+
otherwise write a header mid-pipeline.
|
|
24
|
+
2. The truthy/falsy return convention (`200` / `201` no-body) doesn't cover
|
|
25
|
+
`201 Created` *with* a body, or `204 No Content` on `DELETE` — both
|
|
26
|
+
needed routes existed.
|
|
27
|
+
3. `ApiError` is an anonymous `{ status, data, message }` shape; bridging it
|
|
28
|
+
to an `AppError` class hierarchy with `.code` would mean writing the same
|
|
29
|
+
`instanceof AppError` translation twice — once for `service.onError`, once
|
|
30
|
+
for the app's own `router.error()` (for the routes that aren't behind
|
|
31
|
+
`apiBuilder`).
|
|
32
|
+
|
|
33
|
+
A fourth complaint — that the `scope`/`data`/`methods` instance model is
|
|
34
|
+
incompatible with a pure-function, closure-injected style — does **not**
|
|
35
|
+
require a framework change. All three fields are optional; a service with
|
|
36
|
+
none of them is a singleton instance (`{ $key: 'singleton' }`) that handlers
|
|
37
|
+
are free to ignore entirely. Arrow-function handlers closing over
|
|
38
|
+
injected dependencies already work today and are shown in
|
|
39
|
+
`api-builder-v2-design.md` §9 (`wikiController = (wikiService) =>
|
|
40
|
+
defineController({ ... })`). The actual gap is that `docs/api-builder.md`
|
|
41
|
+
opens with the `data()`/`this`/`methods` example, which reads as if the
|
|
42
|
+
instance model were mandatory. That is a documentation fix (§5 below), not a
|
|
43
|
+
design problem.
|
|
44
|
+
|
|
45
|
+
## 2. Pain points (evidence)
|
|
46
|
+
|
|
47
|
+
### PP1 — No write path to the response inside the pipeline
|
|
48
|
+
|
|
49
|
+
Guards already receive `(ctx, req)` — enough to read `req.session` or
|
|
50
|
+
`req.headers['x-cinema-id']` and turn `requireRole`/`resolveCinemaId` into
|
|
51
|
+
ordinary guards. What guards (and `auth.check`) cannot do is **write** to the
|
|
52
|
+
response: there is no `res` anywhere in the guard or auth-check signatures,
|
|
53
|
+
so a sliding-expiration cookie refresh, or any header that must be set
|
|
54
|
+
*before* the handler runs, has no seam. Separately, `ctx.user` is hardwired
|
|
55
|
+
to `req.user` in `apis.ts` (`user: req.user` in `buildRoutes`), which is a
|
|
56
|
+
JWT-plugin convention (`req.user` is typed `TokenPayload` via module
|
|
57
|
+
augmentation in `jwt-auth.ts`) — a session-based identity has to either
|
|
58
|
+
write into `req.user` itself (works, but fights the JWT typing) or be
|
|
59
|
+
inaccessible from `ctx.user`.
|
|
60
|
+
|
|
61
|
+
### PP2 — Binary return contract
|
|
62
|
+
|
|
63
|
+
Truthy → `200`, falsy → what the docs call "`201 No Content`" — which is
|
|
64
|
+
itself a minor existing inaccuracy: `201 Created` conventionally carries a
|
|
65
|
+
representation of the created resource; a bodyless success is `204 No
|
|
66
|
+
Content`. The binary convention has no slot for `201 Created` *with* a body
|
|
67
|
+
on `POST`, or an explicit `204` on `DELETE` alongside a plain `200` on `GET`
|
|
68
|
+
— exactly the three-way mapping a typical REST resource needs.
|
|
69
|
+
|
|
70
|
+
### PP3 — Disconnected error hierarchies
|
|
71
|
+
|
|
72
|
+
`service.onError(err, ctx, req)` can reshape any thrown value into an
|
|
73
|
+
`ApiError`, but only for routes registered through that one `apiBuilder`
|
|
74
|
+
call. An application also has routes outside `apiBuilder` (static files,
|
|
75
|
+
the JWT plugin's `/auth/*` endpoints, ad hoc routers) whose failures flow
|
|
76
|
+
through `router.error()` / `onError()` instead — a structurally different
|
|
77
|
+
hook (`(err, req, res, next)` vs `(err, ctx, req) => void | ApiError`). A
|
|
78
|
+
custom `Error` subclass hierarchy with its own `.status`/`.code` has to be
|
|
79
|
+
translated at both seams, by hand, with no shared code between them.
|
|
80
|
+
|
|
81
|
+
## 3. Goals and non-goals
|
|
82
|
+
|
|
83
|
+
**Goals**
|
|
84
|
+
|
|
85
|
+
1. Give the pipeline (guards, `auth.check`) a way to reach the response
|
|
86
|
+
object for the narrow set of cases that need it (cookie/header
|
|
87
|
+
mid-pipeline writes), without reintroducing `res` into the handler's
|
|
88
|
+
pure-business-logic contract.
|
|
89
|
+
2. Let a handler opt into an explicit status code (and, for `201`, a body)
|
|
90
|
+
on a per-call basis, without changing the default truthy/falsy
|
|
91
|
+
convention for the common case.
|
|
92
|
+
3. Let one error-translation function serve both `apiBuilder` and
|
|
93
|
+
plain-router error handling, so a custom error hierarchy is mapped to
|
|
94
|
+
`ApiError` exactly once.
|
|
95
|
+
4. Decouple `ctx.user` population from the `req.user` / JWT convention.
|
|
96
|
+
|
|
97
|
+
**Non-goals**
|
|
98
|
+
|
|
99
|
+
- Reopening `ctx.req`/`ctx.res` as handler-level fields. The v2 design
|
|
100
|
+
rejected this for handlers ("handlers that need raw objects should be
|
|
101
|
+
plain router middleware") and that reasoning still holds for the
|
|
102
|
+
*handler* layer — the routes that actually need raw `req`/`res` access
|
|
103
|
+
in this field study are auth/session concerns, which already run as
|
|
104
|
+
guards or `authenticate` middleware, not as `apiBuilder` route handlers.
|
|
105
|
+
This document proposes giving **guards and `auth.check`** a path to
|
|
106
|
+
`res` (§4), which is narrower and keeps the handler contract intact. If
|
|
107
|
+
a future case needs `res` *inside a handler* specifically, that should be
|
|
108
|
+
reconsidered on its own evidence rather than folded in here.
|
|
109
|
+
- A generic response-shaping/serialization hook. `withStatus()` (§5) covers
|
|
110
|
+
status + body; content negotiation, headers, streaming, etc. remain a job
|
|
111
|
+
for plain middleware in front of or instead of `apiBuilder`.
|
|
112
|
+
- Changing the default falsy-return status from `201` to the more correct
|
|
113
|
+
`204` as part of this round — flagged in §7 as a separate, optional
|
|
114
|
+
breaking change for the maintainer to schedule deliberately.
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## 4. P1 — Reaching the response from guards and `auth.check`
|
|
119
|
+
|
|
120
|
+
### `req.res` linking
|
|
121
|
+
|
|
122
|
+
Node's `http.ServerResponse` already exposes `res.req` back to the request
|
|
123
|
+
that produced it; expediate does not currently set the mirror `req.res`.
|
|
124
|
+
Adding it is a one-line change in `updateHttpObjects()` (`src/http-objects.ts`)
|
|
125
|
+
plus a type augmentation, and immediately gives every function that already
|
|
126
|
+
receives `req` — guards, and `auth.check` once it gains a `req` parameter
|
|
127
|
+
(below) — a path to `res` with no new parameter threaded through the whole
|
|
128
|
+
pipeline:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
// src/http-objects.ts, inside updateHttpObjects()
|
|
132
|
+
rReq.res = res;
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
// router-types.ts (or wherever RouterRequest is declared)
|
|
137
|
+
interface RouterRequest {
|
|
138
|
+
// ...
|
|
139
|
+
/** Back-reference to the response for this request, mirroring Node's `res.req`. */
|
|
140
|
+
res?: RouterResponse;
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### `auth.check` gains `req`
|
|
145
|
+
|
|
146
|
+
`AuthBinding.check` currently has no `req` parameter at all — guards get
|
|
147
|
+
`(ctx, req)`, but the primary authorization hook doesn't, which is an
|
|
148
|
+
inconsistency independent of this field study. Adding it (as a third,
|
|
149
|
+
optional-to-use parameter) is additive:
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
export interface AuthBinding<TUser = unknown> {
|
|
153
|
+
authenticate?: Middleware;
|
|
154
|
+
check?: (ctx: ApiContext<TUser>, required: string[], req: RouterRequest) => void | Promise<void>;
|
|
155
|
+
scheme?: Record<string, unknown>;
|
|
156
|
+
permissionsExtension?: string;
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
A resource-scoped check that needs to slide a session's expiry on every
|
|
161
|
+
authorized request can now do so:
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
auth: {
|
|
165
|
+
authenticate: sessionMiddleware,
|
|
166
|
+
check: async (ctx, required, req) => {
|
|
167
|
+
if (!req.session?.userId) throw { status: 401, message: 'Authentication required' };
|
|
168
|
+
requireCinemaAccess(req.session, req.headers['x-cinema-id'] as string, required);
|
|
169
|
+
req.res?.cookie('sid', req.session.id, { signed: true, maxAge: 3600 }); // slide expiry
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
This is deliberately presented as an advanced path, not the default one:
|
|
175
|
+
guards and `check` should still prefer **throw / return state / return
|
|
176
|
+
void** wherever possible (see `docs/errors.md` and `api-builder-v2-design.md`
|
|
177
|
+
§5) — reaching into `req.res` bypasses that declarative contract and should
|
|
178
|
+
be reserved for cases with no other seam, exactly like the session-cookie
|
|
179
|
+
refresh above.
|
|
180
|
+
|
|
181
|
+
### `resolveUser` — decoupling `ctx.user` from `req.user`
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
export interface AuthBinding<TUser = unknown> {
|
|
185
|
+
// ...
|
|
186
|
+
/**
|
|
187
|
+
* Extract the authenticated identity for `ctx.user`. Defaults to
|
|
188
|
+
* `(req) => req.user`, the JWT-plugin convention. Override when the
|
|
189
|
+
* identity lives elsewhere (e.g. `req.session.user`).
|
|
190
|
+
*/
|
|
191
|
+
resolveUser?: (req: RouterRequest) => TUser | undefined;
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
`buildRoutes` reads `service.auth?.resolveUser?.(req) ?? req.user` instead of
|
|
196
|
+
the hardcoded `req.user`. A session-based `authenticate` middleware no longer
|
|
197
|
+
needs to write into the JWT-typed `req.user` field just to populate
|
|
198
|
+
`ctx.user`:
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
auth: {
|
|
202
|
+
authenticate: sessionMiddleware,
|
|
203
|
+
resolveUser: (req) => req.session?.user,
|
|
204
|
+
},
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## 5. P2 — `withStatus()`: an opt-in response-status escape hatch
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
const API_RESULT: unique symbol = Symbol('expediate.apiResult');
|
|
213
|
+
|
|
214
|
+
export interface ApiResult<T = unknown> {
|
|
215
|
+
readonly [API_RESULT]: true;
|
|
216
|
+
status: number;
|
|
217
|
+
body?: T;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Return from a service method to send a specific status code (and,
|
|
222
|
+
* optionally, a JSON body) instead of the default truthy → 200 / falsy → 201
|
|
223
|
+
* convention. Useful for `201 Created` with a body, or an explicit `204 No
|
|
224
|
+
* Content`.
|
|
225
|
+
*/
|
|
226
|
+
export function withStatus<T>(status: number, body?: T): ApiResult<T> {
|
|
227
|
+
return { [API_RESULT]: true, status, body };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function isApiResult(value: unknown): value is ApiResult {
|
|
231
|
+
return typeof value === 'object' && value !== null && API_RESULT in value;
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
In `buildRoutes`, the response step becomes:
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
const val = await route.handler.apply(instance, [ctx, body]);
|
|
239
|
+
if (isApiResult(val)) {
|
|
240
|
+
if (val.body !== undefined) {
|
|
241
|
+
if (validateResponses && route.meta?.responses)
|
|
242
|
+
validateResponseBody(route.meta.responses, val.status, val.body, schemaComponents, validateResponses);
|
|
243
|
+
sendJson(res, val.body);
|
|
244
|
+
res.statusCode = val.status; // set before send, or sendJson(res, val.body, val.status)
|
|
245
|
+
} else {
|
|
246
|
+
res.status(val.status).end();
|
|
247
|
+
}
|
|
248
|
+
} else if (val !== undefined && val !== null && val !== false && val !== 0 && val !== '') {
|
|
249
|
+
// unchanged: existing truthy → 200 path
|
|
250
|
+
} else {
|
|
251
|
+
res.status(201).end(); // unchanged default; see §7 for the 201→204 question
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
(`sendJson` needs the status set before `res.send()` — the sketch above
|
|
256
|
+
needs the actual ordering fixed at implementation time; shown here to
|
|
257
|
+
illustrate the branch, not as final code.)
|
|
258
|
+
|
|
259
|
+
Usage:
|
|
260
|
+
|
|
261
|
+
```ts
|
|
262
|
+
POST: {
|
|
263
|
+
'/items': (ctx, body) => withStatus(201, createItem(body)), // 201 Created + body
|
|
264
|
+
},
|
|
265
|
+
DELETE: {
|
|
266
|
+
'/items/:id': (ctx) => { deleteItem(ctx.params.id); return withStatus(204); },
|
|
267
|
+
},
|
|
268
|
+
GET: {
|
|
269
|
+
'/items': () => listItems(), // unchanged: truthy → 200
|
|
270
|
+
},
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
This is purely additive — existing handlers returning plain values are
|
|
274
|
+
untouched. Response-schema validation (`validateResponseBody`), which today
|
|
275
|
+
hardcodes status `200`, is generalized to validate against whichever status
|
|
276
|
+
was actually sent.
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## 6. P3 — `errorTranslator()`: one mapping, two seams
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
/**
|
|
284
|
+
* Build a single err → ApiError mapping usable both as a `service.onError`
|
|
285
|
+
* hook and as a `router.error()` middleware, so an application-specific
|
|
286
|
+
* error hierarchy is translated exactly once regardless of which seam a
|
|
287
|
+
* given route falls through.
|
|
288
|
+
*/
|
|
289
|
+
export function errorTranslator(
|
|
290
|
+
translate: (err: unknown) => ApiError | undefined,
|
|
291
|
+
): {
|
|
292
|
+
/** Plug into `ServiceDefinition.onError`. */
|
|
293
|
+
onError: (err: unknown) => ApiError | undefined;
|
|
294
|
+
/** Plug into `router.error()`, for routes outside this `apiBuilder` call. */
|
|
295
|
+
middleware: ErrorMiddleware;
|
|
296
|
+
} {
|
|
297
|
+
return {
|
|
298
|
+
onError: translate,
|
|
299
|
+
middleware: (err, _req, res, next) => {
|
|
300
|
+
const apiErr = translate(err);
|
|
301
|
+
if (!apiErr) return next(err); // not ours — forward, let other handlers/bubbling decide
|
|
302
|
+
const status = apiErr.status ?? 500;
|
|
303
|
+
if (apiErr.data !== undefined) {
|
|
304
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
305
|
+
res.status(status).send(JSON.stringify(apiErr.data));
|
|
306
|
+
} else {
|
|
307
|
+
res.status(status).send(apiErr.message ?? 'Internal error');
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
Usage — the `AppError → ApiError` mapping is written once:
|
|
315
|
+
|
|
316
|
+
```ts
|
|
317
|
+
const errors = errorTranslator(err =>
|
|
318
|
+
err instanceof AppError ? { status: err.status, data: { code: err.code, message: err.message } } : undefined);
|
|
319
|
+
|
|
320
|
+
const api = apiBuilder({
|
|
321
|
+
onError: err => errors.onError(err),
|
|
322
|
+
// ...
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
app.use('/api', api);
|
|
326
|
+
app.error(errors.middleware); // catches AppError thrown by routes outside apiBuilder
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
`.code` is preserved end to end (inside `apiErr.data`), so structured clients
|
|
330
|
+
keep working without the project giving up its own error hierarchy.
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## 7. Open question: should the falsy-return default move from `201` to `204`?
|
|
335
|
+
|
|
336
|
+
Independent of `withStatus()`, the existing default for a falsy return is
|
|
337
|
+
`res.status(201).end()` with no body — which doesn't match `201 Created`'s
|
|
338
|
+
conventional meaning (a representation of the created resource, often with
|
|
339
|
+
`Location`). `204 No Content` is the status that means "succeeded, nothing
|
|
340
|
+
to send." `withStatus()` already lets any handler opt into the *correct*
|
|
341
|
+
status today; this section is only about whether the **default** should
|
|
342
|
+
change.
|
|
343
|
+
|
|
344
|
+
This is a behavioural break for any existing handler relying on a bare
|
|
345
|
+
falsy return meaning `201` (status code visible to clients changes from 201
|
|
346
|
+
to 204). Given `apiBuilder` has shipped two minor versions with `201` as the
|
|
347
|
+
documented default, this is listed as an open question for a deliberately
|
|
348
|
+
scheduled breaking change rather than bundled into this round.
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
## 8. Documentation fix (no code change)
|
|
353
|
+
|
|
354
|
+
Add a "Stateless services" example to `docs/api-builder.md`, alongside the
|
|
355
|
+
existing `data()`/`this`/`methods` one, showing a service with none of
|
|
356
|
+
`scope`/`data`/`methods` — just closures over injected dependencies and
|
|
357
|
+
arrow-function handlers — to make explicit what `api-builder-v2-design.md`
|
|
358
|
+
§9 already demonstrates in passing. This addresses PP-stateful-model from
|
|
359
|
+
§1 without touching `src/`.
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
|
|
363
|
+
## 9. Breaking-changes summary
|
|
364
|
+
|
|
365
|
+
| Change | Impact |
|
|
366
|
+
|---|---|
|
|
367
|
+
| `req.res` link on every request | additive |
|
|
368
|
+
| `AuthBinding.check` gains a third `req` parameter | additive (existing two-arg checks keep compiling) |
|
|
369
|
+
| `AuthBinding.resolveUser` | additive; default preserves current `req.user` behaviour |
|
|
370
|
+
| `withStatus()` / `ApiResult` | additive; existing return-value handlers unchanged |
|
|
371
|
+
| `validateResponseBody` validates against the actual sent status, not a hardcoded `200` | additive (only changes behaviour for handlers using `withStatus()`, which are new) |
|
|
372
|
+
| `errorTranslator()` | additive, new export |
|
|
373
|
+
| Falsy-return default `201` → `204` | **not** part of this round — open question, §7 |
|
|
374
|
+
|
|
375
|
+
## 10. Implementation order
|
|
376
|
+
|
|
377
|
+
1. **`req.res` link** — trivial, isolated (`http-objects.ts`). Test: a guard
|
|
378
|
+
reading `req.res` inside a request and setting a header that the test
|
|
379
|
+
asserts on the actual HTTP response.
|
|
380
|
+
2. **`AuthBinding.check(req)` + `resolveUser`** — `apis.ts` pipeline change.
|
|
381
|
+
Tests: `check` reading `req`, `resolveUser` overriding `ctx.user`
|
|
382
|
+
population, default behaviour unchanged when neither is supplied.
|
|
383
|
+
3. **`withStatus()`** — new export + `buildRoutes` response branch. Tests:
|
|
384
|
+
`201` with body, `204` without body, existing truthy/falsy paths
|
|
385
|
+
unchanged, response-schema validation against the dynamic status.
|
|
386
|
+
4. **`errorTranslator()`** — new export. Tests: `onError` usage, `middleware`
|
|
387
|
+
usage standalone on a plain router, `.code`/custom fields preserved,
|
|
388
|
+
forwarding (`next(err)`) when `translate` returns `undefined`.
|
|
389
|
+
5. **Docs** — stateless-services example in `api-builder.md`; new sections
|
|
390
|
+
for `req.res`, `resolveUser`, `withStatus()`, `errorTranslator()`;
|
|
391
|
+
`docs/errors.md` gains a recipe showing `errorTranslator()` bridging
|
|
392
|
+
`apiBuilder` and `router.error()`.
|
|
393
|
+
|
|
394
|
+
Per the project's development checklist: full JSDoc on every new export,
|
|
395
|
+
re-export from `src/index.ts`, tests in `tests/apis.test.ts` /
|
|
396
|
+
`tests/api-guards.test.ts` per step, README and `docs/api-builder.md` /
|
|
397
|
+
`docs/errors.md` updates after implementation.
|