@zerotal/arch 1.7.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.
Files changed (160) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LICENSE +21 -0
  3. package/README.md +67 -0
  4. package/api-surface.md +283 -0
  5. package/docs/about.md +676 -0
  6. package/docs/admin/actions.md +183 -0
  7. package/docs/admin/auth.md +89 -0
  8. package/docs/admin/dashboard.md +155 -0
  9. package/docs/admin/extending-ui.md +229 -0
  10. package/docs/admin/extending.md +205 -0
  11. package/docs/admin/forms.md +155 -0
  12. package/docs/admin/index.md +144 -0
  13. package/docs/admin/operations.md +224 -0
  14. package/docs/admin/references.md +174 -0
  15. package/docs/admin/resources.md +132 -0
  16. package/docs/admin/structure.md +158 -0
  17. package/docs/admin/tables.md +185 -0
  18. package/docs/admin/testing.md +138 -0
  19. package/docs/ai.md +373 -0
  20. package/docs/application.md +435 -0
  21. package/docs/arch.md +263 -0
  22. package/docs/assets.md +755 -0
  23. package/docs/audit.md +367 -0
  24. package/docs/authentication.md +1000 -0
  25. package/docs/authorization.md +529 -0
  26. package/docs/broadcasting/channels.md +170 -0
  27. package/docs/broadcasting/client.md +108 -0
  28. package/docs/broadcasting/events.md +190 -0
  29. package/docs/broadcasting/index.md +163 -0
  30. package/docs/broadcasting/references.md +84 -0
  31. package/docs/broadcasting/testing.md +132 -0
  32. package/docs/cache.md +387 -0
  33. package/docs/carbon.md +830 -0
  34. package/docs/changelog.md +465 -0
  35. package/docs/client/auth.md +113 -0
  36. package/docs/client/errors.md +139 -0
  37. package/docs/client/files.md +118 -0
  38. package/docs/client/index.md +128 -0
  39. package/docs/client/references.md +58 -0
  40. package/docs/client/requests.md +131 -0
  41. package/docs/client/resilience.md +141 -0
  42. package/docs/client/testing.md +146 -0
  43. package/docs/commands.md +450 -0
  44. package/docs/components.md +2427 -0
  45. package/docs/config-system.md +258 -0
  46. package/docs/container.md +596 -0
  47. package/docs/context.md +579 -0
  48. package/docs/contributing.md +164 -0
  49. package/docs/controllers.md +354 -0
  50. package/docs/conventions.md +466 -0
  51. package/docs/cookies.md +101 -0
  52. package/docs/csrf.md +206 -0
  53. package/docs/database.md +476 -0
  54. package/docs/deployment.md +434 -0
  55. package/docs/devtools.md +1013 -0
  56. package/docs/email-verification.md +320 -0
  57. package/docs/encryption.md +380 -0
  58. package/docs/errors.md +317 -0
  59. package/docs/events.md +420 -0
  60. package/docs/flow/components.md +846 -0
  61. package/docs/flow/decorators.md +458 -0
  62. package/docs/flow/events.md +420 -0
  63. package/docs/flow/forms.md +551 -0
  64. package/docs/flow/index.md +568 -0
  65. package/docs/flow/layouts.md +793 -0
  66. package/docs/flow/lifecycle.md +336 -0
  67. package/docs/flow/pagination.md +402 -0
  68. package/docs/flow/performance.md +262 -0
  69. package/docs/flow/references.md +317 -0
  70. package/docs/flow/routing.md +550 -0
  71. package/docs/flow/testing.md +447 -0
  72. package/docs/getting-started.md +286 -0
  73. package/docs/health.md +269 -0
  74. package/docs/helpers.md +423 -0
  75. package/docs/i18n.md +414 -0
  76. package/docs/index.md +65 -0
  77. package/docs/inertia/build.md +95 -0
  78. package/docs/inertia/devtools.md +197 -0
  79. package/docs/inertia/index.md +267 -0
  80. package/docs/inertia/middleware.md +81 -0
  81. package/docs/inertia/props.md +507 -0
  82. package/docs/inertia/references.md +144 -0
  83. package/docs/inertia/rendering.md +189 -0
  84. package/docs/inertia/ssr.md +102 -0
  85. package/docs/inspirations.md +257 -0
  86. package/docs/lifecycle.md +331 -0
  87. package/docs/lock.md +454 -0
  88. package/docs/logger.md +517 -0
  89. package/docs/media.md +582 -0
  90. package/docs/middleware.md +509 -0
  91. package/docs/migrations.md +529 -0
  92. package/docs/monitor.md +377 -0
  93. package/docs/notifications.md +848 -0
  94. package/docs/orm/casts.md +365 -0
  95. package/docs/orm/factories.md +231 -0
  96. package/docs/orm/index.md +663 -0
  97. package/docs/orm/lifecycle.md +421 -0
  98. package/docs/orm/queries.md +692 -0
  99. package/docs/orm/relationships.md +440 -0
  100. package/docs/orm/serialization.md +251 -0
  101. package/docs/package-development.md +536 -0
  102. package/docs/pagination.md +340 -0
  103. package/docs/password-reset.md +335 -0
  104. package/docs/providers.md +447 -0
  105. package/docs/query-builder.md +465 -0
  106. package/docs/queue.md +549 -0
  107. package/docs/rate-limiting.md +253 -0
  108. package/docs/responses.md +336 -0
  109. package/docs/roles-and-2fa.md +418 -0
  110. package/docs/routing.md +1008 -0
  111. package/docs/scaffolding.md +335 -0
  112. package/docs/scheduler.md +533 -0
  113. package/docs/seeding.md +229 -0
  114. package/docs/session.md +451 -0
  115. package/docs/social.md +524 -0
  116. package/docs/storage.md +622 -0
  117. package/docs/structure.md +230 -0
  118. package/docs/support-policy.md +114 -0
  119. package/docs/telemetry.md +505 -0
  120. package/docs/tenancy.md +608 -0
  121. package/docs/testing/browser.md +153 -0
  122. package/docs/testing/console.md +130 -0
  123. package/docs/testing/database.md +238 -0
  124. package/docs/testing/flow-browser.md +216 -0
  125. package/docs/testing/http.md +441 -0
  126. package/docs/testing/index.md +240 -0
  127. package/docs/testing/mocking.md +410 -0
  128. package/docs/upgrade.md +192 -0
  129. package/docs/validator.md +549 -0
  130. package/docs/view.md +453 -0
  131. package/package.json +60 -0
  132. package/src/bin/mcp.ts +73 -0
  133. package/src/config.ts +63 -0
  134. package/src/errors.ts +27 -0
  135. package/src/index.ts +56 -0
  136. package/src/install/ArchInstallCommand.ts +189 -0
  137. package/src/install/detect.ts +98 -0
  138. package/src/install/guidelines.ts +290 -0
  139. package/src/install/markers.ts +77 -0
  140. package/src/install/mcpConfig.ts +81 -0
  141. package/src/mcp/index.ts +27 -0
  142. package/src/mcp/jsonrpc.ts +133 -0
  143. package/src/mcp/server.ts +307 -0
  144. package/src/mcp/stdio.ts +111 -0
  145. package/src/mcp/types.ts +146 -0
  146. package/src/probe/ArchProbeCommand.ts +48 -0
  147. package/src/probe/sentinel.ts +13 -0
  148. package/src/probe/topics.ts +291 -0
  149. package/src/provider/ArchProvider.ts +101 -0
  150. package/src/tools/_probe.ts +164 -0
  151. package/src/tools/apiSurface.ts +241 -0
  152. package/src/tools/appInfo.ts +88 -0
  153. package/src/tools/baselines.ts +210 -0
  154. package/src/tools/context.ts +37 -0
  155. package/src/tools/doctor.ts +93 -0
  156. package/src/tools/index.ts +50 -0
  157. package/src/tools/logs.ts +265 -0
  158. package/src/tools/routes.ts +133 -0
  159. package/src/tools/schema.ts +119 -0
  160. package/src/tools/searchDocs.ts +345 -0
