@luckystack/api 0.1.0 → 0.1.2

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.
@@ -1,264 +1,264 @@
1
- # Generated Types
2
-
3
- > Last updated: 2026-05-20
4
-
5
- ## Overview
6
-
7
- LuckyStack's API layer is fully type-safe end-to-end. Every `src/{page}/_api/{name}_v{N}.ts` file is read by `@luckystack/devkit` via the TypeScript compiler API, its `ApiParams['data']` and `main` return types are extracted, and three artifacts are emitted: `apiTypes.generated.ts` (the static type map), `apiInputSchemas.generated.ts` (runtime Zod schemas), and `apiDocs.generated.json` (introspection JSON used by `@luckystack/docs-ui`). `@luckystack/api` reads the runtime map at request time and uses the input-type text to drive `validateInputByType`. The client-side `apiRequest` from `@luckystack/core` reads the static type map to infer payload + response shapes from the route-name + version literals you pass it.
8
-
9
- This means: any change to an API file's `ApiParams['data']` interface or `main` return type propagates to the client at the next regeneration. The hot-reload watcher (devkit) triggers regeneration on save. The strict typing policy in the repo root contract (`.claude/CLAUDE.md` rule 16) requires this inference chain to remain intact — `unknown` / `any` casts around `apiRequest` defeat the whole pipeline.
10
-
11
- This doc covers the generated type surface, the runtime validation flow that consumes it, and the per-route opt-out for cases where strict input typing is impossible (third-party webhooks).
12
-
13
- ## The two generated files
14
-
15
- | Artifact | Default path | Purpose | Consumer |
16
- | --- | --- | --- | --- |
17
- | `apiTypes.generated.ts` | `src/_sockets/apiTypes.generated.ts` | Static TS type map. Declares `ApiTypeMap`, `PagePath`, `ApiName`, `ApiVersion`, `ApiInput`, `ApiOutput`, `ApiStream`, `ApiMethod`, `FullApiPath`, plus `apiMethodMap` / `apiMetaMap` runtime tables. | Client autocomplete + compile-time `apiRequest` typing. Also module-augments `@luckystack/core`'s `ApiTypeMap` interface so framework code resolves the same shapes without deep-relative imports. |
18
- | `apiInputSchemas.generated.ts` | `src/_sockets/apiInputSchemas.generated.ts` | Runtime Zod schemas (one per route + version), keyed by `[pagePath][apiName][version]`. Falls back to `z.any()` with a TODO comment when the input type cannot be converted. | Test runner's input fuzzer. Available for consumers that want pre-handler Zod validation in custom transports. The default runtime validator (`validateInputByType`) uses the type text, not the Zod file. |
19
-
20
- Paths are resolved through `@luckystack/core`'s path registry (`getGeneratedSocketTypesPath()`, `getGeneratedApiSchemasPath()`). Override via `projectConfig.paths.*` if you ship the generated files somewhere else.
21
-
22
- ## Type Map structure
23
-
24
- `apiTypes.generated.ts` produces a nested object type, one entry per `<page>` → `<name>` → `<version>`:
25
-
26
- ```ts
27
- type _ProjectApiTypeMap = {
28
- 'settings': {
29
- 'updateUser': {
30
- 'v1': {
31
- input: { name: string };
32
- output: { status: 'success'; ok: true; httpStatus?: number } | { status: 'error'; errorCode: string; httpStatus?: number };
33
- stream: never;
34
- method: 'POST';
35
- rateLimit: 20;
36
- };
37
- };
38
- // ...
39
- };
40
- // ...
41
- };
42
-
43
- export interface ApiTypeMap extends _ProjectApiTypeMap {}
44
- ```
45
-
46
- The file also augments `@luckystack/core` so framework code can rely on the same map:
47
-
48
- ```ts
49
- declare module '@luckystack/core' {
50
- interface ApiTypeMap extends _ProjectApiTypeMap {}
51
- interface SyncTypeMap extends _ProjectSyncTypeMap {}
52
- }
53
- ```
54
-
55
- `@luckystack/core` ships a stub `interface ApiTypeMap {}` in `apiTypeStubs.ts` — the module-augmentation above replaces it with the project's concrete map on any import of the generated file.
56
-
57
- ## Helper types
58
-
59
- All exported from `apiTypes.generated.ts` (and re-exported transitively from `@luckystack/core` for framework use):
60
-
61
- | Type | Definition (paraphrased) | Use |
62
- | --- | --- | --- |
63
- | `PagePath` | `keyof ApiTypeMap` | Union of every page path with at least one route. |
64
- | `ApiName<P>` | `keyof ApiTypeMap[P]` | Union of every API name registered under page `P`. |
65
- | `ApiVersion<P, N>` | `keyof ApiTypeMap[P][N]` | Union of every version registered for route `P/N`. |
66
- | `ApiInput<P, N, V>` | `ApiTypeMap[P][N][V]['input']` | Payload shape (`data` parameter of `apiRequest`). |
67
- | `ApiOutput<P, N, V>` | `ApiTypeMap[P][N][V]['output']` | Discriminated success/error union returned by `apiRequest`. |
68
- | `ApiStream<P, N, V>` | `ApiTypeMap[P][N][V]['stream']` | Union of every `stream(...)` payload shape emitted by `main`. `never` when the route does not stream. |
69
- | `ApiMethod<P, N, V>` | `ApiTypeMap[P][N][V]['method']` | HTTP method literal (`'GET' \| 'POST' \| 'PUT' \| 'DELETE'`). |
70
- | `FullApiPath<P, N, V>` | `` `api/${P}/${N}/${V}` `` | Wire-format route string. Used internally — most code uses `name + version` literals. |
71
- | `ApiResponse<T>` | `{ status: 'success'; ...T } \| { status: 'error'; errorCode; errorParams?; httpStatus? }` | Return type for `main(...)`. |
72
- | `ApiStreamEmitter<T>` | `(payload?: T) => void \| Promise<void>` | Type of `stream` parameter passed into `main`. |
73
- | `Functions` | Project-emitted interface | The injected functions registry available to every route (`db`, `redis`, `notify`, ...). |
74
-
75
- `ApiInput` / `ApiOutput` / `ApiStream` are what `apiRequest` uses to type its arguments and return value:
76
-
77
- ```ts
78
- import { apiRequest } from '@luckystack/core/client';
79
-
80
- const result = await apiRequest({
81
- name: 'settings/updateUser', // ApiName<'settings'>
82
- version: 'v1', // ApiVersion<'settings', 'updateUser'>
83
- data: { name: 'Alice' }, // ApiInput<'settings', 'updateUser', 'v1'>
84
- });
85
-
86
- if (result.status === 'success') {
87
- // result is narrowed to ApiOutput<'settings', 'updateUser', 'v1'> success branch
88
- }
89
- ```
90
-
91
- The discriminated `status` field on `ApiOutput` lets TypeScript narrow success / error branches without `as` casts. Branch-specific literal properties (`submitted: true` vs `submitted: false`) stay discriminated in the union.
92
-
93
- ## Runtime input validation
94
-
95
- At request time, the API handler reads two fields from the runtime map:
96
-
97
- ```ts
98
- interface RuntimeApiEntry {
99
- // ...
100
- inputType?: string; // The text form of the `data` interface ("{ name: string }")
101
- inputTypeFilePath?: string; // The source file path, for cross-file type resolution
102
- // ...
103
- }
104
- ```
105
-
106
- `validateInputByType` (re-exported from `@luckystack/core`) is called with:
107
-
108
- ```ts
109
- const inputValidation = await validateInputByType({
110
- typeText: apiEntry.inputType,
111
- value: normalizedData,
112
- rootKey: 'data',
113
- filePath: apiEntry.inputTypeFilePath,
114
- });
115
- ```
116
-
117
- | Field | Type | Purpose |
118
- | --- | --- | --- |
119
- | `typeText` | `string \| undefined` | The TypeScript type text captured at generation time. May reference imports — `filePath` lets the resolver expand them. |
120
- | `value` | `unknown` | The wire-level payload. |
121
- | `rootKey` | `string` | Label used in error messages (`data` for API, `clientInput` for sync). |
122
- | `filePath` | `string \| undefined` | Source file path. The dev-mode resolver lazy-loads `@luckystack/devkit` to re-walk imports / re-exports. |
123
-
124
- **Return shape**: `{ status: 'success' } | { status: 'error'; message: string }`. On error the handler responds with `api.invalidInputType` + `400`, surfacing the validator's `message` as `errorParams[0].value`.
125
-
126
- `validateInputByType` lazy-loads `@luckystack/devkit`'s deep resolver in development so types defined across files (re-exported aliases, utility wrappers like `Partial`/`Pick`/`Omit`/`Record`) expand correctly. In production it relies on the resolver's last-known cache — regenerate before shipping.
127
-
128
- ## Missing `inputType`
129
-
130
- When a route's input cannot be extracted (the type erased to `any`, the generator hit an unresolvable symbol), `inputType` is empty or `'any'`. Strict-mode generation will throw at devkit time — but if you have it disabled or are running on a stale build, runtime validation is effectively a no-op.
131
-
132
- The dev-only `warnIfInputTypeMissing(resolvedName, inputType)` helper emits a one-shot warning per route:
133
-
134
- ```
135
- api: route settings/updateUser/v1 has no inputType — runtime input validation is disabled. Regenerate types or set the inputType on the handler.
136
- ```
137
-
138
- Gated by `projectConfig.dev.warnOnMissingInputType`. Each route warns once per process; the module-level `warnedMissingInputType: Set<string>` prevents log spam.
139
-
140
- ## `validation: 'relaxed'` / `{ input: 'skip' }`
141
-
142
- Some endpoints receive payloads that cannot reasonably be modeled in TypeScript — public webhooks (Stripe, Slack, GitHub), legacy clients in a migration window, or untyped third-party integrations. For these, the route file can opt out:
143
-
144
- ```ts
145
- // src/integrations/_api/stripeWebhook_v1.ts
146
- export const validation = 'relaxed' as const;
147
- // or
148
- export const validation = { input: 'skip' } as const;
149
- ```
150
-
151
- When set, the socket transport skips `validateInputByType` entirely:
152
-
153
- - `warnIfInputTypeMissing` is **not** called.
154
- - `preApiValidate` still fires (handlers can inspect raw `data`).
155
- - `validateInputByType` is **not** called.
156
- - `postApiValidate` fires with `{ status: 'success' }` so audit handlers see the skip.
157
-
158
- The HTTP transport does **not** honor the flag — relax-mode endpoints typically live on the socket transport (where webhook payloads still parse as `Record<string, unknown>`). If you need relax-mode on HTTP, structure the input as `Record<string, unknown>` and validate inside `main(...)`.
159
-
160
- ## Strict typing policy
161
-
162
- Repository rule (root `.claude/CLAUDE.md` §16): **NEVER** cast typed API/sync payloads to `unknown`, `any`, or `unsafe*` wrappers when calling `apiRequest`, `syncRequest`, or `upsertSyncEventCallback`.
163
-
164
- ```ts
165
- // Bad — defeats the entire generated-types pipeline
166
- const unsafeApi = async (name: string, version: string, data: unknown) =>
167
- apiRequest({ name: name as any, version: version as any, data: data as any });
168
-
169
- // Good — direct typed call with literal route + version
170
- const response = await apiRequest({
171
- name: 'organization/settings/sendInvite',
172
- version: 'v1',
173
- data: { email: 'a@b.com' },
174
- });
175
- ```
176
-
177
- The AI workflow allows temporary casts during generator-lag windows (after a save, before types refresh), but they must be local, minimal, and removed once regeneration completes.
178
-
179
- ## Regenerating types
180
-
181
- | Trigger | What happens |
182
- | --- | --- |
183
- | Save an `_api/` file in dev | `setupWatchers` (devkit) coalesces a hot-reload + type-map regeneration. The route is re-imported into `devApis`; the type map writes if its content changed. |
184
- | `npm run build` | The build script invokes `generateTypeMapFile()` once before bundling. |
185
- | Manual: `npm run ai:index` (or the project's equivalent) | Forces a full regeneration. |
186
- | `postinstall` (in this repo) | Regenerates `apiInputSchemas.generated.ts` so fresh clones never miss the runtime Zod schemas. |
187
-
188
- If the generator throws `[TypeMapGenerator] Generated type map has unresolved type identifiers: <symbols>`, an `ApiParams['data']` or `main` return type references a symbol the resolver could not expand. Common causes: a renamed file, a barrel re-export missing the type, an import cycle. Fix the typing source — do not add casts.
189
-
190
- ## API Reference
191
-
192
- ### `validateInputByType` (re-exported from `@luckystack/core`)
193
-
194
- **Signature**
195
-
196
- ```ts
197
- async function validateInputByType(params: {
198
- typeText: string | undefined;
199
- value: unknown;
200
- rootKey: string;
201
- filePath?: string;
202
- }): Promise<{ status: 'success' } | { status: 'error'; message: string }>;
203
- ```
204
-
205
- **Behavior**
206
-
207
- - Returns `{ status: 'success' }` when `typeText` is falsy (validation effectively disabled).
208
- - In dev, lazy-loads `@luckystack/devkit`'s `resolveRuntimeTypeText` to expand cross-file aliases.
209
- - Walks the type AST recursively. Records the first mismatch and returns a path-level message (`data.users[0].email: expected string, got number`).
210
- - Caches resolved types per `filePath`; cache is invalidated by devkit on hot reload.
211
-
212
- ### `warnIfInputTypeMissing` (internal to both handlers)
213
-
214
- **Signature**
215
-
216
- ```ts
217
- const warnIfInputTypeMissing = (resolvedName: string, inputType: string | undefined): void;
218
- ```
219
-
220
- **Behavior**
221
-
222
- - No-op when `projectConfig.dev.warnOnMissingInputType` is `false`.
223
- - No-op when `inputType` is non-empty and not `'any'`.
224
- - Otherwise, logs once per route via `getLogger().warn(...)` and adds the route to a module-level `Set` so subsequent requests stay quiet.
225
-
226
- ### `RuntimeApiEntry` (internal type)
227
-
228
- The shape `getRuntimeApiMaps()` returns for each route:
229
-
230
- ```ts
231
- interface RuntimeApiEntry {
232
- auth: AuthProps;
233
- main: (params: { data, user, functions, stream }) => Promise<RuntimeApiResponse>;
234
- inputType?: string;
235
- inputTypeFilePath?: string;
236
- rateLimit?: number | false;
237
- httpMethod?: HttpMethod;
238
- validation?: 'strict' | 'relaxed' | { input: 'skip' | 'strict' };
239
- }
240
- ```
241
-
242
- Devkit emits these. The handler reads them.
243
-
244
- ## Hooks dispatched
245
-
246
- This package does not own hooks specific to type generation. The runtime validation step fires `preApiValidate` / `postApiValidate` — see [`./api-request-lifecycle.md`](./api-request-lifecycle.md).
247
-
248
- ## Config keys
249
-
250
- | Key | Effect |
251
- | --- | --- |
252
- | `dev.warnOnMissingInputType` | Toggle the dev-only `warnIfInputTypeMissing` log. |
253
- | `paths.generatedSocketTypes` | Output path for `apiTypes.generated.ts`. |
254
- | `paths.generatedApiSchemas` | Output path for `apiInputSchemas.generated.ts`. |
255
- | `paths.generatedApiDocs` | Output path for `apiDocs.generated.json`. |
256
-
257
- ## Related
258
-
259
- - API request lifecycle: [`./api-request-lifecycle.md`](./api-request-lifecycle.md)
260
- - Routing conventions: [`./routing-conventions.md`](./routing-conventions.md)
261
- - Error handling: [`./error-handling.md`](./error-handling.md)
262
- - Architecture: [`/docs/ARCHITECTURE_ROUTING.md`](../../../docs/ARCHITECTURE_ROUTING.md) (see "Generated maps")
263
- - Devkit type-map generation: `@luckystack/devkit/docs/type-map-generation.md`
264
- - Root rule on casts: [`/.claude/CLAUDE.md`](../../../.claude/CLAUDE.md) §16
1
+ # Generated Types
2
+
3
+ > Last updated: 2026-05-20
4
+
5
+ ## Overview
6
+
7
+ LuckyStack's API layer is fully type-safe end-to-end. Every `src/{page}/_api/{name}_v{N}.ts` file is read by `@luckystack/devkit` via the TypeScript compiler API, its `ApiParams['data']` and `main` return types are extracted, and three artifacts are emitted: `apiTypes.generated.ts` (the static type map), `apiInputSchemas.generated.ts` (runtime Zod schemas), and `apiDocs.generated.json` (introspection JSON used by `@luckystack/docs-ui`). `@luckystack/api` reads the runtime map at request time and uses the input-type text to drive `validateInputByType`. The client-side `apiRequest` from `@luckystack/core` reads the static type map to infer payload + response shapes from the route-name + version literals you pass it.
8
+
9
+ This means: any change to an API file's `ApiParams['data']` interface or `main` return type propagates to the client at the next regeneration. The hot-reload watcher (devkit) triggers regeneration on save. The strict typing policy in the repo root contract (`.claude/CLAUDE.md` rule 16) requires this inference chain to remain intact — `unknown` / `any` casts around `apiRequest` defeat the whole pipeline.
10
+
11
+ This doc covers the generated type surface, the runtime validation flow that consumes it, and the per-route opt-out for cases where strict input typing is impossible (third-party webhooks).
12
+
13
+ ## The two generated files
14
+
15
+ | Artifact | Default path | Purpose | Consumer |
16
+ | --- | --- | --- | --- |
17
+ | `apiTypes.generated.ts` | `src/_sockets/apiTypes.generated.ts` | Static TS type map. Declares `ApiTypeMap`, `PagePath`, `ApiName`, `ApiVersion`, `ApiInput`, `ApiOutput`, `ApiStream`, `ApiMethod`, `FullApiPath`, plus `apiMethodMap` / `apiMetaMap` runtime tables. | Client autocomplete + compile-time `apiRequest` typing. Also module-augments `@luckystack/core`'s `ApiTypeMap` interface so framework code resolves the same shapes without deep-relative imports. |
18
+ | `apiInputSchemas.generated.ts` | `src/_sockets/apiInputSchemas.generated.ts` | Runtime Zod schemas (one per route + version), keyed by `[pagePath][apiName][version]`. Falls back to `z.any()` with a TODO comment when the input type cannot be converted. | Test runner's input fuzzer. Available for consumers that want pre-handler Zod validation in custom transports. The default runtime validator (`validateInputByType`) uses the type text, not the Zod file. |
19
+
20
+ Paths are resolved through `@luckystack/core`'s path registry (`getGeneratedSocketTypesPath()`, `getGeneratedApiSchemasPath()`). Override via `projectConfig.paths.*` if you ship the generated files somewhere else.
21
+
22
+ ## Type Map structure
23
+
24
+ `apiTypes.generated.ts` produces a nested object type, one entry per `<page>` → `<name>` → `<version>`:
25
+
26
+ ```ts
27
+ type _ProjectApiTypeMap = {
28
+ 'settings': {
29
+ 'updateUser': {
30
+ 'v1': {
31
+ input: { name: string };
32
+ output: { status: 'success'; ok: true; httpStatus?: number } | { status: 'error'; errorCode: string; httpStatus?: number };
33
+ stream: never;
34
+ method: 'POST';
35
+ rateLimit: 20;
36
+ };
37
+ };
38
+ // ...
39
+ };
40
+ // ...
41
+ };
42
+
43
+ export interface ApiTypeMap extends _ProjectApiTypeMap {}
44
+ ```
45
+
46
+ The file also augments `@luckystack/core` so framework code can rely on the same map:
47
+
48
+ ```ts
49
+ declare module '@luckystack/core' {
50
+ interface ApiTypeMap extends _ProjectApiTypeMap {}
51
+ interface SyncTypeMap extends _ProjectSyncTypeMap {}
52
+ }
53
+ ```
54
+
55
+ `@luckystack/core` ships a stub `interface ApiTypeMap {}` in `apiTypeStubs.ts` — the module-augmentation above replaces it with the project's concrete map on any import of the generated file.
56
+
57
+ ## Helper types
58
+
59
+ All exported from `apiTypes.generated.ts` (and re-exported transitively from `@luckystack/core` for framework use):
60
+
61
+ | Type | Definition (paraphrased) | Use |
62
+ | --- | --- | --- |
63
+ | `PagePath` | `keyof ApiTypeMap` | Union of every page path with at least one route. |
64
+ | `ApiName<P>` | `keyof ApiTypeMap[P]` | Union of every API name registered under page `P`. |
65
+ | `ApiVersion<P, N>` | `keyof ApiTypeMap[P][N]` | Union of every version registered for route `P/N`. |
66
+ | `ApiInput<P, N, V>` | `ApiTypeMap[P][N][V]['input']` | Payload shape (`data` parameter of `apiRequest`). |
67
+ | `ApiOutput<P, N, V>` | `ApiTypeMap[P][N][V]['output']` | Discriminated success/error union returned by `apiRequest`. |
68
+ | `ApiStream<P, N, V>` | `ApiTypeMap[P][N][V]['stream']` | Union of every `stream(...)` payload shape emitted by `main`. `never` when the route does not stream. |
69
+ | `ApiMethod<P, N, V>` | `ApiTypeMap[P][N][V]['method']` | HTTP method literal (`'GET' \| 'POST' \| 'PUT' \| 'DELETE'`). |
70
+ | `FullApiPath<P, N, V>` | `` `api/${P}/${N}/${V}` `` | Wire-format route string. Used internally — most code uses `name + version` literals. |
71
+ | `ApiResponse<T>` | `{ status: 'success'; ...T } \| { status: 'error'; errorCode; errorParams?; httpStatus? }` | Return type for `main(...)`. |
72
+ | `ApiStreamEmitter<T>` | `(payload?: T) => void \| Promise<void>` | Type of `stream` parameter passed into `main`. |
73
+ | `Functions` | Project-emitted interface | The injected functions registry available to every route (`db`, `redis`, `notify`, ...). |
74
+
75
+ `ApiInput` / `ApiOutput` / `ApiStream` are what `apiRequest` uses to type its arguments and return value:
76
+
77
+ ```ts
78
+ import { apiRequest } from '@luckystack/core/client';
79
+
80
+ const result = await apiRequest({
81
+ name: 'settings/updateUser', // ApiName<'settings'>
82
+ version: 'v1', // ApiVersion<'settings', 'updateUser'>
83
+ data: { name: 'Alice' }, // ApiInput<'settings', 'updateUser', 'v1'>
84
+ });
85
+
86
+ if (result.status === 'success') {
87
+ // result is narrowed to ApiOutput<'settings', 'updateUser', 'v1'> success branch
88
+ }
89
+ ```
90
+
91
+ The discriminated `status` field on `ApiOutput` lets TypeScript narrow success / error branches without `as` casts. Branch-specific literal properties (`submitted: true` vs `submitted: false`) stay discriminated in the union.
92
+
93
+ ## Runtime input validation
94
+
95
+ At request time, the API handler reads two fields from the runtime map:
96
+
97
+ ```ts
98
+ interface RuntimeApiEntry {
99
+ // ...
100
+ inputType?: string; // The text form of the `data` interface ("{ name: string }")
101
+ inputTypeFilePath?: string; // The source file path, for cross-file type resolution
102
+ // ...
103
+ }
104
+ ```
105
+
106
+ `validateInputByType` (re-exported from `@luckystack/core`) is called with:
107
+
108
+ ```ts
109
+ const inputValidation = await validateInputByType({
110
+ typeText: apiEntry.inputType,
111
+ value: normalizedData,
112
+ rootKey: 'data',
113
+ filePath: apiEntry.inputTypeFilePath,
114
+ });
115
+ ```
116
+
117
+ | Field | Type | Purpose |
118
+ | --- | --- | --- |
119
+ | `typeText` | `string \| undefined` | The TypeScript type text captured at generation time. May reference imports — `filePath` lets the resolver expand them. |
120
+ | `value` | `unknown` | The wire-level payload. |
121
+ | `rootKey` | `string` | Label used in error messages (`data` for API, `clientInput` for sync). |
122
+ | `filePath` | `string \| undefined` | Source file path. The dev-mode resolver lazy-loads `@luckystack/devkit` to re-walk imports / re-exports. |
123
+
124
+ **Return shape**: `{ status: 'success' } | { status: 'error'; message: string }`. On error the handler responds with `api.invalidInputType` + `400`, surfacing the validator's `message` as `errorParams[0].value`.
125
+
126
+ `validateInputByType` lazy-loads `@luckystack/devkit`'s deep resolver in development so types defined across files (re-exported aliases, utility wrappers like `Partial`/`Pick`/`Omit`/`Record`) expand correctly. In production it relies on the resolver's last-known cache — regenerate before shipping.
127
+
128
+ ## Missing `inputType`
129
+
130
+ When a route's input cannot be extracted (the type erased to `any`, the generator hit an unresolvable symbol), `inputType` is empty or `'any'`. Strict-mode generation will throw at devkit time — but if you have it disabled or are running on a stale build, runtime validation is effectively a no-op.
131
+
132
+ The dev-only `warnIfInputTypeMissing(resolvedName, inputType)` helper emits a one-shot warning per route:
133
+
134
+ ```
135
+ api: route settings/updateUser/v1 has no inputType — runtime input validation is disabled. Regenerate types or set the inputType on the handler.
136
+ ```
137
+
138
+ Gated by `projectConfig.dev.warnOnMissingInputType`. Each route warns once per process; the module-level `warnedMissingInputType: Set<string>` prevents log spam.
139
+
140
+ ## `validation: 'relaxed'` / `{ input: 'skip' }`
141
+
142
+ Some endpoints receive payloads that cannot reasonably be modeled in TypeScript — public webhooks (Stripe, Slack, GitHub), legacy clients in a migration window, or untyped third-party integrations. For these, the route file can opt out:
143
+
144
+ ```ts
145
+ // src/integrations/_api/stripeWebhook_v1.ts
146
+ export const validation = 'relaxed' as const;
147
+ // or
148
+ export const validation = { input: 'skip' } as const;
149
+ ```
150
+
151
+ When set, the socket transport skips `validateInputByType` entirely:
152
+
153
+ - `warnIfInputTypeMissing` is **not** called.
154
+ - `preApiValidate` still fires (handlers can inspect raw `data`).
155
+ - `validateInputByType` is **not** called.
156
+ - `postApiValidate` fires with `{ status: 'success' }` so audit handlers see the skip.
157
+
158
+ The HTTP transport does **not** honor the flag — relax-mode endpoints typically live on the socket transport (where webhook payloads still parse as `Record<string, unknown>`). If you need relax-mode on HTTP, structure the input as `Record<string, unknown>` and validate inside `main(...)`.
159
+
160
+ ## Strict typing policy
161
+
162
+ Repository rule (root `.claude/CLAUDE.md` §16): **NEVER** cast typed API/sync payloads to `unknown`, `any`, or `unsafe*` wrappers when calling `apiRequest`, `syncRequest`, or `upsertSyncEventCallback`.
163
+
164
+ ```ts
165
+ // Bad — defeats the entire generated-types pipeline
166
+ const unsafeApi = async (name: string, version: string, data: unknown) =>
167
+ apiRequest({ name: name as any, version: version as any, data: data as any });
168
+
169
+ // Good — direct typed call with literal route + version
170
+ const response = await apiRequest({
171
+ name: 'organization/settings/sendInvite',
172
+ version: 'v1',
173
+ data: { email: 'a@b.com' },
174
+ });
175
+ ```
176
+
177
+ The AI workflow allows temporary casts during generator-lag windows (after a save, before types refresh), but they must be local, minimal, and removed once regeneration completes.
178
+
179
+ ## Regenerating types
180
+
181
+ | Trigger | What happens |
182
+ | --- | --- |
183
+ | Save an `_api/` file in dev | `setupWatchers` (devkit) coalesces a hot-reload + type-map regeneration. The route is re-imported into `devApis`; the type map writes if its content changed. |
184
+ | `npm run build` | The build script invokes `generateTypeMapFile()` once before bundling. |
185
+ | Manual: `npm run ai:index` (or the project's equivalent) | Forces a full regeneration. |
186
+ | `postinstall` (in this repo) | Regenerates `apiInputSchemas.generated.ts` so fresh clones never miss the runtime Zod schemas. |
187
+
188
+ If the generator throws `[TypeMapGenerator] Generated type map has unresolved type identifiers: <symbols>`, an `ApiParams['data']` or `main` return type references a symbol the resolver could not expand. Common causes: a renamed file, a barrel re-export missing the type, an import cycle. Fix the typing source — do not add casts.
189
+
190
+ ## API Reference
191
+
192
+ ### `validateInputByType` (re-exported from `@luckystack/core`)
193
+
194
+ **Signature**
195
+
196
+ ```ts
197
+ async function validateInputByType(params: {
198
+ typeText: string | undefined;
199
+ value: unknown;
200
+ rootKey: string;
201
+ filePath?: string;
202
+ }): Promise<{ status: 'success' } | { status: 'error'; message: string }>;
203
+ ```
204
+
205
+ **Behavior**
206
+
207
+ - Returns `{ status: 'success' }` when `typeText` is falsy (validation effectively disabled).
208
+ - In dev, lazy-loads `@luckystack/devkit`'s `resolveRuntimeTypeText` to expand cross-file aliases.
209
+ - Walks the type AST recursively. Records the first mismatch and returns a path-level message (`data.users[0].email: expected string, got number`).
210
+ - Caches resolved types per `filePath`; cache is invalidated by devkit on hot reload.
211
+
212
+ ### `warnIfInputTypeMissing` (internal to both handlers)
213
+
214
+ **Signature**
215
+
216
+ ```ts
217
+ const warnIfInputTypeMissing = (resolvedName: string, inputType: string | undefined): void;
218
+ ```
219
+
220
+ **Behavior**
221
+
222
+ - No-op when `projectConfig.dev.warnOnMissingInputType` is `false`.
223
+ - No-op when `inputType` is non-empty and not `'any'`.
224
+ - Otherwise, logs once per route via `getLogger().warn(...)` and adds the route to a module-level `Set` so subsequent requests stay quiet.
225
+
226
+ ### `RuntimeApiEntry` (internal type)
227
+
228
+ The shape `getRuntimeApiMaps()` returns for each route:
229
+
230
+ ```ts
231
+ interface RuntimeApiEntry {
232
+ auth: AuthProps;
233
+ main: (params: { data, user, functions, stream }) => Promise<RuntimeApiResponse>;
234
+ inputType?: string;
235
+ inputTypeFilePath?: string;
236
+ rateLimit?: number | false;
237
+ httpMethod?: HttpMethod;
238
+ validation?: 'strict' | 'relaxed' | { input: 'skip' | 'strict' };
239
+ }
240
+ ```
241
+
242
+ Devkit emits these. The handler reads them.
243
+
244
+ ## Hooks dispatched
245
+
246
+ This package does not own hooks specific to type generation. The runtime validation step fires `preApiValidate` / `postApiValidate` — see [`./api-request-lifecycle.md`](./api-request-lifecycle.md).
247
+
248
+ ## Config keys
249
+
250
+ | Key | Effect |
251
+ | --- | --- |
252
+ | `dev.warnOnMissingInputType` | Toggle the dev-only `warnIfInputTypeMissing` log. |
253
+ | `paths.generatedSocketTypes` | Output path for `apiTypes.generated.ts`. |
254
+ | `paths.generatedApiSchemas` | Output path for `apiInputSchemas.generated.ts`. |
255
+ | `paths.generatedApiDocs` | Output path for `apiDocs.generated.json`. |
256
+
257
+ ## Related
258
+
259
+ - API request lifecycle: [`./api-request-lifecycle.md`](./api-request-lifecycle.md)
260
+ - Routing conventions: [`./routing-conventions.md`](./routing-conventions.md)
261
+ - Error handling: [`./error-handling.md`](./error-handling.md)
262
+ - Architecture: [`/docs/ARCHITECTURE_ROUTING.md`](../../../docs/ARCHITECTURE_ROUTING.md) (see "Generated maps")
263
+ - Devkit type-map generation: `@luckystack/devkit/docs/type-map-generation.md`
264
+ - Root rule on casts: [`/.claude/CLAUDE.md`](../../../.claude/CLAUDE.md) §16