@luckystack/api 0.6.7 → 0.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.
@@ -1,164 +1,164 @@
1
- # Routing Conventions
2
-
3
- > Last updated: 2026-05-20
4
-
5
- ## Overview
6
-
7
- `@luckystack/api` is transport-agnostic — it does not own file-based routing. Discovery of `_api/<name>_v<N>.ts` files lives in `@luckystack/devkit`, which walks `src/`, parses each file's exports, and emits two artifacts the API handler reads at runtime: the route map (`getRuntimeApiMaps()`) and the generated type map (`apiTypes.generated.ts`). This document describes the route-name conventions both handlers enforce, the per-route exports the runtime map carries, and the HTTP-method inference contract the HTTP transport applies.
8
-
9
- If a route name on the wire does not match the conventions below, both `handleApiRequest` and `handleHttpApiRequest` reject with `routing.invalidServiceRouteName` before any other side effect. If a name parses but does not match a registered route, both return `api.notFound`.
10
-
11
- ## Wire-level route names
12
-
13
- Both transports accept the same shape:
14
-
15
- ```
16
- api/<page>/<name>/v<N>
17
- ```
18
-
19
- | Segment | Constraints |
20
- | --- | --- |
21
- | `api/` | Literal prefix. The HTTP transport accepts `name` with or without it and normalizes; the socket transport requires the prefix to be present in `msg.name`. |
22
- | `<page>` | Slash-separated page path. May be deeply nested (`organization/settings`). Top-level system APIs use `system` as the page (e.g. `api/system/session/v1`). |
23
- | `<name>` | Endpoint name. Free-form identifier; influences HTTP method inference (see below). |
24
- | `v<N>` | Version token (`v1`, `v2`, ...). Required. Files that do not end in `_v<N>.ts` are skipped by the discovery walker. |
25
-
26
- `parseTransportRouteName({ value, prefix: 'api' })` (exported from `@luckystack/core`) is the single source of truth. The returned `normalizedFullName` is what the handler uses for runtime-map lookup, rate-limit keying, and hook payloads.
27
-
28
- ## File-system convention
29
-
30
- Discovery is owned by `@luckystack/devkit`. Files are picked up when they live under `src/` and match `**/_api/<name>_v<N>.ts` (or any subdirectory, including `_api/<sub>/<name>_v<N>.ts` for nested routes). The marker segment `_api` is configurable via `registerRoutingRules` but defaults to `_api`.
31
-
32
- | Path | Resolved route |
33
- | --- | --- |
34
- | `src/_api/session_v1.ts` | `api/system/session/v1` (top-level APIs are mapped under the `system` service) |
35
- | `src/settings/_api/updateUser_v1.ts` | `api/settings/updateUser/v1` |
36
- | `src/organization/_api/settings/sendInvite_v1.ts` | `api/organization/settings/sendInvite/v1` |
37
- | `src/playground/_api/streamCounter_v1.ts` | `api/playground/streamCounter/v1` |
38
-
39
- Files that do not match the `_v<N>` suffix are not discovered. The version digit must be a positive integer. Versions are sorted numerically when emitted into the type map.
40
-
41
- Folders prefixed with `_` are private — they may sit alongside `_api/`, `_sync/`, `_components/`, etc. without being treated as page routes by the frontend router.
42
-
43
- ## Per-route exports
44
-
45
- Each route file declares its behavior via exports. The discovery walker captures these via the TypeScript compiler API and emits them into the runtime map.
46
-
47
- ```ts
48
- // src/settings/_api/updateUser_v1.ts
49
- import type { AuthProps } from '@luckystack/core';
50
- import type { ApiResponse } from 'src/_sockets/apiTypes.generated';
51
-
52
- export const rateLimit: number | false = 20;
53
- export const httpMethod = 'POST' as const;
54
- export const auth: AuthProps = { login: true };
55
-
56
- export interface ApiParams {
57
- data: { name: string };
58
- user: SessionLayout;
59
- functions: Functions;
60
- }
61
-
62
- export const main = async ({ data, user, functions }: ApiParams): Promise<ApiResponse> => {
63
- // ...
64
- return { status: 'success', ok: true };
65
- };
66
- ```
67
-
68
- | Export | Type | Default | Effect |
69
- | --- | --- | --- | --- |
70
- | `main` | `(params) => Promise<ApiResponse>` | required | Handler invoked by `executeApiHandler` inside the `tryCatch` + Sentry span. |
71
- | `auth` | `AuthProps` | required | Auth gate (see [`./auth-flow.md`](./auth-flow.md)). |
72
- | `rateLimit` | `number \| false` | `rateLimiting.defaultApiLimit` from config | Per-route requests-per-window cap. `false` disables the per-route bucket; the global IP bucket still applies. See [`./rate-limiting.md`](./rate-limiting.md). |
73
- | `httpMethod` | `'GET' \| 'POST' \| 'PUT' \| 'DELETE'` | inferred from name | Overrides `inferHttpMethod` for the HTTP transport. Socket transport ignores this field. |
74
- | `ApiParams` interface | `{ data, user, functions, stream? }` | required | TypeScript-only. The `data` field's type is what `validateInputByType` checks at runtime via the generated Zod schema. |
75
- | `validation` | `'strict' \| 'relaxed' \| { input: 'skip' \| 'strict' }` | `'strict'` | Socket-only escape hatch for public webhooks where the third-party payload is not modelable in TypeScript. See "Input validation" below. |
76
-
77
- `ApiResponse` is exact: either `{ status: 'success'; httpStatus?: number; ...payload }` or `{ status: 'error'; errorCode: string; errorParams?: [...]; httpStatus?: number }`. Returning anything else is a runtime error (`api.invalidResponseStatus`).
78
-
79
- ## HTTP-method inference
80
-
81
- Used by `handleHttpApiRequest` when the route file does not export `httpMethod`. Implemented in `inferHttpMethod` (from `@luckystack/core/src/httpApiUtils.ts`):
82
-
83
- | Name prefix (case-insensitive, last `/`-segment) | Inferred method |
84
- | --- | --- |
85
- | `get*`, `fetch*`, `list*` | `GET` |
86
- | `delete*`, `remove*` | `DELETE` |
87
- | `update*`, `edit*`, `patch*` | `PUT` |
88
- | Everything else (`create*`, `send*`, `sync*`, bare verbs) | `POST` |
89
-
90
- The inference reads the last segment before the version token. `api/admin/users/getList/v1` infers `GET`; `api/admin/getUsers/v2` also infers `GET`.
91
-
92
- The socket transport does not validate HTTP method — only the HTTP transport does. Mismatches return `api.methodNotAllowed` + `405` with `errorParams: [{ key: 'method', value: expectedMethod }]`.
93
-
94
- ## Input validation
95
-
96
- The discovery walker reads the `data` field of the `ApiParams` interface (or whatever the `main` parameter's `data` member is named) and:
97
-
98
- 1. Emits the **text form** of the type into the runtime map (`apiEntry.inputType`).
99
- 2. Emits a **Zod schema** for the same type into `apiInputSchemas.generated.ts` for runtime checking + test-runner fuzz.
100
- 3. Emits the **structural type** into `apiTypes.generated.ts` so `apiRequest({ name, version, data })` on the client gets full autocomplete + a compile-time error if `data` does not match.
101
-
102
- At request time, `validateInputByType({ typeText, value, rootKey: 'data', filePath })` matches the payload against the stored type. The validator lazy-loads `@luckystack/devkit`'s deep resolver in dev so imported / re-exported aliases expand correctly; in production it falls back to a static resolver.
103
-
104
- The per-route `validation` flag turns the validator off:
105
-
106
- ```ts
107
- // Public webhook receiving a payload from Stripe / Slack / GitHub
108
- export const validation = 'relaxed' as const;
109
-
110
- // Or, equivalent:
111
- export const validation = { input: 'skip' } as const;
112
- ```
113
-
114
- Use sparingly. The skip suppresses both `api.invalidInputType` rejections and the dev-only "no inputType" warning. `postApiValidate` still fires with `{ status: 'success' }` so audit handlers see the skip.
115
-
116
- ## Wire-level route examples
117
-
118
- ```
119
- api/system/session/v1 ← src/_api/session_v1.ts
120
- api/system/logout/v1 ← src/_api/logout_v1.ts (built-in socket shortcut)
121
- api/settings/updateUser/v1 ← src/settings/_api/updateUser_v1.ts
122
- api/organization-settings/sendInvite/v1
123
- ← src/organization-settings/_api/sendInvite_v1.ts
124
- api/admin/users/listAll/v3 ← src/admin/_api/users/listAll_v3.ts
125
- ```
126
-
127
- ## Reserved routes
128
-
129
- | Route | Owner | Notes |
130
- | --- | --- | --- |
131
- | `api/system/logout/v1` | `handleApiRequest` (socket only) | Short-circuited before runtime map lookup. Calls `logout(...)` from `@luckystack/login`. HTTP requests for this route go through the normal pipeline. |
132
- | `api/system/session/v1` | Project | Conventional location for the session-fetch endpoint. Not framework-reserved. |
133
-
134
- Consumer routes whose final segment happens to be `logout` (`admin/logout/v1`) are NOT hijacked — the short-circuit matches the full normalized route `system/logout`, not just the trailing segment.
135
-
136
- ## Hooks dispatched
137
-
138
- Discovery / route-naming hooks live in `@luckystack/devkit`. The runtime API handler does not emit routing-level hooks beyond the standard `pre/postApiValidate` / `pre/postApiExecute` / `pre/postApiRespond` / `transformApiResponse` / `rateLimitExceeded` set. See [`./api-request-lifecycle.md`](./api-request-lifecycle.md).
139
-
140
- ## Config keys
141
-
142
- | Key | Effect |
143
- | --- | --- |
144
- | `paths.srcDir` | Root directory for `_api/` discovery (devkit). |
145
- | `paths.generatedSocketTypes` | Output path for `apiTypes.generated.ts` (devkit). |
146
- | `paths.generatedApiSchemas` | Output path for `apiInputSchemas.generated.ts` (devkit). |
147
- | `paths.generatedApiDocs` | Output path for `apiDocs.generated.json` (devkit). |
148
-
149
- No routing-specific keys are read at request time by `@luckystack/api`.
150
-
151
- ## Edge cases
152
-
153
- - **Two files normalize to the same route key.** `assertNoDuplicateNormalizedRouteKeys` (devkit) throws at generation time. The runtime map will reflect whichever file was loaded last; treat the throw as a build-time guard.
154
- - **File saved without a version suffix.** Discovery skips the file. The route never registers; `apiRequest` calls fail with `api.notFound` at runtime.
155
- - **`auth` export missing.** TypeScript will not compile; if compilation is bypassed, the runtime map's `apiEntry.auth` is `undefined` and the auth gate treats it as `{ login: false }`. Do not rely on this — declare `auth` explicitly.
156
- - **Non-`_api/` folders matching the regex.** Discovery only descends through folders; the marker segment `_api` is required, and folders without the underscore prefix are ignored.
157
-
158
- ## Related
159
-
160
- - API request lifecycle: [`./api-request-lifecycle.md`](./api-request-lifecycle.md)
161
- - Generated types: [`./generated-types.md`](./generated-types.md)
162
- - Architecture: [`/docs/ARCHITECTURE_ROUTING.md`](../../../docs/ARCHITECTURE_ROUTING.md)
163
- - Devkit type-map: `@luckystack/devkit/docs/type-map-generation.md`
164
- - Devkit loader pipeline: `@luckystack/devkit/docs/loader-pipeline.md`
1
+ # Routing Conventions
2
+
3
+ > Last updated: 2026-05-20
4
+
5
+ ## Overview
6
+
7
+ `@luckystack/api` is transport-agnostic — it does not own file-based routing. Discovery of `_api/<name>_v<N>.ts` files lives in `@luckystack/devkit`, which walks `src/`, parses each file's exports, and emits two artifacts the API handler reads at runtime: the route map (`getRuntimeApiMaps()`) and the generated type map (`apiTypes.generated.ts`). This document describes the route-name conventions both handlers enforce, the per-route exports the runtime map carries, and the HTTP-method inference contract the HTTP transport applies.
8
+
9
+ If a route name on the wire does not match the conventions below, both `handleApiRequest` and `handleHttpApiRequest` reject with `routing.invalidServiceRouteName` before any other side effect. If a name parses but does not match a registered route, both return `api.notFound`.
10
+
11
+ ## Wire-level route names
12
+
13
+ Both transports accept the same shape:
14
+
15
+ ```
16
+ api/<page>/<name>/v<N>
17
+ ```
18
+
19
+ | Segment | Constraints |
20
+ | --- | --- |
21
+ | `api/` | Literal prefix. The HTTP transport accepts `name` with or without it and normalizes; the socket transport requires the prefix to be present in `msg.name`. |
22
+ | `<page>` | Slash-separated page path. May be deeply nested (`organization/settings`). Top-level system APIs use `system` as the page (e.g. `api/system/session/v1`). |
23
+ | `<name>` | Endpoint name. Free-form identifier; influences HTTP method inference (see below). |
24
+ | `v<N>` | Version token (`v1`, `v2`, ...). Required. Files that do not end in `_v<N>.ts` are skipped by the discovery walker. |
25
+
26
+ `parseTransportRouteName({ value, prefix: 'api' })` (exported from `@luckystack/core`) is the single source of truth. The returned `normalizedFullName` is what the handler uses for runtime-map lookup, rate-limit keying, and hook payloads.
27
+
28
+ ## File-system convention
29
+
30
+ Discovery is owned by `@luckystack/devkit`. Files are picked up when they live under `src/` and match `**/_api/<name>_v<N>.ts` (or any subdirectory, including `_api/<sub>/<name>_v<N>.ts` for nested routes). The marker segment `_api` is configurable via `registerRoutingRules` but defaults to `_api`.
31
+
32
+ | Path | Resolved route |
33
+ | --- | --- |
34
+ | `src/_api/session_v1.ts` | `api/system/session/v1` (top-level APIs are mapped under the `system` service) |
35
+ | `src/settings/_api/updateUser_v1.ts` | `api/settings/updateUser/v1` |
36
+ | `src/organization/_api/settings/sendInvite_v1.ts` | `api/organization/settings/sendInvite/v1` |
37
+ | `src/playground/_api/streamCounter_v1.ts` | `api/playground/streamCounter/v1` |
38
+
39
+ Files that do not match the `_v<N>` suffix are not discovered. The version digit must be a positive integer. Versions are sorted numerically when emitted into the type map.
40
+
41
+ Folders prefixed with `_` are private — they may sit alongside `_api/`, `_sync/`, `_components/`, etc. without being treated as page routes by the frontend router.
42
+
43
+ ## Per-route exports
44
+
45
+ Each route file declares its behavior via exports. The discovery walker captures these via the TypeScript compiler API and emits them into the runtime map.
46
+
47
+ ```ts
48
+ // src/settings/_api/updateUser_v1.ts
49
+ import type { AuthProps } from '@luckystack/core';
50
+ import type { ApiResponse } from 'src/_sockets/apiTypes.generated';
51
+
52
+ export const rateLimit: number | false = 20;
53
+ export const httpMethod = 'POST' as const;
54
+ export const auth: AuthProps = { login: true };
55
+
56
+ export interface ApiParams {
57
+ data: { name: string };
58
+ user: SessionLayout;
59
+ functions: Functions;
60
+ }
61
+
62
+ export const main = async ({ data, user, functions }: ApiParams): Promise<ApiResponse> => {
63
+ // ...
64
+ return { status: 'success', ok: true };
65
+ };
66
+ ```
67
+
68
+ | Export | Type | Default | Effect |
69
+ | --- | --- | --- | --- |
70
+ | `main` | `(params) => Promise<ApiResponse>` | required | Handler invoked by `executeApiHandler` inside the `tryCatch` + Sentry span. |
71
+ | `auth` | `AuthProps` | required | Auth gate (see [`./auth-flow.md`](./auth-flow.md)). |
72
+ | `rateLimit` | `number \| false` | `rateLimiting.defaultApiLimit` from config | Per-route requests-per-window cap. `false` disables the per-route bucket; the global IP bucket still applies. See [`./rate-limiting.md`](./rate-limiting.md). |
73
+ | `httpMethod` | `'GET' \| 'POST' \| 'PUT' \| 'DELETE'` | inferred from name | Overrides `inferHttpMethod` for the HTTP transport. Socket transport ignores this field. |
74
+ | `ApiParams` interface | `{ data, user, functions, stream? }` | required | TypeScript-only. The `data` field's type is what `validateInputByType` checks at runtime via the generated Zod schema. |
75
+ | `validation` | `'strict' \| 'relaxed' \| { input: 'skip' \| 'strict' }` | `'strict'` | Socket-only escape hatch for public webhooks where the third-party payload is not modelable in TypeScript. See "Input validation" below. |
76
+
77
+ `ApiResponse` is exact: either `{ status: 'success'; httpStatus?: number; ...payload }` or `{ status: 'error'; errorCode: string; errorParams?: [...]; httpStatus?: number }`. Returning anything else is a runtime error (`api.invalidResponseStatus`).
78
+
79
+ ## HTTP-method inference
80
+
81
+ Used by `handleHttpApiRequest` when the route file does not export `httpMethod`. Implemented in `inferHttpMethod` (from `@luckystack/core/src/httpApiUtils.ts`):
82
+
83
+ | Name prefix (case-insensitive, last `/`-segment) | Inferred method |
84
+ | --- | --- |
85
+ | `get*`, `fetch*`, `list*` | `GET` |
86
+ | `delete*`, `remove*` | `DELETE` |
87
+ | `update*`, `edit*`, `patch*` | `PUT` |
88
+ | Everything else (`create*`, `send*`, `sync*`, bare verbs) | `POST` |
89
+
90
+ The inference reads the last segment before the version token. `api/admin/users/getList/v1` infers `GET`; `api/admin/getUsers/v2` also infers `GET`.
91
+
92
+ The socket transport does not validate HTTP method — only the HTTP transport does. Mismatches return `api.methodNotAllowed` + `405` with `errorParams: [{ key: 'method', value: expectedMethod }]`.
93
+
94
+ ## Input validation
95
+
96
+ The discovery walker reads the `data` field of the `ApiParams` interface (or whatever the `main` parameter's `data` member is named) and:
97
+
98
+ 1. Emits the **text form** of the type into the runtime map (`apiEntry.inputType`).
99
+ 2. Emits a **Zod schema** for the same type into `apiInputSchemas.generated.ts` for runtime checking + test-runner fuzz.
100
+ 3. Emits the **structural type** into `apiTypes.generated.ts` so `apiRequest({ name, version, data })` on the client gets full autocomplete + a compile-time error if `data` does not match.
101
+
102
+ At request time, `validateInputByType({ typeText, value, rootKey: 'data', filePath })` matches the payload against the stored type. The validator lazy-loads `@luckystack/devkit`'s deep resolver in dev so imported / re-exported aliases expand correctly; in production it falls back to a static resolver.
103
+
104
+ The per-route `validation` flag turns the validator off:
105
+
106
+ ```ts
107
+ // Public webhook receiving a payload from Stripe / Slack / GitHub
108
+ export const validation = 'relaxed' as const;
109
+
110
+ // Or, equivalent:
111
+ export const validation = { input: 'skip' } as const;
112
+ ```
113
+
114
+ Use sparingly. The skip suppresses both `api.invalidInputType` rejections and the dev-only "no inputType" warning. `postApiValidate` still fires with `{ status: 'success' }` so audit handlers see the skip.
115
+
116
+ ## Wire-level route examples
117
+
118
+ ```
119
+ api/system/session/v1 ← src/_api/session_v1.ts
120
+ api/system/logout/v1 ← src/_api/logout_v1.ts (built-in socket shortcut)
121
+ api/settings/updateUser/v1 ← src/settings/_api/updateUser_v1.ts
122
+ api/organization-settings/sendInvite/v1
123
+ ← src/organization-settings/_api/sendInvite_v1.ts
124
+ api/admin/users/listAll/v3 ← src/admin/_api/users/listAll_v3.ts
125
+ ```
126
+
127
+ ## Reserved routes
128
+
129
+ | Route | Owner | Notes |
130
+ | --- | --- | --- |
131
+ | `api/system/logout/v1` | `handleApiRequest` (socket only) | Short-circuited before runtime map lookup. Calls `logout(...)` from `@luckystack/login`. HTTP requests for this route go through the normal pipeline. |
132
+ | `api/system/session/v1` | Project | Conventional location for the session-fetch endpoint. Not framework-reserved. |
133
+
134
+ Consumer routes whose final segment happens to be `logout` (`admin/logout/v1`) are NOT hijacked — the short-circuit matches the full normalized route `system/logout`, not just the trailing segment.
135
+
136
+ ## Hooks dispatched
137
+
138
+ Discovery / route-naming hooks live in `@luckystack/devkit`. The runtime API handler does not emit routing-level hooks beyond the standard `pre/postApiValidate` / `pre/postApiExecute` / `pre/postApiRespond` / `transformApiResponse` / `rateLimitExceeded` set. See [`./api-request-lifecycle.md`](./api-request-lifecycle.md).
139
+
140
+ ## Config keys
141
+
142
+ | Key | Effect |
143
+ | --- | --- |
144
+ | `paths.srcDir` | Root directory for `_api/` discovery (devkit). |
145
+ | `paths.generatedSocketTypes` | Output path for `apiTypes.generated.ts` (devkit). |
146
+ | `paths.generatedApiSchemas` | Output path for `apiInputSchemas.generated.ts` (devkit). |
147
+ | `paths.generatedApiDocs` | Output path for `apiDocs.generated.json` (devkit). |
148
+
149
+ No routing-specific keys are read at request time by `@luckystack/api`.
150
+
151
+ ## Edge cases
152
+
153
+ - **Two files normalize to the same route key.** `assertNoDuplicateNormalizedRouteKeys` (devkit) throws at generation time. The runtime map will reflect whichever file was loaded last; treat the throw as a build-time guard.
154
+ - **File saved without a version suffix.** Discovery skips the file. The route never registers; `apiRequest` calls fail with `api.notFound` at runtime.
155
+ - **`auth` export missing.** TypeScript will not compile; if compilation is bypassed, the runtime map's `apiEntry.auth` is `undefined` and the auth gate treats it as `{ login: false }`. Do not rely on this — declare `auth` explicitly.
156
+ - **Non-`_api/` folders matching the regex.** Discovery only descends through folders; the marker segment `_api` is required, and folders without the underscore prefix are ignored.
157
+
158
+ ## Related
159
+
160
+ - API request lifecycle: [`./api-request-lifecycle.md`](./api-request-lifecycle.md)
161
+ - Generated types: [`./generated-types.md`](./generated-types.md)
162
+ - Architecture: [`/docs/ARCHITECTURE_ROUTING.md`](../../../docs/ARCHITECTURE_ROUTING.md)
163
+ - Devkit type-map: `@luckystack/devkit/docs/type-map-generation.md`
164
+ - Devkit loader pipeline: `@luckystack/devkit/docs/loader-pipeline.md`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luckystack/api",
3
- "version": "0.6.7",
3
+ "version": "0.7.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -56,8 +56,8 @@
56
56
  "test": "vitest run"
57
57
  },
58
58
  "dependencies": {
59
- "@luckystack/core": "^0.6.7",
60
- "@luckystack/error-tracking": "^0.6.7"
59
+ "@luckystack/core": "^0.7.0",
60
+ "@luckystack/error-tracking": "^0.7.0"
61
61
  },
62
62
  "peerDependencies": {
63
63
  "@prisma/client": "^6.19.0",