@@ -0,0 +1,139 @@
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.
@@ -0,0 +1,118 @@
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.
@@ -0,0 +1,128 @@
1
+ ---
2
+ title: API Client
3
+ description: Call your HTTP API with full TypeScript inference of params, body, query, and response shapes.
4
+ ---
5
+
6
+ # API Client
7
+
8
+ A type-safe HTTP client bound to a route map. The TypeScript compiler infers
9
+ request body types, path parameters, query parameters, and response shapes directly
10
+ from the map — no casting required.
11
+
12
+ This is the **frontend/SPA** client for calling _your own_ typed API. For
13
+ **server-to-server** outgoing HTTP from inside your app, reach for core's `Http`
14
+ facade ([HTTP Client helpers](/docs/context)) instead — it's the fluent,
15
+ fakeable, server-side HTTP client. Same protocol, different jobs.
16
+
17
+ ## Getting Started
18
+
19
+ ```bash
20
+ # in your project root
21
+ bun add @zerotal/client
22
+ ```
23
+
24
+ You can use the client two ways: create instances yourself with `createApiClient()`
25
+ (typical for the browser/frontend), or register `ClientProvider` to resolve a shared,
26
+ config-driven client from the container (typical for server-side and console code).
27
+
28
+ ## Register the provider
29
+
30
+ Registering the provider is optional — it gives you one container-bound `client`
31
+ built from `config/client.ts`. Add `ClientProvider` to the providers array in
32
+ `bootstrap/providers.ts`:
33
+
34
+ ```ts
35
+ // bootstrap/providers.ts
36
+ import { ClientProvider } from "@zerotal/client";
37
+
38
+ const providers = [
39
+ // …your other providers
40
+ ClientProvider,
41
+ ];
42
+
43
+ export default providers;
44
+ ```
45
+
46
+ Registering the provider switches on the following:
47
+
48
+ - `onRegister` — binds an `ApiClient` as a lazy singleton under the `client` key, built from the `client` config namespace.
49
+ - `onBooted` — pre-resolves that async singleton so the `Client` facade can be accessed synchronously after boot.
50
+
51
+ The provider is active in the `web`, `console`, `worker`, `test`, and `repl`
52
+ environments. Resolve the shared client via the container or the `Client` facade:
53
+
54
+ ```ts
55
+ // in a controller or service
56
+ import { Client } from "@zerotal/client";
57
+
58
+ const users = await Client.get("/api/users");
59
+ ```
60
+
61
+ ## Configuration
62
+
63
+ Create `config/client.ts` with the `ClientConfig()` helper (or `satisfies
64
+ ClientConfigShape`) so every field stays type-checked. The shape extends
65
+ [`ApiClientConfig`](/docs/client/references) — every client option below is valid here:
66
+
67
+ ```ts
68
+ // config/client.ts
69
+ import { ClientConfig } from "@zerotal/client";
70
+ import { env } from "zerotal";
71
+
72
+ export default ClientConfig({
73
+ baseUrl: env("API_BASE_URL", "https://api.example.com"),
74
+ headers: { Accept: "application/json" },
75
+ });
76
+ ```
77
+
78
+ | Field | Required | Default | Description |
79
+ | ----------------- | -------- | ------- | -------------------------------------------------------------- |
80
+ | `baseUrl` | no | `""` | Base URL prepended to every request path. |
81
+ | `headers` | no | `{}` | Default headers sent with every request. |
82
+ | `token` | no | — | Bearer token (string or resolver) attached as `Authorization`. |
83
+ | `withCredentials` | no | `false` | Send cookies (`credentials: 'include'`); also turns CSRF on. |
84
+ | `csrf` | no | — | CSRF cookie/header names, or a boolean to force on/off. |
85
+ | `timeout` | no | — | Default per-request timeout in ms (`0`/omitted = none). |
86
+ | `retry` | no | — | Default retry policy (number of attempts or `RetryOptions`). |
87
+
88
+ > **Note** — The provider carries no framework-level defaults of its own; `ApiClient`
89
+ > applies its own internal defaults. The config file is just typed input.
90
+
91
+ ## Create a client directly
92
+
93
+ In the browser (or anywhere you want a standalone instance), build a client with
94
+ `createApiClient<Routes>()`:
95
+
96
+ ```ts
97
+ // app/api/client.ts
98
+ import { createApiClient } from "@zerotal/client";
99
+ import { env } from "zerotal";
100
+ import type { Routes } from "./api-types.ts";
101
+
102
+ export const api = createApiClient<Routes>({
103
+ baseUrl: env("API_BASE_URL", "https://api.example.com"),
104
+ headers: { Accept: "application/json" },
105
+ });
106
+ ```
107
+
108
+ > **Tip** — Don't instantiate `ApiClient` directly; `createApiClient<Routes>(config)`
109
+ > binds the route map type for you.
110
+
111
+ ## The rest of the guide
112
+
113
+ | Page | What it covers |
114
+ | ------------------------------------------ | ------------------------------------------------------------------------------ |
115
+ | [Requests](/docs/client/requests) | The typed route map, making requests, and shaping them with interceptors. |
116
+ | [Authentication](/docs/client/auth) | Bearer tokens, CSRF, and refreshing credentials on a 401. |
117
+ | [Error Handling](/docs/client/errors) | What a failed request throws, and how to tell the failure modes apart. |
118
+ | [Resilience](/docs/client/resilience) | Timeouts, retries, and the circuit breaker that spares a failing upstream. |
119
+ | [File Transfers](/docs/client/files) | Uploading and downloading binary payloads. |
120
+ | [Testing the Client](/docs/client/testing) | Stub the global fetch, cover the failure paths, and drive the circuit breaker. |
121
+ | [References](/docs/client/references) | Every ApiClient method, config key, and error type in one table. |
122
+
123
+ ## Next steps
124
+
125
+ - [Rate Limiting](/docs/rate-limiting) — throttle outbound and inbound traffic.
126
+ - [Authentication](/docs/authentication) — issue the tokens your interceptors attach.
127
+ - [Validator](/docs/validator) — the server side that produces the `ValidationError` body.
128
+ - [Telemetry](/docs/telemetry) — observe request failures and circuit state.
@@ -0,0 +1,58 @@
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.
@@ -0,0 +1,131 @@
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.