ts-procedures 5.2.0 → 5.4.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 (49) hide show
  1. package/README.md +150 -0
  2. package/agent_config/bin/postinstall.mjs +105 -0
  3. package/agent_config/bin/setup.mjs +286 -0
  4. package/agent_config/claude-code/.claude-plugin/plugin.json +5 -0
  5. package/agent_config/claude-code/agents/ts-procedures-architect.md +188 -0
  6. package/agent_config/claude-code/skills/guide/SKILL.md +142 -0
  7. package/agent_config/claude-code/skills/guide/anti-patterns.md +608 -0
  8. package/agent_config/claude-code/skills/guide/api-reference.md +696 -0
  9. package/agent_config/claude-code/skills/guide/patterns.md +727 -0
  10. package/agent_config/claude-code/skills/review/SKILL.md +53 -0
  11. package/agent_config/claude-code/skills/review/checklist.md +163 -0
  12. package/agent_config/claude-code/skills/scaffold/SKILL.md +56 -0
  13. package/agent_config/claude-code/skills/scaffold/templates/express-rpc.md +134 -0
  14. package/agent_config/claude-code/skills/scaffold/templates/hono-api.md +169 -0
  15. package/agent_config/claude-code/skills/scaffold/templates/hono-rpc.md +139 -0
  16. package/agent_config/claude-code/skills/scaffold/templates/hono-stream.md +134 -0
  17. package/agent_config/claude-code/skills/scaffold/templates/procedure.md +77 -0
  18. package/agent_config/claude-code/skills/scaffold/templates/stream-procedure.md +113 -0
  19. package/agent_config/copilot/copilot-instructions.md +290 -0
  20. package/agent_config/cursor/cursorrules +290 -0
  21. package/agent_config/lib/install-claude.mjs +109 -0
  22. package/build/implementations/http/hono-api/index.d.ts +102 -0
  23. package/build/implementations/http/hono-api/index.js +339 -0
  24. package/build/implementations/http/hono-api/index.js.map +1 -0
  25. package/build/implementations/http/hono-api/index.test.d.ts +1 -0
  26. package/build/implementations/http/hono-api/index.test.js +983 -0
  27. package/build/implementations/http/hono-api/index.test.js.map +1 -0
  28. package/build/implementations/http/hono-api/types.d.ts +13 -0
  29. package/build/implementations/http/hono-api/types.js +2 -0
  30. package/build/implementations/http/hono-api/types.js.map +1 -0
  31. package/build/implementations/types.d.ts +44 -0
  32. package/build/index.d.ts +28 -6
  33. package/build/index.js +28 -0
  34. package/build/index.js.map +1 -1
  35. package/build/schema/compute-schema.d.ts +5 -0
  36. package/build/schema/compute-schema.js +8 -1
  37. package/build/schema/compute-schema.js.map +1 -1
  38. package/build/schema/parser.d.ts +6 -5
  39. package/build/schema/parser.js +54 -0
  40. package/build/schema/parser.js.map +1 -1
  41. package/package.json +14 -4
  42. package/src/implementations/http/README.md +45 -2
  43. package/src/implementations/http/hono-api/index.test.ts +1328 -0
  44. package/src/implementations/http/hono-api/index.ts +461 -0
  45. package/src/implementations/http/hono-api/types.ts +16 -0
  46. package/src/implementations/types.ts +52 -0
  47. package/src/index.ts +87 -10
  48. package/src/schema/compute-schema.ts +23 -2
  49. package/src/schema/parser.ts +70 -3
