@finesoft/front 0.1.75 → 0.1.77

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 (58) hide show
  1. package/README.md +2 -411
  2. package/dist/browser.d.mts +2 -0
  3. package/dist/browser.mjs +1 -0
  4. package/dist/index.d.mts +2 -1248
  5. package/dist/index.mjs +54 -3557
  6. package/dist/server-data-DGbiKzMS.d.mts +1249 -0
  7. package/dist/start-app-BdXBCcor.mjs +2 -0
  8. package/docs/01-getting-started.md +230 -0
  9. package/docs/02-routing-and-controllers.md +197 -0
  10. package/docs/03-middleware.md +214 -0
  11. package/docs/04-rendering-and-hydration.md +271 -0
  12. package/docs/05-i18n.md +243 -0
  13. package/docs/06-http-client.md +286 -0
  14. package/docs/07-di-container.md +264 -0
  15. package/docs/08-observability.md +290 -0
  16. package/docs/09-server-and-deployment.md +242 -0
  17. package/docs/10-features-platform-pwa.md +238 -0
  18. package/docs/README.md +72 -0
  19. package/docs/advanced/custom-action-handler.md +248 -0
  20. package/docs/advanced/custom-adapter.md +264 -0
  21. package/docs/advanced/custom-event-recorder.md +318 -0
  22. package/docs/advanced/inline-proxy-codegen.md +200 -0
  23. package/docs/advanced/multi-tenant-scopes.md +330 -0
  24. package/docs/engineering/ci-release-flow.md +244 -0
  25. package/docs/engineering/project-structure.md +296 -0
  26. package/docs/engineering/testing.md +317 -0
  27. package/docs/pitfalls/container-scope-leak.md +215 -0
  28. package/docs/pitfalls/i18n-bundle-size.md +182 -0
  29. package/docs/pitfalls/proxy-binary-payloads.md +133 -0
  30. package/docs/pitfalls/redirect-vs-rewrite.md +147 -0
  31. package/docs/pitfalls/ssr-hydration-mismatch.md +163 -0
  32. package/docs/pitfalls/ssr-vs-csr-globals.md +176 -0
  33. package/docs/zh/01-getting-started.md +230 -0
  34. package/docs/zh/02-routing-and-controllers.md +197 -0
  35. package/docs/zh/03-middleware.md +214 -0
  36. package/docs/zh/04-rendering-and-hydration.md +271 -0
  37. package/docs/zh/05-i18n.md +243 -0
  38. package/docs/zh/06-http-client.md +286 -0
  39. package/docs/zh/07-di-container.md +264 -0
  40. package/docs/zh/08-observability.md +287 -0
  41. package/docs/zh/09-server-and-deployment.md +242 -0
  42. package/docs/zh/10-features-platform-pwa.md +238 -0
  43. package/docs/zh/README.md +72 -0
  44. package/docs/zh/advanced/custom-action-handler.md +248 -0
  45. package/docs/zh/advanced/custom-adapter.md +264 -0
  46. package/docs/zh/advanced/custom-event-recorder.md +318 -0
  47. package/docs/zh/advanced/inline-proxy-codegen.md +200 -0
  48. package/docs/zh/advanced/multi-tenant-scopes.md +330 -0
  49. package/docs/zh/engineering/ci-release-flow.md +244 -0
  50. package/docs/zh/engineering/project-structure.md +296 -0
  51. package/docs/zh/engineering/testing.md +317 -0
  52. package/docs/zh/pitfalls/container-scope-leak.md +215 -0
  53. package/docs/zh/pitfalls/i18n-bundle-size.md +182 -0
  54. package/docs/zh/pitfalls/proxy-binary-payloads.md +133 -0
  55. package/docs/zh/pitfalls/redirect-vs-rewrite.md +147 -0
  56. package/docs/zh/pitfalls/ssr-hydration-mismatch.md +163 -0
  57. package/docs/zh/pitfalls/ssr-vs-csr-globals.md +176 -0
  58. package/package.json +12 -3
