@zerotal/arch 1.7.2 → 1.7.4
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/api-surface.md +4 -4
- package/docs/about.md +4 -4
- package/docs/changelog.md +122 -6
- package/docs/client/index.md +277 -70
- package/docs/contributing.md +20 -2
- package/docs/errors.md +22 -0
- package/docs/flow/components.md +4 -8
- package/docs/flow/decorators.md +17 -13
- package/docs/flow/events.md +3 -3
- package/docs/flow/icons.md +199 -0
- package/docs/flow/index.md +11 -7
- package/docs/flow/layouts.md +1 -1
- package/docs/flow/lifecycle.md +31 -16
- package/docs/flow/models.md +284 -0
- package/docs/flow/performance.md +6 -6
- package/docs/flow/references.md +3 -3
- package/docs/flow/routing.md +14 -2
- package/docs/getting-started.md +9 -9
- package/docs/i18n.md +3 -3
- package/docs/inertia/index.md +6 -1
- package/docs/inertia/props.md +1 -1
- package/docs/inertia/rendering.md +79 -0
- package/docs/routing.md +11 -0
- package/docs/support-policy.md +28 -5
- package/docs/upgrade.md +2 -0
- package/package.json +3 -3
- package/docs/client/auth.md +0 -113
- package/docs/client/errors.md +0 -139
- package/docs/client/files.md +0 -118
- package/docs/client/references.md +0 -58
- package/docs/client/requests.md +0 -131
- package/docs/client/resilience.md +0 -141
- package/docs/client/testing.md +0 -146
package/docs/client/auth.md
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Client Authentication
|
|
3
|
-
description: Bearer tokens, CSRF, and refreshing credentials on a 401.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Authentication & CSRF
|
|
7
|
-
|
|
8
|
-
The client supports the two ways a browser app proves who it is, and the choice is
|
|
9
|
-
usually made for you by where the API lives:
|
|
10
|
-
|
|
11
|
-
- **Bearer tokens** suit APIs on another origin, mobile clients, and anything where
|
|
12
|
-
the caller holds a credential it can attach itself.
|
|
13
|
-
- **Session cookies** suit an API served from your own domain, where the browser
|
|
14
|
-
already carries the session and CSRF protection is the concern instead.
|
|
15
|
-
|
|
16
|
-
## Bearer tokens
|
|
17
|
-
|
|
18
|
-
Attach a bearer token (string or a resolver, sync or async) without writing an interceptor —
|
|
19
|
-
update it at runtime with `setToken()`:
|
|
20
|
-
|
|
21
|
-
```ts
|
|
22
|
-
// app/api/client.ts
|
|
23
|
-
const api = createApiClient<Routes>({
|
|
24
|
-
token: () => authStore.accessToken, // re-read on every request
|
|
25
|
-
});
|
|
26
|
-
api.setToken(freshToken); // or update imperatively
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
Prefer the resolver form. A plain string is captured once at construction, so a
|
|
30
|
-
token refreshed later never reaches the client; a function is consulted on every
|
|
31
|
-
request and always sees the current value.
|
|
32
|
-
|
|
33
|
-
Calling `setToken()` with no argument clears the token, which is what a logout
|
|
34
|
-
should do — otherwise the next request still carries the credential of the user who
|
|
35
|
-
just signed out.
|
|
36
|
-
|
|
37
|
-
> **Note** — The `token` is only applied when no `Authorization` header is already
|
|
38
|
-
> present on the request, so a per-request override always wins.
|
|
39
|
-
|
|
40
|
-
## Session cookies and CSRF
|
|
41
|
-
|
|
42
|
-
For session/cookie (SPA) auth, set `withCredentials` to send cookies, which also turns on CSRF:
|
|
43
|
-
the client reads the `XSRF-TOKEN` cookie and sends it as `X-XSRF-TOKEN` on mutating requests
|
|
44
|
-
(matching the session/CSRF middleware). Customize the names with `csrf`:
|
|
45
|
-
|
|
46
|
-
```ts
|
|
47
|
-
// app/api/client.ts
|
|
48
|
-
createApiClient<Routes>({
|
|
49
|
-
withCredentials: true, // credentials: 'include' + CSRF on
|
|
50
|
-
csrf: { cookie: "XSRF-TOKEN", header: "X-XSRF-TOKEN" }, // defaults shown
|
|
51
|
-
});
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
Enabling `withCredentials` turns CSRF on by default, so the two travel together and
|
|
55
|
-
neither needs configuring in the common case. Set `csrf: false` to opt out, or pass
|
|
56
|
-
an object to rename the cookie and header to match a server that uses different
|
|
57
|
-
ones.
|
|
58
|
-
|
|
59
|
-
The token is attached only to mutating requests — `POST`, `PUT`, `PATCH`, `DELETE`.
|
|
60
|
-
A `GET` is exempt because it should not change state, so it needs no protection
|
|
61
|
-
from being triggered cross-site. If a `GET` in your API does change something, that
|
|
62
|
-
is the thing to fix; adding a CSRF header to it would only hide the problem.
|
|
63
|
-
|
|
64
|
-
The header is skipped when the request already carries one, so a caller that sets
|
|
65
|
-
its own value keeps it.
|
|
66
|
-
|
|
67
|
-
## 401 / token refresh
|
|
68
|
-
|
|
69
|
-
`onUnauthorized` is called when any request receives a 401 response. It receives
|
|
70
|
-
the error and a `retry` function. Call `retry()` — optionally with header overrides
|
|
71
|
-
— to re-execute the failed request. The retry is limited to **one attempt**.
|
|
72
|
-
|
|
73
|
-
```ts
|
|
74
|
-
// app/api/client.ts
|
|
75
|
-
const api = createApiClient<Routes>({
|
|
76
|
-
baseUrl: "https://api.example.com",
|
|
77
|
-
|
|
78
|
-
onUnauthorized: async (err, retry) => {
|
|
79
|
-
const newToken = await authStore.refresh();
|
|
80
|
-
return retry({ Authorization: `Bearer ${newToken}` });
|
|
81
|
-
},
|
|
82
|
-
});
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
If `onUnauthorized` is not provided or does not call `retry`, the 401 error is
|
|
86
|
-
thrown normally.
|
|
87
|
-
|
|
88
|
-
The single-attempt limit is deliberate: a refresh that itself returns 401 would
|
|
89
|
-
otherwise retry forever, turning an expired session into an endless loop of
|
|
90
|
-
requests. When the retry also fails, the error is thrown and the app can send the
|
|
91
|
-
user to the login screen.
|
|
92
|
-
|
|
93
|
-
One case the hook does not solve on its own is a page that fires several requests
|
|
94
|
-
at once. Each 401 calls `onUnauthorized` separately, so a naive handler triggers
|
|
95
|
-
several concurrent refreshes and the losers of that race may invalidate the winner's
|
|
96
|
-
token. Have the refresh itself de-duplicate — cache the in-flight promise in your
|
|
97
|
-
auth store and hand the same one to every caller until it settles:
|
|
98
|
-
|
|
99
|
-
```ts
|
|
100
|
-
// app/api/authStore.ts
|
|
101
|
-
let inflight: Promise<string> | null = null;
|
|
102
|
-
|
|
103
|
-
export function refresh(): Promise<string> {
|
|
104
|
-
inflight ??= requestNewToken().finally(() => (inflight = null));
|
|
105
|
-
return inflight;
|
|
106
|
-
}
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
## Next steps
|
|
110
|
-
|
|
111
|
-
- [Client overview](/docs/client) — the guide's front page and the rest of the sections.
|
|
112
|
-
- [Error handling](/docs/client/errors) — the errors a rejected request throws.
|
|
113
|
-
- [CSRF protection](/docs/csrf) — the server side of the cookie and header pair.
|
package/docs/client/errors.md
DELETED
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Client Error Handling
|
|
3
|
-
description: What a failed request throws, and how to tell the failure modes apart.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Error handling
|
|
7
|
-
|
|
8
|
-
Non-2xx responses throw `ApiClientError`:
|
|
9
|
-
|
|
10
|
-
```ts
|
|
11
|
-
// in any frontend module
|
|
12
|
-
import { ApiClientError } from "@zerotal/client";
|
|
13
|
-
|
|
14
|
-
try {
|
|
15
|
-
await api.post("/api/users", { name: "", email: "bad" });
|
|
16
|
-
} catch (err) {
|
|
17
|
-
if (err instanceof ApiClientError) {
|
|
18
|
-
console.log(err.status); // 422
|
|
19
|
-
console.log(err.statusText); // 'Unprocessable Entity'
|
|
20
|
-
console.log(err.body); // raw response text (the error message truncates it to 200 chars)
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
## Telling the failure modes apart
|
|
26
|
-
|
|
27
|
-
Two very different things can go wrong, and only one of them produces an
|
|
28
|
-
`ApiClientError`:
|
|
29
|
-
|
|
30
|
-
| What happened | What is thrown |
|
|
31
|
-
| -------------------------------- | ----------------------------------- |
|
|
32
|
-
| The server answered with non-2xx | `ApiClientError` |
|
|
33
|
-
| A 422 in the validator's shape | `ValidationError` |
|
|
34
|
-
| The circuit breaker is open | `CircuitBreakerOpenError` |
|
|
35
|
-
| No answer at all | The platform's own error, unwrapped |
|
|
36
|
-
|
|
37
|
-
That last row is the one worth internalising. A DNS failure, a dropped connection,
|
|
38
|
-
a CORS rejection, or an aborted request never reaches the point where a status
|
|
39
|
-
exists, so `fetch` rejects with its own error and the client passes it through
|
|
40
|
-
untouched. An `instanceof ApiClientError` check therefore does _not_ catch an
|
|
41
|
-
offline user — and a `catch` block that assumes `err.status` exists throws a second
|
|
42
|
-
error while handling the first.
|
|
43
|
-
|
|
44
|
-
```ts
|
|
45
|
-
try {
|
|
46
|
-
await api.get("/api/users");
|
|
47
|
-
} catch (err) {
|
|
48
|
-
if (err instanceof ValidationError) showFieldErrors(err.errors);
|
|
49
|
-
else if (err instanceof ApiClientError) showStatus(err.status);
|
|
50
|
-
else showOffline(); // no response: network, CORS, timeout, or abort
|
|
51
|
-
}
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
Timeouts and cancellations land in that final branch too, since both abort the
|
|
55
|
-
request rather than producing a response.
|
|
56
|
-
|
|
57
|
-
## Reading response headers
|
|
58
|
-
|
|
59
|
-
`ApiClientError` carries the response headers when there were any, which is where
|
|
60
|
-
rate limiters and throttles put the information you need to react well:
|
|
61
|
-
|
|
62
|
-
```ts
|
|
63
|
-
// in any frontend module
|
|
64
|
-
if (err instanceof ApiClientError && err.status === 429) {
|
|
65
|
-
const waitMs = err.retryAfterMs; // parsed Retry-After, or null
|
|
66
|
-
if (waitMs !== null) scheduleRetry(waitMs);
|
|
67
|
-
console.log(err.headers?.get("X-RateLimit-Remaining"));
|
|
68
|
-
}
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
`retryAfterMs` handles both forms the header takes — a delta in seconds and an
|
|
72
|
-
HTTP-date — and returns milliseconds, or `null` when the header is absent or
|
|
73
|
-
cannot be parsed.
|
|
74
|
-
|
|
75
|
-
## Global handlers
|
|
76
|
-
|
|
77
|
-
The `onError` callback fires for every non-2xx response before the error is thrown.
|
|
78
|
-
Use it for global side-effects (toasts, logging) without needing try/catch at every
|
|
79
|
-
call site:
|
|
80
|
-
|
|
81
|
-
```ts
|
|
82
|
-
// app/api/client.ts
|
|
83
|
-
const api = createApiClient<Routes>({
|
|
84
|
-
baseUrl: "https://api.example.com",
|
|
85
|
-
onError: (err) => {
|
|
86
|
-
toast.error(`${err.status}: ${err.statusText}`);
|
|
87
|
-
logger.error("api_error", { status: err.status, body: err.body });
|
|
88
|
-
},
|
|
89
|
-
});
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
> **Warning** — `onError` fires for every non-2xx error including 401, even when `onUnauthorized` is also configured. To suppress the global error callback for 401 during token refresh, guard by status inside `onError`.
|
|
93
|
-
|
|
94
|
-
Because `onError` only ever sees responses, it does not report the network failures
|
|
95
|
-
described above. Reporting that should also cover "the request never arrived"
|
|
96
|
-
belongs in the caller, or in a wrapper around it.
|
|
97
|
-
|
|
98
|
-
### Typed validation errors
|
|
99
|
-
|
|
100
|
-
A `422` response whose body matches the framework's validation shape (`{ message, errors }`,
|
|
101
|
-
as produced by [`@zerotal/validator`](/docs/validator)) throws a `ValidationError` — an
|
|
102
|
-
`ApiClientError` subclass with the field errors already parsed:
|
|
103
|
-
|
|
104
|
-
```ts
|
|
105
|
-
// in any frontend module
|
|
106
|
-
import { ValidationError } from "@zerotal/client";
|
|
107
|
-
|
|
108
|
-
try {
|
|
109
|
-
await api.post("/api/users", form);
|
|
110
|
-
} catch (err) {
|
|
111
|
-
if (err instanceof ValidationError) {
|
|
112
|
-
setFieldErrors(err.errors); // { email: ["…"], password: ["…"] }
|
|
113
|
-
err.has("email"); // boolean
|
|
114
|
-
err.first("email"); // first message, or undefined
|
|
115
|
-
err.fields(); // ["email", "password"]
|
|
116
|
-
err.validationMessage; // "The given data was invalid."
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
Check for `ValidationError` before `ApiClientError`. It is a subclass, so the
|
|
122
|
-
broader check also matches it and would swallow the parsed field errors.
|
|
123
|
-
|
|
124
|
-
A 422 whose body does not match that shape stays a plain `ApiClientError`, so an
|
|
125
|
-
endpoint returning its own error format still surfaces as an ordinary failure
|
|
126
|
-
rather than quietly producing an empty `errors` object.
|
|
127
|
-
|
|
128
|
-
`onForbidden` is the 403 counterpart of `onUnauthorized`:
|
|
129
|
-
|
|
130
|
-
```ts
|
|
131
|
-
// app/api/client.ts
|
|
132
|
-
createApiClient<Routes>({ onForbidden: () => router.push("/403") });
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
## Next steps
|
|
136
|
-
|
|
137
|
-
- [Client overview](/docs/client) — the guide's front page and the rest of the sections.
|
|
138
|
-
- [Resilience](/docs/client/resilience) — retries, timeouts, and the circuit breaker.
|
|
139
|
-
- [Authentication](/docs/client/auth) — the 401 refresh hook.
|
package/docs/client/files.md
DELETED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Client File Transfers
|
|
3
|
-
description: Uploading and downloading binary payloads.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# File uploads & downloads
|
|
7
|
-
|
|
8
|
-
The client encodes plain objects as JSON, which is the right default until the
|
|
9
|
-
payload is binary. Recognising the difference is the whole of file handling here:
|
|
10
|
-
on the way out, a body the platform already knows how to encode is passed through
|
|
11
|
-
untouched; on the way back, you name the shape you expect.
|
|
12
|
-
|
|
13
|
-
## Uploading
|
|
14
|
-
|
|
15
|
-
A body that is `FormData`, `Blob`, `File`, `URLSearchParams`, `ArrayBuffer`, or a
|
|
16
|
-
string is sent as-is with no JSON encoding, which lets `fetch` set the correct
|
|
17
|
-
`Content-Type` — including the multipart boundary, a value that cannot be written
|
|
18
|
-
by hand:
|
|
19
|
-
|
|
20
|
-
```ts
|
|
21
|
-
// in any frontend module
|
|
22
|
-
const form = new FormData();
|
|
23
|
-
form.append("avatar", file);
|
|
24
|
-
await api.post("/api/avatars", form);
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
| Body | Sent as |
|
|
28
|
-
| ----------------- | ------------------------------------------- |
|
|
29
|
-
| Plain object | JSON, with `Content-Type: application/json` |
|
|
30
|
-
| `FormData` | Multipart, boundary set by the platform |
|
|
31
|
-
| `File` / `Blob` | Raw bytes |
|
|
32
|
-
| `URLSearchParams` | Form-encoded |
|
|
33
|
-
| `ArrayBuffer` | Raw bytes |
|
|
34
|
-
| String | Sent verbatim |
|
|
35
|
-
|
|
36
|
-
Do not set `Content-Type` yourself for a `FormData` upload. A hand-written header
|
|
37
|
-
has no boundary parameter, and the server then fails to parse a body that is
|
|
38
|
-
otherwise perfectly formed — a confusing failure worth avoiding by simply leaving
|
|
39
|
-
the header alone.
|
|
40
|
-
|
|
41
|
-
To send a file alongside ordinary fields, put everything in the `FormData`;
|
|
42
|
-
mixing a JSON body and a file in one request is not possible:
|
|
43
|
-
|
|
44
|
-
```ts
|
|
45
|
-
const form = new FormData();
|
|
46
|
-
form.append("title", "Quarterly report");
|
|
47
|
-
form.append("document", file);
|
|
48
|
-
await api.post("/api/reports", form);
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
## Downloading
|
|
52
|
-
|
|
53
|
-
Responses are parsed as JSON unless you say otherwise. `responseType` names the
|
|
54
|
-
shape you want:
|
|
55
|
-
|
|
56
|
-
| `responseType` | Returns | Reach for it when |
|
|
57
|
-
| --------------- | -------------------- | ------------------------------------ |
|
|
58
|
-
| `"auto"` | Parsed JSON, or text | The default — JSON with a safety net |
|
|
59
|
-
| `"json"` | Parsed JSON, or text | The body is known to be JSON |
|
|
60
|
-
| `"text"` | `string` | CSV, XML, plain text |
|
|
61
|
-
| `"blob"` | `Blob` | Saving or displaying a file |
|
|
62
|
-
| `"arrayBuffer"` | `ArrayBuffer` | Reading bytes directly |
|
|
63
|
-
|
|
64
|
-
```ts
|
|
65
|
-
// in any frontend module
|
|
66
|
-
const pdf = await api.get("/api/report", undefined, { responseType: "blob" });
|
|
67
|
-
```
|
|
68
|
-
|
|
69
|
-
`"auto"` and `"json"` fall back to the raw text when the body will not parse, so a
|
|
70
|
-
misconfigured endpoint returning an HTML error page surfaces that page rather than
|
|
71
|
-
a parse exception.
|
|
72
|
-
|
|
73
|
-
A `204 No Content` response — or any response with `Content-Length: 0` — resolves
|
|
74
|
-
to `undefined` rather than throwing, so a `DELETE` needs no special handling:
|
|
75
|
-
|
|
76
|
-
```ts
|
|
77
|
-
await api.delete("/api/avatars/1"); // → undefined
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
Handing a downloaded blob to the browser takes one more step, since the client
|
|
81
|
-
returns the data rather than saving it:
|
|
82
|
-
|
|
83
|
-
```ts
|
|
84
|
-
const blob = await api.get("/api/report", undefined, { responseType: "blob" });
|
|
85
|
-
|
|
86
|
-
const url = URL.createObjectURL(blob);
|
|
87
|
-
const a = Object.assign(document.createElement("a"), { href: url, download: "report.pdf" });
|
|
88
|
-
a.click();
|
|
89
|
-
URL.revokeObjectURL(url);
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
## Reading response metadata
|
|
93
|
-
|
|
94
|
-
Binary endpoints often carry the interesting information in headers. A per-request
|
|
95
|
-
`meta` callback reads them without installing a global interceptor:
|
|
96
|
-
|
|
97
|
-
```ts
|
|
98
|
-
// in any frontend module
|
|
99
|
-
let total: string | null = null;
|
|
100
|
-
const users = await api.get("/api/users", undefined, {
|
|
101
|
-
meta: (m) => (total = m.headers.get("X-Total")),
|
|
102
|
-
});
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
This is the way to reach pagination totals, rate-limit counters, and `ETag` values
|
|
106
|
-
while still receiving the parsed body as the return value.
|
|
107
|
-
|
|
108
|
-
## Query serialization
|
|
109
|
-
|
|
110
|
-
Query objects serialize arrays and nested objects with bracket notation:
|
|
111
|
-
`{ ids: [1, 2], filter: { status: "open" } }` becomes
|
|
112
|
-
`?ids[]=1&ids[]=2&filter[status]=open`.
|
|
113
|
-
|
|
114
|
-
## Next steps
|
|
115
|
-
|
|
116
|
-
- [Client overview](/docs/client) — the guide's front page and the rest of the sections.
|
|
117
|
-
- [Making requests](/docs/client/requests) — the full request surface.
|
|
118
|
-
- [Error handling](/docs/client/errors) — what a failed transfer throws.
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Client References
|
|
3
|
-
description: Every ApiClient method, config key, and error type in one table.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# References
|
|
7
|
-
|
|
8
|
-
## `createApiClient<Routes>(config)`
|
|
9
|
-
|
|
10
|
-
Returns an `ApiClient<Routes>` bound to your route map. The `config` is an
|
|
11
|
-
`ApiClientConfig` (see the [Configuration](/docs/client#configuration) table for the common
|
|
12
|
-
fields, plus `onError`, `onResponse`, `onRequest`, `onUnauthorized`, `onForbidden`,
|
|
13
|
-
and `circuitBreaker`).
|
|
14
|
-
|
|
15
|
-
## ApiClient methods
|
|
16
|
-
|
|
17
|
-
| Method | Signature | Description |
|
|
18
|
-
| ---------- | -------------------------------------------------------------------- | ---------------------------------- |
|
|
19
|
-
| `get` | `get(path, params?, options?: GetOptions): Promise<Response>` | Typed GET; path params then query. |
|
|
20
|
-
| `post` | `post(path, body?, options?: MutationOptions): Promise<Response>` | Typed POST with body. |
|
|
21
|
-
| `put` | `put(path, body?, options?: MutationOptions): Promise<Response>` | Typed PUT with body. |
|
|
22
|
-
| `patch` | `patch(path, body?, options?: MutationOptions): Promise<Response>` | Typed PATCH with body. |
|
|
23
|
-
| `delete` | `delete(path, params?, options?: RequestOptions): Promise<Response>` | Typed DELETE; path params only. |
|
|
24
|
-
| `setToken` | `setToken(token: TokenSource \| null): void` | Update or clear the bearer token. |
|
|
25
|
-
|
|
26
|
-
## ApiClientError
|
|
27
|
-
|
|
28
|
-
| Member | Signature | Description |
|
|
29
|
-
| -------------- | ----------------------- | -------------------------------------- |
|
|
30
|
-
| `status` | `number` | HTTP status code. |
|
|
31
|
-
| `statusText` | `string` | HTTP status text. |
|
|
32
|
-
| `body` | `string` | Raw response text. |
|
|
33
|
-
| `headers` | `Headers \| undefined` | Response headers, when available. |
|
|
34
|
-
| `retryAfterMs` | `get(): number \| null` | Parsed `Retry-After` in ms, or `null`. |
|
|
35
|
-
|
|
36
|
-
## ValidationError extends ApiClientError
|
|
37
|
-
|
|
38
|
-
| Member | Signature | Description |
|
|
39
|
-
| ------------------- | ------------------------------------------- | ---------------------------------- |
|
|
40
|
-
| `errors` | `Record<string, string[]>` | Field → messages map. |
|
|
41
|
-
| `validationMessage` | `string` | Top-level `message` from the body. |
|
|
42
|
-
| `has` | `has(field: string): boolean` | Whether a field has any error. |
|
|
43
|
-
| `first` | `first(field: string): string \| undefined` | First message for a field. |
|
|
44
|
-
| `all` | `all(): Record<string, string[]>` | The full field-error map. |
|
|
45
|
-
| `fields` | `fields(): string[]` | Names of every failed field. |
|
|
46
|
-
|
|
47
|
-
## CircuitBreaker
|
|
48
|
-
|
|
49
|
-
| Member | Signature | Description |
|
|
50
|
-
| ---------- | ------------------------------------------- | --------------------------------------- |
|
|
51
|
-
| `call` | `call<T>(fn: () => Promise<T>): Promise<T>` | Run `fn` under the breaker. |
|
|
52
|
-
| `state` | `get(): CircuitState` | `'closed'`, `'open'`, or `'half-open'`. |
|
|
53
|
-
| `failures` | `get(): number` | Current consecutive failure count. |
|
|
54
|
-
| `reset` | `reset(): void` | Manually return to the closed state. |
|
|
55
|
-
|
|
56
|
-
## Next steps
|
|
57
|
-
|
|
58
|
-
- [Client overview](/docs/client) — the guide's front page and the rest of the sections.
|
package/docs/client/requests.md
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Client Requests
|
|
3
|
-
description: The typed route map, making requests, and shaping them with interceptors.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Route map
|
|
7
|
-
|
|
8
|
-
Define your API surface once. Keys are `'METHOD /path'` strings — path params are
|
|
9
|
-
`{braces}` style. All fields are optional.
|
|
10
|
-
|
|
11
|
-
```ts
|
|
12
|
-
// app/api/api-types.ts
|
|
13
|
-
export interface Routes {
|
|
14
|
-
"GET /api/users": {
|
|
15
|
-
query: { page?: number; perPage?: number; search?: string };
|
|
16
|
-
response: { data: UserResource[]; total: number };
|
|
17
|
-
};
|
|
18
|
-
"GET /api/users/{id}": {
|
|
19
|
-
params: { id: number };
|
|
20
|
-
response: UserResource;
|
|
21
|
-
};
|
|
22
|
-
"POST /api/users": {
|
|
23
|
-
body: { name: string; email: string; password: string };
|
|
24
|
-
response: UserResource;
|
|
25
|
-
};
|
|
26
|
-
"PUT /api/users/{id}": {
|
|
27
|
-
params: { id: number };
|
|
28
|
-
body: { name?: string; email?: string };
|
|
29
|
-
response: UserResource;
|
|
30
|
-
};
|
|
31
|
-
"DELETE /api/users/{id}": {
|
|
32
|
-
params: { id: number };
|
|
33
|
-
response: void;
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
## Making requests
|
|
39
|
-
|
|
40
|
-
```ts
|
|
41
|
-
// in any frontend module
|
|
42
|
-
// GET with path params
|
|
43
|
-
const user = await api.get("/api/users/{id}", { id: 42 });
|
|
44
|
-
// ^? UserResource — inferred from the route map
|
|
45
|
-
|
|
46
|
-
// GET with query string
|
|
47
|
-
const list = await api.get("/api/users", undefined, {
|
|
48
|
-
query: { page: 2, perPage: 25, search: "alice" },
|
|
49
|
-
});
|
|
50
|
-
// ^? { data: UserResource[]; total: number }
|
|
51
|
-
|
|
52
|
-
// POST with body
|
|
53
|
-
const created = await api.post("/api/users", {
|
|
54
|
-
name: "Alice",
|
|
55
|
-
email: "alice@example.com",
|
|
56
|
-
password: "hunter2",
|
|
57
|
-
});
|
|
58
|
-
// ^? UserResource
|
|
59
|
-
|
|
60
|
-
// PUT
|
|
61
|
-
await api.put("/api/users/{id}", { name: "Alice Smith" }, { params: { id: 42 } });
|
|
62
|
-
|
|
63
|
-
// PATCH
|
|
64
|
-
await api.patch("/api/users/{id}", { email: "new@example.com" }, { params: { id: 42 } });
|
|
65
|
-
|
|
66
|
-
// DELETE
|
|
67
|
-
await api.delete("/api/users/{id}", { id: 42 });
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
All methods accept an optional `options` argument for per-request headers and
|
|
71
|
-
extra `fetch` init fields (`signal`, `credentials`, etc.):
|
|
72
|
-
|
|
73
|
-
```ts
|
|
74
|
-
// in any frontend module
|
|
75
|
-
const ctrl = new AbortController();
|
|
76
|
-
|
|
77
|
-
await api.get("/api/users", undefined, {
|
|
78
|
-
headers: { "X-Trace-Id": requestId },
|
|
79
|
-
init: { signal: ctrl.signal },
|
|
80
|
-
});
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
## Request interceptors
|
|
84
|
-
|
|
85
|
-
Run one or more async functions before every outgoing request. Each interceptor
|
|
86
|
-
receives the current `RequestConfig` and must return it (or a new one). Useful for
|
|
87
|
-
attaching authorization headers from a reactive store without coupling the store
|
|
88
|
-
to the client's constructor.
|
|
89
|
-
|
|
90
|
-
```ts
|
|
91
|
-
// app/api/client.ts
|
|
92
|
-
const api = createApiClient<Routes>({
|
|
93
|
-
baseUrl: "https://api.example.com",
|
|
94
|
-
|
|
95
|
-
// Single interceptor
|
|
96
|
-
onRequest: async (config) => ({
|
|
97
|
-
...config,
|
|
98
|
-
headers: {
|
|
99
|
-
...config.headers,
|
|
100
|
-
Authorization: `Bearer ${await tokenStore.get()}`,
|
|
101
|
-
},
|
|
102
|
-
}),
|
|
103
|
-
});
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
Multiple interceptors execute in declaration order:
|
|
107
|
-
|
|
108
|
-
```ts
|
|
109
|
-
// app/api/client.ts
|
|
110
|
-
const api = createApiClient<Routes>({
|
|
111
|
-
baseUrl: "https://api.example.com",
|
|
112
|
-
onRequest: [addAuthHeader, addRequestId, logOutgoing],
|
|
113
|
-
});
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
A symmetric `onResponse` runs after every successful (2xx) response — receiving a
|
|
117
|
-
`ResponseContext` of `{ status, headers, data, request }` — to unwrap envelopes or log.
|
|
118
|
-
|
|
119
|
-
### RequestConfig shape
|
|
120
|
-
|
|
121
|
-
| Field | Type | Description |
|
|
122
|
-
| --------- | ------------------------ | ------------------------------------------------------------ |
|
|
123
|
-
| `method` | `string` | HTTP verb — `'GET'`, `'POST'`, … |
|
|
124
|
-
| `url` | `string` | Full resolved URL (base + path + query string) |
|
|
125
|
-
| `headers` | `Record<string, string>` | Merged headers — add/override here |
|
|
126
|
-
| `body` | `BodyInit \| undefined` | Serialised body (JSON string, `FormData`, …), or `undefined` |
|
|
127
|
-
|
|
128
|
-
## Next steps
|
|
129
|
-
|
|
130
|
-
- [Client overview](/docs/client) — the guide's front page and the rest of the sections.
|
|
131
|
-
- [Reference](/docs/client/references) — the full API surface in one table.
|