@@ -0,0 +1,53 @@
1
+ ---
2
+ name: review
3
+ description: "Review code for ts-procedures pattern adherence. Usage: /ts-procedures:review <path>"
4
+ invocable_by:
5
+ - user
6
+ - model
7
+ user_instructions: |
8
+ Usage: /ts-procedures:review <path>
9
+
10
+ Reviews files at the given path for ts-procedures pattern adherence.
11
+ Accepts a file path or directory.
12
+
13
+ Examples:
14
+ /ts-procedures:review src/procedures/
15
+ /ts-procedures:review src/procedures/GetUser.procedure.ts
16
+ ---
17
+
18
+ # Review ts-procedures Code
19
+
20
+ Parse `$ARGUMENTS` as a file or directory path. If a directory, review all `.ts` and `.tsx` files within it.
21
+
22
+ ## Instructions
23
+
24
+ 1. Read the target file(s).
25
+ 2. Identify ts-procedures imports (`ts-procedures`, `ts-procedures/express-rpc`, `ts-procedures/hono-rpc`, `ts-procedures/hono-stream`, `ts-procedures/http`) to determine file types.
26
+ 3. Check each file against the categorized checklist in `checklist.md`.
27
+ 4. Output findings grouped by severity.
28
+
29
+ ## Output Format
30
+
31
+ For each finding:
32
+
33
+ ```
34
+ [SEVERITY] file:line — Violation
35
+ Problem: What's wrong
36
+ Fix: Concrete before/after code
37
+ ```
38
+
39
+ Severity levels:
40
+ - **CRITICAL** — Will cause bugs, silent failures, resource leaks, or runtime errors. Must fix.
41
+ - **WARNING** — Anti-pattern that hurts maintainability or correctness. Should fix.
42
+ - **SUGGESTION** — Improvement for readability, type safety, or DX. Nice to have.
43
+
44
+ ## Summary
45
+
46
+ After individual findings, provide:
47
+ - Total findings by severity
48
+ - Overall assessment (healthy / needs attention / significant issues)
49
+ - Top 3 priorities to address
50
+
51
+ ## Reference
52
+
53
+ See `checklist.md` for the complete categorized checklist by file type.
@@ -0,0 +1,163 @@
1
+ # ts-procedures Review Checklist
2
+
3
+ ## Procedure Definition Checks
4
+
5
+ ### CRITICAL
6
+ - [ ] Uses `ctx.error()` for business logic errors, never throws raw `Error` instances
7
+ - [ ] Does not expect `schema.returnType` to be validated at runtime (it's documentation only)
8
+ - [ ] No duplicate procedure names within the same factory
9
+ - [ ] Schema uses TypeBox builders (`Type.Object(...)`, etc.), not plain JSON Schema objects
10
+ - [ ] Stream handlers check `ctx.signal.aborted` in loops to prevent infinite resource consumption
11
+
12
+ ### WARNING
13
+ - [ ] Passes `ctx.signal` to all downstream async calls (fetch, database queries, etc.)
14
+ - [ ] Does not put manual validation logic in handler when `schema.params` handles it
15
+ - [ ] Does not swallow errors in try/catch without re-throwing (hides failures)
16
+ - [ ] Uses `validateYields: true` only when `schema.yieldType` is also defined
17
+ - [ ] Does not mix Create procedures and CreateStream procedures in confusing ways
18
+
19
+ ### SUGGESTION
20
+ - [ ] Has `description` field set for documentation/introspection
21
+ - [ ] Uses meaningful procedure names (PascalCase, verb-noun: `GetUser`, `CreateOrder`)
22
+ - [ ] Destructures return value for clean exports: `const { GetUser, info } = Create(...)`
23
+
24
+ ---
25
+
26
+ ## Schema Checks
27
+
28
+ ### CRITICAL
29
+ - [ ] Uses TypeBox (`Type.Object(...)`, `Type.String()`, etc.) — not plain objects
30
+ - [ ] Required fields use `Type.String()` directly; optional fields wrapped with `Type.Optional(...)`
31
+ - [ ] Does not define both `schema.params` and `schema.input` on the same procedure (mutually exclusive)
32
+
33
+ ### WARNING
34
+ - [ ] Understands AJV `coerceTypes: true` — no manual type parsing for query string values
35
+ - [ ] Understands AJV `removeAdditional: true` — extra fields in params are silently stripped
36
+ - [ ] Does not rely on params fields not in the schema (they get removed)
37
+
38
+ ### SUGGESTION
39
+ - [ ] `returnType` schema provided for documentation generation
40
+ - [ ] `yieldType` schema provided for streaming procedures
41
+
42
+ ---
43
+
44
+ ## Context Checks
45
+
46
+ ### CRITICAL
47
+ - [ ] Factory has explicit `TContext` type parameter when using HTTP builders
48
+ - [ ] Context factory function handles errors gracefully (async failures don't crash requests)
49
+
50
+ ### WARNING
51
+ - [ ] Context type includes `signal?: AbortSignal` if procedures need cancellation support
52
+ - [ ] Context factory resolves only what handlers need (avoid expensive unused computations)
53
+
54
+ ### SUGGESTION
55
+ - [ ] Context type is documented or self-descriptive
56
+ - [ ] Separate factories for different access levels (public vs authenticated)
57
+
58
+ ---
59
+
60
+ ## HTTP Builder Checks (Express/Hono)
61
+
62
+ ### CRITICAL
63
+ - [ ] Standard `Create` procedures registered with `ExpressRPCAppBuilder` or `HonoRPCAppBuilder`, NOT `HonoStreamAppBuilder`
64
+ - [ ] Stream `CreateStream` procedures registered with `HonoStreamAppBuilder`, NOT the RPC builders
65
+ - [ ] `onError` callback handles `ProcedureValidationError` and `ProcedureError` differently
66
+
67
+ ### WARNING
68
+ - [ ] `pathPrefix` set consistently across builders
69
+ - [ ] `scope` and `version` set on all procedures when using `RPCConfig`
70
+ - [ ] Uses `extendProcedureDoc` for documentation generation instead of manual doc building
71
+ - [ ] `onRequestEnd` used for cleanup/logging, not business logic
72
+
73
+ ### SUGGESTION
74
+ - [ ] Route documentation accessed via `builder.docs` for OpenAPI generation
75
+ - [ ] Lifecycle hooks used for observability (logging, metrics)
76
+
77
+ ---
78
+
79
+ ## Streaming Checks (HonoStreamAppBuilder)
80
+
81
+ ### CRITICAL
82
+ - [ ] `onPreStreamError` handles validation errors before streaming starts (returns HTTP response)
83
+ - [ ] `onMidStreamError` handles runtime errors during streaming (yields error event)
84
+ - [ ] Stream handler does not assume `signal.reason` is always `'stream-completed'` — check for external abort
85
+
86
+ ### WARNING
87
+ - [ ] Uses `sse()` helper for SSE metadata when custom event types or IDs are needed
88
+ - [ ] Correctly distinguishes stream modes: `'sse'` for EventSource clients, `'text'` for simple consumers
89
+ - [ ] Does not yield after `ctx.signal.aborted` is true
90
+
91
+ ### SUGGESTION
92
+ - [ ] Stream mode documented in comments or config
93
+ - [ ] Error data shape matches `yieldType` schema for consistent client parsing
94
+
95
+ ---
96
+
97
+ ## API Builder Checks (HonoAPIAppBuilder)
98
+
99
+ ### CRITICAL
100
+ - [ ] `build()` is `await`ed — returns `Promise<Hono>`, not `Hono`
101
+ - [ ] Path param names in route template match `schema.input.pathParams` property names exactly
102
+ - [ ] Does not define both `schema.params` and `schema.input` on the same procedure
103
+ - [ ] `onError` callback handles `ProcedureValidationError` and `ProcedureError` differently
104
+
105
+ ### WARNING
106
+ - [ ] Uses `schema.input` channels appropriate for the HTTP method (no `body` on GET/HEAD)
107
+ - [ ] `pathPrefix` set consistently
108
+ - [ ] `successStatus` overridden only when default (POST→201, DELETE→204) is wrong
109
+ - [ ] Uses `APIInput` type constraint (`satisfies APIInput`) to catch channel name typos
110
+
111
+ ### SUGGESTION
112
+ - [ ] Route documentation accessed via `builder.docs` for OpenAPI generation
113
+ - [ ] Custom `queryParser` provided if complex query string formats needed
114
+ - [ ] Lifecycle hooks used for observability (logging, metrics)
115
+
116
+ ---
117
+
118
+ ## Error Handling Checks
119
+
120
+ ### CRITICAL
121
+ - [ ] Handler errors use `ctx.error(message, meta?)` — not `throw new Error()`
122
+ - [ ] Does not catch and swallow errors without re-throwing (hides failures from caller)
123
+ - [ ] HTTP builder has `onError` callback — default behavior may expose internal details
124
+
125
+ ### WARNING
126
+ - [ ] `ProcedureValidationError` handled separately (400 status) from `ProcedureError` (4xx/5xx)
127
+ - [ ] Error `meta` object contains useful context (error codes, IDs) — not sensitive data
128
+ - [ ] Stack traces not exposed in production responses
129
+
130
+ ### SUGGESTION
131
+ - [ ] Consistent error response shape across all procedures
132
+ - [ ] Error codes documented for API consumers
133
+
134
+ ---
135
+
136
+ ## Test File Checks
137
+
138
+ ### CRITICAL
139
+ - [ ] Tests both valid and invalid param scenarios
140
+ - [ ] Tests `ProcedureValidationError` is thrown for schema violations
141
+ - [ ] Tests `ProcedureError` is thrown for business logic failures
142
+
143
+ ### WARNING
144
+ - [ ] Tests pass context with required fields (doesn't rely on undefined context)
145
+ - [ ] HTTP builder tests verify response status codes and body shape
146
+ - [ ] Stream tests consume values with `for await...of` and break after expected count
147
+
148
+ ### SUGGESTION
149
+ - [ ] Tests verify `info` metadata (name, schema, description)
150
+ - [ ] Tests verify `getProcedures()` returns expected registrations
151
+ - [ ] Tests verify error `meta` contains expected fields
152
+
153
+ ---
154
+
155
+ ## Extended Config Checks
156
+
157
+ ### WARNING
158
+ - [ ] All procedures include required `TExtendedConfig` fields (e.g., `scope`, `version` for `RPCConfig`)
159
+ - [ ] Custom config fields used consistently across procedure group
160
+
161
+ ### SUGGESTION
162
+ - [ ] Custom config fields documented
163
+ - [ ] `getProcedures()` used for config introspection (permissions audit, etc.)
@@ -0,0 +1,56 @@
1
+ ---
2
+ name: scaffold
3
+ description: "Scaffold ts-procedures code with correct patterns. Usage: /ts-procedures:scaffold <type> <Name>"
4
+ invocable_by:
5
+ - user
6
+ - model
7
+ user_instructions: |
8
+ Usage: /ts-procedures:scaffold <type> <Name>
9
+
10
+ Types: procedure, stream-procedure, express-rpc, hono-rpc, hono-stream, hono-api
11
+
12
+ Examples:
13
+ /ts-procedures:scaffold procedure GetUser
14
+ /ts-procedures:scaffold stream-procedure StreamActivity
15
+ /ts-procedures:scaffold express-rpc UserApi
16
+ /ts-procedures:scaffold hono-rpc OrderApi
17
+ /ts-procedures:scaffold hono-stream LiveFeed
18
+ /ts-procedures:scaffold hono-api ProductApi
19
+ ---
20
+
21
+ # Scaffold ts-procedures Code
22
+
23
+ Parse `$ARGUMENTS` as `<type> <Name>` (case-insensitive type, PascalCase Name).
24
+
25
+ ## Instructions
26
+
27
+ 1. Parse the arguments. If missing, ask the user for `<type>` and `<Name>`.
28
+ 2. Derive placeholder variants from the provided PascalCase Name:
29
+ - `{{Name}}` — PascalCase as given (e.g., `UserProfile`)
30
+ - `{{name}}` — camelCase (e.g., `userProfile`)
31
+ - For URL scopes and file paths, use kebab-case (e.g., `user-profile`)
32
+ 3. Read the template file from `templates/<type>.md` in this skill directory.
33
+ 4. Replace all `{{Name}}` and `{{name}}` placeholders with the appropriate variants.
34
+ 5. Generate the implementation file(s) following the template exactly.
35
+ 6. Also generate a colocated test file following ts-procedures test conventions.
36
+
37
+ ## Valid Types
38
+
39
+ | Type | Template | Files Generated |
40
+ |------|----------|----------------|
41
+ | `procedure` | `templates/procedure.md` | `{{Name}}.procedure.ts`, `{{Name}}.procedure.test.ts` |
42
+ | `stream-procedure` | `templates/stream-procedure.md` | `{{Name}}.stream.ts`, `{{Name}}.stream.test.ts` |
43
+ | `express-rpc` | `templates/express-rpc.md` | `{{Name}}.rpc.ts`, `{{Name}}.rpc.test.ts` |
44
+ | `hono-rpc` | `templates/hono-rpc.md` | `{{Name}}.rpc.ts`, `{{Name}}.rpc.test.ts` |
45
+ | `hono-stream` | `templates/hono-stream.md` | `{{Name}}.stream-rpc.ts`, `{{Name}}.stream-rpc.test.ts` |
46
+ | `hono-api` | `templates/hono-api.md` | `{{Name}}.api.ts`, `{{Name}}.api.test.ts` |
47
+
48
+ ## Rules
49
+
50
+ - Use `ctx.error()` for business logic errors, never throw raw `Error`.
51
+ - `schema.params` is validated at runtime; `schema.returnType` is documentation only.
52
+ - Pass `ctx.signal` to all downstream async calls.
53
+ - Stream handlers always have `ctx.signal` (guaranteed `AbortSignal`).
54
+ - Use TypeBox for schema definitions (`import { Type } from 'typebox'`).
55
+ - AJV config: `allErrors: true`, `coerceTypes: true`, `removeAdditional: true`.
56
+ - Test files use `describe`/`test` from vitest.
@@ -0,0 +1,134 @@
1
+ # Express RPC Template: {{Name}}
2
+
3
+ ## Implementation — `{{Name}}.rpc.ts`
4
+
5
+ ```typescript
6
+ import { Procedures, ProcedureError, ProcedureValidationError } from 'ts-procedures'
7
+ import { ExpressRPCAppBuilder } from 'ts-procedures/express-rpc'
8
+ import type { RPCConfig } from 'ts-procedures/http'
9
+ import { Type } from 'typebox'
10
+
11
+ // ─── Context ──────────────────────────────────────────────
12
+
13
+ type {{Name}}Context = {
14
+ userId: string
15
+ requestId: string
16
+ }
17
+
18
+ // ─── Procedures ───────────────────────────────────────────
19
+
20
+ const RPC = Procedures<{{Name}}Context, RPCConfig>()
21
+
22
+ export const { GetItem } = RPC.Create(
23
+ 'GetItem',
24
+ {
25
+ scope: '{{name}}', // TODO: set URL scope
26
+ version: 1,
27
+ description: 'Fetch item by ID',
28
+ schema: {
29
+ params: Type.Object({
30
+ id: Type.String(),
31
+ }),
32
+ returnType: Type.Object({
33
+ id: Type.String(),
34
+ name: Type.String(),
35
+ }),
36
+ },
37
+ },
38
+ async (ctx, params) => {
39
+ // TODO: implement
40
+ return { id: params.id, name: 'Example' }
41
+ }
42
+ )
43
+
44
+ export const { ListItems } = RPC.Create(
45
+ 'ListItems',
46
+ {
47
+ scope: '{{name}}',
48
+ version: 1,
49
+ description: 'List all items',
50
+ schema: {
51
+ params: Type.Object({
52
+ page: Type.Optional(Type.Number()),
53
+ limit: Type.Optional(Type.Number()),
54
+ }),
55
+ },
56
+ },
57
+ async (ctx, params) => {
58
+ // TODO: implement
59
+ return { items: [], total: 0 }
60
+ }
61
+ )
62
+
63
+ // ─── Express App Builder ──────────────────────────────────
64
+
65
+ export const {{name}}App = new ExpressRPCAppBuilder({
66
+ pathPrefix: '/api',
67
+ onError: (procedure, req, res, error) => {
68
+ if (error instanceof ProcedureValidationError) {
69
+ res.status(400).json({
70
+ error: error.message,
71
+ details: error.errors,
72
+ procedure: error.procedureName,
73
+ })
74
+ } else if (error instanceof ProcedureError) {
75
+ res.status(422).json({
76
+ error: error.message,
77
+ meta: error.meta,
78
+ procedure: error.procedureName,
79
+ })
80
+ } else {
81
+ res.status(500).json({ error: 'Internal server error' })
82
+ }
83
+ },
84
+ })
85
+ .register(RPC, async (req) => ({
86
+ userId: req.headers['x-user-id'] as string || 'anonymous',
87
+ requestId: req.headers['x-request-id'] as string || crypto.randomUUID(),
88
+ }))
89
+ .build()
90
+
91
+ // Route map:
92
+ // POST /api/{{name}}/get-item/1
93
+ // POST /api/{{name}}/list-items/1
94
+
95
+ // Documentation: {{name}}App.docs
96
+ ```
97
+
98
+ ## Test — `{{Name}}.rpc.test.ts`
99
+
100
+ ```typescript
101
+ import { describe, test, expect } from 'vitest'
102
+ import supertest from 'supertest'
103
+ import { {{name}}App } from './{{Name}}.rpc'
104
+
105
+ describe('{{Name}} RPC', () => {
106
+ test('GET item returns expected result', async () => {
107
+ const res = await supertest({{name}}App)
108
+ .post('/api/{{name}}/get-item/1')
109
+ .send({ id: 'item-1' })
110
+ .expect(200)
111
+
112
+ expect(res.body).toEqual({ id: 'item-1', name: 'Example' })
113
+ })
114
+
115
+ test('returns 400 for invalid params', async () => {
116
+ const res = await supertest({{name}}App)
117
+ .post('/api/{{name}}/get-item/1')
118
+ .send({})
119
+ .expect(400)
120
+
121
+ expect(res.body.error).toBeDefined()
122
+ })
123
+
124
+ test('LIST items returns array', async () => {
125
+ const res = await supertest({{name}}App)
126
+ .post('/api/{{name}}/list-items/1')
127
+ .send({ page: 1, limit: 10 })
128
+ .expect(200)
129
+
130
+ expect(res.body).toHaveProperty('items')
131
+ expect(res.body).toHaveProperty('total')
132
+ })
133
+ })
134
+ ```
@@ -0,0 +1,169 @@
1
+ # Hono API Template: {{Name}}
2
+
3
+ ## Implementation — `{{Name}}.api.ts`
4
+
5
+ ```typescript
6
+ import { Procedures, ProcedureError, ProcedureValidationError } from 'ts-procedures'
7
+ import { HonoAPIAppBuilder } from 'ts-procedures/hono-api'
8
+ import type { APIConfig, APIInput } from 'ts-procedures/http'
9
+ import { Type } from 'typebox'
10
+
11
+ // ─── Context ──────────────────────────────────────────────
12
+
13
+ type {{Name}}Context = {
14
+ userId: string
15
+ requestId: string
16
+ }
17
+
18
+ // ─── Procedures ───────────────────────────────────────────
19
+
20
+ const API = Procedures<{{Name}}Context, APIConfig>()
21
+
22
+ export const { GetItem } = API.Create(
23
+ 'GetItem',
24
+ {
25
+ path: '/{{name}}/:id', // TODO: set route path
26
+ method: 'get',
27
+ description: 'Fetch item by ID',
28
+ schema: {
29
+ input: {
30
+ pathParams: Type.Object({ id: Type.String() }),
31
+ } satisfies APIInput,
32
+ returnType: Type.Object({
33
+ id: Type.String(),
34
+ name: Type.String(),
35
+ }),
36
+ },
37
+ },
38
+ async (ctx, { pathParams }) => {
39
+ // TODO: implement
40
+ return { id: pathParams.id, name: 'Example' }
41
+ }
42
+ )
43
+
44
+ export const { CreateItem } = API.Create(
45
+ 'CreateItem',
46
+ {
47
+ path: '/{{name}}',
48
+ method: 'post',
49
+ description: 'Create a new item',
50
+ schema: {
51
+ input: {
52
+ body: Type.Object({
53
+ name: Type.String(),
54
+ }),
55
+ } satisfies APIInput,
56
+ returnType: Type.Object({
57
+ id: Type.String(),
58
+ name: Type.String(),
59
+ }),
60
+ },
61
+ },
62
+ async (ctx, { body }) => {
63
+ // TODO: implement
64
+ return { id: 'new-id', name: body.name }
65
+ }
66
+ )
67
+
68
+ export const { DeleteItem } = API.Create(
69
+ 'DeleteItem',
70
+ {
71
+ path: '/{{name}}/:id',
72
+ method: 'delete',
73
+ schema: {
74
+ input: {
75
+ pathParams: Type.Object({ id: Type.String() }),
76
+ } satisfies APIInput,
77
+ },
78
+ },
79
+ async (ctx, { pathParams }) => {
80
+ // TODO: implement deletion
81
+ }
82
+ )
83
+
84
+ // ─── Hono App Builder ─────────────────────────────────────
85
+
86
+ export const {{name}}App = await new HonoAPIAppBuilder({
87
+ pathPrefix: '/api',
88
+ onError: (procedure, c, error) => {
89
+ if (error instanceof ProcedureValidationError) {
90
+ return c.json({
91
+ error: error.message,
92
+ details: error.errors,
93
+ procedure: error.procedureName,
94
+ }, 400)
95
+ } else if (error instanceof ProcedureError) {
96
+ return c.json({
97
+ error: error.message,
98
+ meta: error.meta,
99
+ procedure: error.procedureName,
100
+ }, 422)
101
+ }
102
+ return c.json({ error: 'Internal server error' }, 500)
103
+ },
104
+ })
105
+ .register(API, (c) => ({
106
+ userId: c.req.header('x-user-id') || 'anonymous',
107
+ requestId: c.req.header('x-request-id') || crypto.randomUUID(),
108
+ }))
109
+ .build()
110
+
111
+ // Route map:
112
+ // GET /api/{{name}}/:id → 200
113
+ // POST /api/{{name}} → 201
114
+ // DELETE /api/{{name}}/:id → 204
115
+
116
+ // Documentation: builder.docs (access before .build() resolves)
117
+ ```
118
+
119
+ ## Test — `{{Name}}.api.test.ts`
120
+
121
+ ```typescript
122
+ import { describe, test, expect } from 'vitest'
123
+ import { {{name}}App } from './{{Name}}.api'
124
+
125
+ describe('{{Name}} API', () => {
126
+ test('GET item returns expected result', async () => {
127
+ const app = await {{name}}App
128
+ const res = await app.request('/api/{{name}}/item-1')
129
+
130
+ expect(res.status).toBe(200)
131
+ const body = await res.json()
132
+ expect(body).toEqual({ id: 'item-1', name: 'Example' })
133
+ })
134
+
135
+ test('POST item returns 201', async () => {
136
+ const app = await {{name}}App
137
+ const res = await app.request('/api/{{name}}', {
138
+ method: 'POST',
139
+ headers: { 'Content-Type': 'application/json' },
140
+ body: JSON.stringify({ name: 'New Item' }),
141
+ })
142
+
143
+ expect(res.status).toBe(201)
144
+ const body = await res.json()
145
+ expect(body).toHaveProperty('id')
146
+ expect(body.name).toBe('New Item')
147
+ })
148
+
149
+ test('DELETE item returns 204', async () => {
150
+ const app = await {{name}}App
151
+ const res = await app.request('/api/{{name}}/item-1', {
152
+ method: 'DELETE',
153
+ })
154
+
155
+ expect(res.status).toBe(204)
156
+ })
157
+
158
+ test('returns 400 for invalid POST body', async () => {
159
+ const app = await {{name}}App
160
+ const res = await app.request('/api/{{name}}', {
161
+ method: 'POST',
162
+ headers: { 'Content-Type': 'application/json' },
163
+ body: JSON.stringify({}),
164
+ })
165
+
166
+ expect(res.status).toBe(400)
167
+ })
168
+ })
169
+ ```