@@ -0,0 +1,318 @@
1
+ # Advanced: custom event recorder
2
+
3
+ Build a production-grade `EventRecorder` that batches, retries, and survives navigation. This recipe assumes you've read [chapter 8: observability](../08-observability.md).
4
+
5
+ ## Goals
6
+
7
+ A good recorder:
8
+
9
+ - **Doesn't block.** `record()` returns synchronously; transmission happens in the background.
10
+ - **Batches.** One HTTP request per N events or T seconds, not per event.
11
+ - **Survives navigation.** Pending events flush on page unload via `sendBeacon`.
12
+ - **Drops gracefully.** Network failures don't crash the app; events that fail to send don't pile up forever.
13
+ - **Lifecycle-aware.** `destroy()` flushes everything before disposal.
14
+
15
+ ## Skeleton
16
+
17
+ ```ts
18
+ // src/lib/recorders/http-recorder.ts
19
+ import type { EventRecorder, EventRecord } from "@finesoft/front";
20
+
21
+ export interface HttpRecorderOptions {
22
+ endpoint: string;
23
+ batchSize?: number;
24
+ flushIntervalMs?: number;
25
+ maxQueueSize?: number;
26
+ }
27
+
28
+ export class HttpEventRecorder implements EventRecorder {
29
+ private queue: EventRecord[] = [];
30
+ private timer: ReturnType<typeof setInterval> | null = null;
31
+ private flushing = false;
32
+ private readonly opts: Required<HttpRecorderOptions>;
33
+
34
+ constructor(options: HttpRecorderOptions) {
35
+ this.opts = {
36
+ batchSize: 50,
37
+ flushIntervalMs: 5000,
38
+ maxQueueSize: 1000,
39
+ ...options,
40
+ };
41
+
42
+ if (typeof window !== "undefined") {
43
+ this.timer = setInterval(() => this.flush(), this.opts.flushIntervalMs);
44
+ window.addEventListener("pagehide", this.beaconFlush);
45
+ window.addEventListener("beforeunload", this.beaconFlush);
46
+ }
47
+ }
48
+
49
+ record(event: EventRecord): void {
50
+ if (this.queue.length >= this.opts.maxQueueSize) {
51
+ // overflow protection — drop the oldest to bound memory
52
+ this.queue.shift();
53
+ }
54
+ this.queue.push(event);
55
+ if (this.queue.length >= this.opts.batchSize) {
56
+ void this.flush();
57
+ }
58
+ }
59
+
60
+ destroy(): void {
61
+ if (this.timer) clearInterval(this.timer);
62
+ if (typeof window !== "undefined") {
63
+ window.removeEventListener("pagehide", this.beaconFlush);
64
+ window.removeEventListener("beforeunload", this.beaconFlush);
65
+ }
66
+ this.beaconFlush();
67
+ }
68
+
69
+ private async flush(): Promise<void> {
70
+ if (this.flushing || this.queue.length === 0) return;
71
+ this.flushing = true;
72
+ const batch = this.queue.splice(0, this.opts.batchSize);
73
+
74
+ try {
75
+ const resp = await fetch(this.opts.endpoint, {
76
+ method: "POST",
77
+ headers: { "Content-Type": "application/json" },
78
+ body: JSON.stringify(batch),
79
+ keepalive: true,
80
+ });
81
+ if (!resp.ok) {
82
+ // 4xx — drop. 5xx — re-queue at the front.
83
+ if (resp.status >= 500) this.queue.unshift(...batch);
84
+ }
85
+ } catch {
86
+ // network failure — re-queue
87
+ this.queue.unshift(...batch);
88
+ } finally {
89
+ this.flushing = false;
90
+ }
91
+ }
92
+
93
+ private beaconFlush = (): void => {
94
+ if (this.queue.length === 0) return;
95
+ if (typeof navigator === "undefined" || !navigator.sendBeacon) return;
96
+ const batch = this.queue.splice(0, this.queue.length);
97
+ navigator.sendBeacon(this.opts.endpoint, JSON.stringify(batch));
98
+ };
99
+ }
100
+ ```
101
+
102
+ ## Why each piece exists
103
+
104
+ ### `keepalive: true`
105
+
106
+ Tells the browser the request should complete even if the page is navigating away. Has a max body size (~64 KB) but works across navigation. Pair with `sendBeacon` for unload — beacons are smaller and more reliable for that case.
107
+
108
+ ### `pagehide` and `beforeunload`
109
+
110
+ `pagehide` fires when the page enters the bfcache (back/forward navigation). `beforeunload` fires on regular navigation/close. Both should flush pending events. Some browsers fire one and not the other, so listen for both.
111
+
112
+ ### `keepalive` vs `sendBeacon`
113
+
114
+ | Method | Body limit | Returns response? | When |
115
+ | ---------------------- | ---------- | ----------------- | ------------------------------- |
116
+ | `fetch(..keepalive)` | ~64KB | Yes | Periodic flush during page life |
117
+ | `navigator.sendBeacon` | ~64KB | No | Final flush on unload |
118
+
119
+ Use both: periodic `fetch` for visibility into successes/failures, `sendBeacon` as the final escape hatch.
120
+
121
+ ### Re-queue on 5xx, drop on 4xx
122
+
123
+ A 5xx is the server's fault — retry later. A 4xx is your fault — retrying won't help, and infinite retry would flood the server. Drop the batch and move on.
124
+
125
+ ### Overflow protection
126
+
127
+ If the network is down for hours and your app keeps emitting events, the queue would grow without bound. `maxQueueSize` caps it; oldest events drop when new ones arrive. This trades completeness for memory safety — pick the size based on how much you can afford to lose vs how much memory you can spend.
128
+
129
+ ## Wiring it up
130
+
131
+ ```ts
132
+ // src/main.ts
133
+ import { startBrowserApp, CompositeEventRecorder, ConsoleEventRecorder } from "@finesoft/front/browser";
134
+ import { bootstrap } from "./bootstrap";
135
+ import { HttpEventRecorder } from "./lib/recorders/http-recorder";
136
+
137
+ startBrowserApp({
138
+ bootstrap,
139
+ frameworkConfig: {
140
+ eventRecorder: new CompositeEventRecorder([
141
+ new ConsoleEventRecorder(),
142
+ new HttpEventRecorder({
143
+ endpoint: "/api/events",
144
+ batchSize: 50,
145
+ flushIntervalMs: 5000,
146
+ }),
147
+ ]),
148
+ },
149
+ mount: /* ... */,
150
+ });
151
+ ```
152
+
153
+ The `CompositeEventRecorder` wraps both so events go to console (for dev visibility) **and** to your backend.
154
+
155
+ ## Adding cross-cutting fields
156
+
157
+ Decorate with `WithFieldsRecorder` to attach session-level fields:
158
+
159
+ ```ts
160
+ import { WithFieldsRecorder, type FieldProvider } from "@finesoft/front";
161
+
162
+ const sessionFields: FieldProvider = {
163
+ getFields: () => ({
164
+ sessionId: getSessionId(),
165
+ appVersion: __APP_VERSION__,
166
+ userAgent: navigator.userAgent,
167
+ }),
168
+ };
169
+
170
+ const userFields: FieldProvider = {
171
+ getFields: () => {
172
+ const user = getCurrentUser();
173
+ return user ? { userId: user.id, role: user.role } : {};
174
+ },
175
+ };
176
+
177
+ new WithFieldsRecorder(new HttpEventRecorder({ endpoint: "/api/events" }), [
178
+ sessionFields,
179
+ userFields,
180
+ ]);
181
+ ```
182
+
183
+ `getFields` runs **per event**, so user-state changes between events reflect correctly.
184
+
185
+ ## Server-side recording
186
+
187
+ The framework records `PageView` for every SSR request. If you want to capture those server-side:
188
+
189
+ ```ts
190
+ // src/ssr.ts
191
+ import { createSSRRender } from "@finesoft/front";
192
+ import { HttpEventRecorder } from "./lib/recorders/http-recorder";
193
+
194
+ // Single recorder shared across all SSR requests
195
+ const serverRecorder = new HttpEventRecorder({
196
+ endpoint: "https://internal-events.example/v1/events",
197
+ batchSize: 100, // server batches more aggressively
198
+ flushIntervalMs: 1000,
199
+ });
200
+
201
+ export const render = createSSRRender({
202
+ bootstrap,
203
+ frameworkConfig: {
204
+ eventRecorder: serverRecorder,
205
+ },
206
+ /* ... */
207
+ });
208
+
209
+ process.on("SIGTERM", () => serverRecorder.destroy());
210
+ ```
211
+
212
+ Server-side recorders should:
213
+
214
+ - Share a single instance across requests (don't construct per-request)
215
+ - Use a larger batch / longer flush interval (no UI to block)
216
+ - Wire `destroy()` into graceful shutdown so in-flight events flush
217
+
218
+ ## Integration with Sentry / Datadog
219
+
220
+ If you use both a `ReportCallback` (for `warn`/`error` logs to Sentry) and an `EventRecorder` (for structured events to your backend), keep them separate:
221
+
222
+ ```ts
223
+ Framework.create({
224
+ reportCallback: (level, category, args) => {
225
+ Sentry.captureMessage(`[${category}] ${args.join(" ")}`, level);
226
+ },
227
+ eventRecorder: new CompositeEventRecorder([
228
+ new HttpEventRecorder({ endpoint: "/api/events" }),
229
+ // Optionally: forward to Datadog too
230
+ new DatadogEventRecorder({ apiKey: env.DD_API_KEY }),
231
+ ]),
232
+ });
233
+ ```
234
+
235
+ Different sinks for different purposes — errors go to Sentry for triage, structured events go to your warehouse for analytics. Don't try to make one recorder do both.
236
+
237
+ ## Sampling
238
+
239
+ For high-traffic apps, sample events:
240
+
241
+ ```ts
242
+ class SamplingRecorder implements EventRecorder {
243
+ constructor(
244
+ private inner: EventRecorder,
245
+ private rate: number,
246
+ ) {}
247
+ record(event: EventRecord): void {
248
+ if (Math.random() < this.rate) this.inner.record(event);
249
+ }
250
+ destroy(): void {
251
+ this.inner.destroy?.();
252
+ }
253
+ }
254
+
255
+ new SamplingRecorder(new HttpEventRecorder({ endpoint: "/api/events" }), 0.1);
256
+ // Records 10% of events
257
+ ```
258
+
259
+ Sample at the recorder level, not the call site — call sites shouldn't know whether they're being sampled.
260
+
261
+ ## Testing
262
+
263
+ ```ts
264
+ import { afterEach, describe, expect, test, vi } from "vite-plus/test";
265
+ import { HttpEventRecorder } from "./http-recorder";
266
+
267
+ afterEach(() => {
268
+ vi.useRealTimers();
269
+ vi.unstubAllGlobals();
270
+ });
271
+
272
+ describe("HttpEventRecorder", () => {
273
+ test("flushes when batch fills", async () => {
274
+ const fetchMock = vi.fn().mockResolvedValue(new Response(null, { status: 200 }));
275
+ vi.stubGlobal("fetch", fetchMock);
276
+
277
+ const recorder = new HttpEventRecorder({
278
+ endpoint: "/api/events",
279
+ batchSize: 3,
280
+ flushIntervalMs: 60_000,
281
+ });
282
+
283
+ recorder.record({ name: "A", fields: {} });
284
+ recorder.record({ name: "B", fields: {} });
285
+ recorder.record({ name: "C", fields: {} });
286
+
287
+ await vi.waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(1));
288
+ expect(JSON.parse(fetchMock.mock.calls[0][1].body)).toHaveLength(3);
289
+ });
290
+
291
+ test("re-queues batch on 5xx", async () => {
292
+ const fetchMock = vi
293
+ .fn()
294
+ .mockResolvedValueOnce(new Response(null, { status: 503 }))
295
+ .mockResolvedValueOnce(new Response(null, { status: 200 }));
296
+ vi.stubGlobal("fetch", fetchMock);
297
+
298
+ const recorder = new HttpEventRecorder({
299
+ endpoint: "/api/events",
300
+ batchSize: 1,
301
+ flushIntervalMs: 60_000,
302
+ });
303
+
304
+ recorder.record({ name: "A", fields: {} });
305
+ await vi.waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(1));
306
+
307
+ // simulate the next flush
308
+ await (recorder as any).flush();
309
+
310
+ expect(fetchMock).toHaveBeenCalledTimes(2);
311
+ });
312
+ });
313
+ ```
314
+
315
+ ## Related
316
+
317
+ - [Chapter 8: Observability](../08-observability.md) — base primitives and built-in events
318
+ - The framework's own composite/with-fields recorders: `packages/core/src/metrics/`
@@ -0,0 +1,200 @@
1
+ # Advanced: inline proxy codegen
2
+
3
+ For serverless and edge deployments where you want proxy logic inlined into the function bundle — no runtime call to `registerProxyRoutes`, no extra dependencies — the framework exposes `generateProxyCode`.
4
+
5
+ ## Use case
6
+
7
+ You're deploying to Cloudflare Workers / Vercel Edge / AWS Lambda@Edge. Each function has:
8
+
9
+ - Tight cold-start budget
10
+ - Cold-bundle-size budget (Workers: 1 MB compressed)
11
+ - No `process.env` in some runtimes
12
+
13
+ Importing the proxy router and its support files (validators, Hono integration) adds bytes. `generateProxyCode` emits **only** the lines you need for the routes you declared. The output is self-contained: a few `app.get(...)` / `app.all(...)` calls plus a single `_sanitizeProxyPath` helper.
14
+
15
+ ## Generated output
16
+
17
+ Given this input:
18
+
19
+ ```ts
20
+ import { generateProxyCode } from "@finesoft/front";
21
+
22
+ const code = generateProxyCode([
23
+ {
24
+ prefix: "/api",
25
+ target: "https://upstream.example",
26
+ headers: { "X-App": "myapp" },
27
+ auth: { type: "bearer", envKey: "API_TOKEN" },
28
+ cache: "max-age=60",
29
+ },
30
+ ]);
31
+
32
+ console.log(code);
33
+ ```
34
+
35
+ You get something like:
36
+
37
+ ```js
38
+ // ─── 框架声明式代理路由 ───
39
+ function _sanitizeProxyPath(raw) {
40
+ if (raw.length > 2048) return null;
41
+ try {
42
+ if (decodeURIComponent(raw) !== raw) return null;
43
+ } catch {
44
+ return null;
45
+ }
46
+ if (raw.startsWith("//")) return null;
47
+ if (!/^[/\w.\-~%:@!$&'()*+,;=]*$/.test(raw)) return null;
48
+ return raw.startsWith("/") ? raw : "/" + raw;
49
+ }
50
+
51
+ app.all("/api/*", async (c) => {
52
+ const _sub = _sanitizeProxyPath(c.req.path.replace("/api", ""));
53
+ if (!_sub) return c.text("Invalid path", 400);
54
+ const _target = new URL(_sub, "https://upstream.example");
55
+ if (_target.origin !== "https://upstream.example") return c.text("Invalid proxy target", 400);
56
+ const _reqUrl = new URL(c.req.url);
57
+ _reqUrl.searchParams.forEach((v, k) => _target.searchParams.set(k, v));
58
+ const _headers = { "X-App": "myapp" };
59
+ const _token =
60
+ (typeof process !== "undefined" && process.env && process.env["API_TOKEN"]) || "";
61
+ if (_token) _headers.Authorization = "Bearer " + _token;
62
+ try {
63
+ const _resp = await fetch(_target.toString(), { headers: _headers, redirect: "manual" });
64
+ const _cl = _resp.headers.get("Content-Length");
65
+ if (_cl && parseInt(_cl, 10) > 10485760) {
66
+ return c.text("Proxy response too large", 502);
67
+ }
68
+ const _body = await _resp.arrayBuffer();
69
+ if (_body.byteLength > 10485760) {
70
+ return c.text("Proxy response too large", 502);
71
+ }
72
+ const _rh = { "Content-Type": _resp.headers.get("Content-Type") || "application/json" };
73
+ if ("max-age=60") _rh["Cache-Control"] = "max-age=60";
74
+ return c.newResponse(_body, _resp.status, _rh);
75
+ } catch (_e) {
76
+ console.error("[Proxy /api]", _e);
77
+ return c.json({ error: "Proxy request failed" }, 502);
78
+ }
79
+ });
80
+ ```
81
+
82
+ Everything is inline. No imports from `@finesoft/front` for the proxy path. Drop this into your function bundle alongside the SSR entry.
83
+
84
+ ## When to use codegen vs runtime registration
85
+
86
+ | Concern | Runtime (`registerProxyRoutes`) | Codegen (`generateProxyCode`) |
87
+ | ----------------------------------------- | ------------------------------- | ------------------------------------------- |
88
+ | Long-lived server (Node, Workers) | ✅ preferred | ✅ also fine |
89
+ | Tiny edge functions (Lambda@Edge) | Heavier import | ✅ minimal |
90
+ | Need to update routes without redeploying | ✅ change config, restart | ❌ redeploy required |
91
+ | Config from a remote service | ✅ supported | ❌ codegen runs at build time |
92
+ | Multiple proxies sharing helpers | ✅ shared at runtime | Code duplication unless you dedupe yourself |
93
+
94
+ Use codegen specifically when bundle size matters. For most deployments, the runtime path is fine.
95
+
96
+ ## Build-time integration
97
+
98
+ A typical setup:
99
+
100
+ ```ts
101
+ // scripts/build-proxy.mjs
102
+ import { generateProxyCode } from "@finesoft/front";
103
+ import { writeFile } from "node:fs/promises";
104
+
105
+ const code = generateProxyCode([
106
+ { prefix: "/api/users", target: "https://users.internal" },
107
+ { prefix: "/api/products", target: "https://products.internal", cache: "max-age=30" },
108
+ {
109
+ prefix: "/api/orders",
110
+ target: "https://orders.internal",
111
+ auth: { type: "bearer", envKey: "ORDERS_TOKEN" },
112
+ },
113
+ ]);
114
+
115
+ const wrapper = `
116
+ import { Hono } from "hono";
117
+ const app = new Hono();
118
+
119
+ ${code}
120
+
121
+ export default app;
122
+ `;
123
+
124
+ await writeFile("dist/proxy.js", wrapper, "utf8");
125
+ ```
126
+
127
+ Then import `./proxy.js` from your serverless function entry:
128
+
129
+ ```ts
130
+ // dist/main.ts (for Cloudflare Worker)
131
+ import proxyApp from "./proxy.js";
132
+ import ssrApp from "./ssr-bundle.js";
133
+
134
+ const app = new Hono();
135
+ app.route("/", proxyApp);
136
+ app.route("/", ssrApp);
137
+
138
+ export default app;
139
+ ```
140
+
141
+ ## What the generated code does for you
142
+
143
+ The generated handler enforces the same guarantees as the runtime path:
144
+
145
+ - **SSRF protection**: path validation rejects encoded chars, `//` prefix, non-allowed characters
146
+ - **Open-redirect protection**: `target.origin` must match the configured target's origin
147
+ - **10 MB response size limit**: `Content-Length` fast-reject + `byteLength` actual-bytes check
148
+ - **Binary integrity**: `arrayBuffer()` forwarding (no UTF-8 decoding)
149
+ - **Auth from env**: reads `process.env[envKey]` at request time
150
+
151
+ The framework's test suite asserts **parity** between runtime and generated code with these checks:
152
+
153
+ ```ts
154
+ // packages/server/test/proxy.test.ts
155
+ test("generated proxy code embeds the same response size limit as runtime (parity)", () => {
156
+ const code = generateProxyCode([{ prefix: "/api", target: "https://upstream.example" }]);
157
+
158
+ const MAX = String(10 * 1024 * 1024);
159
+ expect(code).toContain(`parseInt(_cl, 10) > ${MAX}`);
160
+ expect(code).toContain(`_body.byteLength > ${MAX}`);
161
+ });
162
+ ```
163
+
164
+ If you patch the runtime path's size limit, the generated code's limit is updated in lockstep.
165
+
166
+ ## Caveats
167
+
168
+ ### `process.env` may not exist
169
+
170
+ The generated code guards with `typeof process !== "undefined"`. On runtimes without `process` (some edge environments), the auth header is simply not added — the upstream sees no auth.
171
+
172
+ For platforms like Cloudflare Workers that inject env via function args instead of `process.env`, you'll need to either:
173
+
174
+ - Wrap the generated code to inject the auth header from the worker's env arg
175
+ - Replace the auth section after generation with the platform-appropriate access
176
+
177
+ ### No retries, no breakers
178
+
179
+ The generated handler does one `fetch` and bubbles failures up as `502 Proxy request failed`. For retry / breaker logic, write your own proxy code — `generateProxyCode` is intentionally minimal.
180
+
181
+ ### Multiple proxies share helper code
182
+
183
+ `_sanitizeProxyPath` is emitted once at the top of the generated string. Multiple `app.all` calls share it. If you `generateProxyCode` separately for each route and concatenate, you'll get the helper repeated — call it once with all routes.
184
+
185
+ ### Validation at generation time
186
+
187
+ `generateProxyCode` runs the same `validateConfig` as `registerProxyRoutes`. Invalid configs throw at build time:
188
+
189
+ ```ts
190
+ generateProxyCode([{ prefix: "/api", target: "file:///etc/passwd" }]);
191
+ // Error: [proxy] target must start with "https://" or "http://": "file:///etc/passwd"
192
+ ```
193
+
194
+ This catches config errors before the deploy ships.
195
+
196
+ ## Related
197
+
198
+ - [Chapter 9: Server & deployment — proxy routes](../09-server-and-deployment.md#proxy-routes)
199
+ - The implementation: `packages/server/src/proxy.ts`
200
+ - The parity test: `packages/server/test/proxy.test.ts`