ts-procedures 5.3.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 (38) hide show
  1. package/README.md +90 -0
  2. package/agent_config/claude-code/agents/ts-procedures-architect.md +15 -0
  3. package/agent_config/claude-code/skills/guide/anti-patterns.md +106 -0
  4. package/agent_config/claude-code/skills/guide/api-reference.md +150 -4
  5. package/agent_config/claude-code/skills/guide/patterns.md +155 -0
  6. package/agent_config/claude-code/skills/review/checklist.md +22 -0
  7. package/agent_config/claude-code/skills/scaffold/SKILL.md +3 -1
  8. package/agent_config/claude-code/skills/scaffold/templates/hono-api.md +169 -0
  9. package/agent_config/copilot/copilot-instructions.md +35 -0
  10. package/agent_config/cursor/cursorrules +35 -0
  11. package/build/implementations/http/hono-api/index.d.ts +102 -0
  12. package/build/implementations/http/hono-api/index.js +339 -0
  13. package/build/implementations/http/hono-api/index.js.map +1 -0
  14. package/build/implementations/http/hono-api/index.test.d.ts +1 -0
  15. package/build/implementations/http/hono-api/index.test.js +983 -0
  16. package/build/implementations/http/hono-api/index.test.js.map +1 -0
  17. package/build/implementations/http/hono-api/types.d.ts +13 -0
  18. package/build/implementations/http/hono-api/types.js +2 -0
  19. package/build/implementations/http/hono-api/types.js.map +1 -0
  20. package/build/implementations/types.d.ts +44 -0
  21. package/build/index.d.ts +28 -6
  22. package/build/index.js +28 -0
  23. package/build/index.js.map +1 -1
  24. package/build/schema/compute-schema.d.ts +5 -0
  25. package/build/schema/compute-schema.js +8 -1
  26. package/build/schema/compute-schema.js.map +1 -1
  27. package/build/schema/parser.d.ts +6 -5
  28. package/build/schema/parser.js +54 -0
  29. package/build/schema/parser.js.map +1 -1
  30. package/package.json +8 -3
  31. package/src/implementations/http/README.md +45 -2
  32. package/src/implementations/http/hono-api/index.test.ts +1328 -0
  33. package/src/implementations/http/hono-api/index.ts +461 -0
  34. package/src/implementations/http/hono-api/types.ts +16 -0
  35. package/src/implementations/types.ts +52 -0
  36. package/src/index.ts +87 -10
  37. package/src/schema/compute-schema.ts +23 -2
  38. package/src/schema/parser.ts +70 -3
@@ -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
+ ```
@@ -34,6 +34,10 @@ import { HonoRPCAppBuilder } from 'ts-procedures/hono-rpc'
34
34
 
35
35
  // Hono Streaming
36
36
  import { HonoStreamAppBuilder, sse } from 'ts-procedures/hono-stream'
37
+
38
+ // Hono API (REST-style)
39
+ import { HonoAPIAppBuilder } from 'ts-procedures/hono-api'
40
+ import type { APIConfig, APIInput } from 'ts-procedures/hono-api'
37
41
  ```
38
42
 
39
43
  ## Architecture Rules
@@ -156,6 +160,34 @@ const app = new HonoStreamAppBuilder({ defaultStreamMode: 'sse' })
156
160
  // GET|POST /events/feed/1
157
161
  ```
158
162
 
163
+ ## Hono API Pattern (REST-style)
164
+
165
+ ```typescript
166
+ import { HonoAPIAppBuilder } from 'ts-procedures/hono-api'
167
+ import type { APIConfig } from 'ts-procedures/http'
168
+
169
+ const API = Procedures<AppContext, APIConfig>()
170
+
171
+ API.Create('GetUser', {
172
+ path: '/users/:id', method: 'get',
173
+ schema: {
174
+ input: { pathParams: Type.Object({ id: Type.String() }) },
175
+ },
176
+ }, async (ctx, { pathParams }) => fetchUser(pathParams.id))
177
+
178
+ API.Create('CreateUser', {
179
+ path: '/users', method: 'post',
180
+ schema: {
181
+ input: { body: Type.Object({ name: Type.String() }) },
182
+ },
183
+ }, async (ctx, { body }) => createUser(body))
184
+
185
+ const app = await new HonoAPIAppBuilder({ pathPrefix: '/api' })
186
+ .register(API, (c) => ({ userId: c.req.header('x-user-id') }))
187
+ .build()
188
+ // GET /api/users/:id → 200, POST /api/users → 201
189
+ ```
190
+
159
191
  ## Error Handling
160
192
 
161
193
  | Error Class | Trigger | HTTP Status |
@@ -206,6 +238,7 @@ onRequestStart → factoryContext() → validation → onStreamStart → handler
206
238
  - Express → `ExpressRPCAppBuilder`
207
239
  - Hono (standard) → `HonoRPCAppBuilder`
208
240
  - Hono (streaming) → `HonoStreamAppBuilder`
241
+ - Hono (REST-style, per-channel input) → `HonoAPIAppBuilder`
209
242
 
210
243
  **Stream mode?**
211
244
  - Browser EventSource → `'sse'` (default)
@@ -223,6 +256,8 @@ onRequestStart → factoryContext() → validation → onStreamStart → handler
223
256
  8. **Never swallow errors without re-throwing** — hides failures
224
257
  9. **Never assume extra params fields survive** — `removeAdditional: true` strips them
225
258
  10. **Never manually parse types AJV coerces** — `coerceTypes: true` handles it
259
+ 11. **Never define both schema.params and schema.input** — mutually exclusive, throws ProcedureRegistrationError
260
+ 12. **Never forget to await HonoAPIAppBuilder.build()** — it's async (resolves query parser)
226
261
 
227
262
  ## Testing
228
263
 
@@ -34,6 +34,10 @@ import { HonoRPCAppBuilder } from 'ts-procedures/hono-rpc'
34
34
 
35
35
  // Hono Streaming
36
36
  import { HonoStreamAppBuilder, sse } from 'ts-procedures/hono-stream'
37
+
38
+ // Hono API (REST-style)
39
+ import { HonoAPIAppBuilder } from 'ts-procedures/hono-api'
40
+ import type { APIConfig, APIInput } from 'ts-procedures/hono-api'
37
41
  ```
38
42
 
39
43
  ## Architecture Rules
@@ -156,6 +160,34 @@ const app = new HonoStreamAppBuilder({ defaultStreamMode: 'sse' })
156
160
  // GET|POST /events/feed/1
157
161
  ```
158
162
 
163
+ ## Hono API Pattern (REST-style)
164
+
165
+ ```typescript
166
+ import { HonoAPIAppBuilder } from 'ts-procedures/hono-api'
167
+ import type { APIConfig } from 'ts-procedures/http'
168
+
169
+ const API = Procedures<AppContext, APIConfig>()
170
+
171
+ API.Create('GetUser', {
172
+ path: '/users/:id', method: 'get',
173
+ schema: {
174
+ input: { pathParams: Type.Object({ id: Type.String() }) },
175
+ },
176
+ }, async (ctx, { pathParams }) => fetchUser(pathParams.id))
177
+
178
+ API.Create('CreateUser', {
179
+ path: '/users', method: 'post',
180
+ schema: {
181
+ input: { body: Type.Object({ name: Type.String() }) },
182
+ },
183
+ }, async (ctx, { body }) => createUser(body))
184
+
185
+ const app = await new HonoAPIAppBuilder({ pathPrefix: '/api' })
186
+ .register(API, (c) => ({ userId: c.req.header('x-user-id') }))
187
+ .build()
188
+ // GET /api/users/:id → 200, POST /api/users → 201
189
+ ```
190
+
159
191
  ## Error Handling
160
192
 
161
193
  | Error Class | Trigger | HTTP Status |
@@ -206,6 +238,7 @@ onRequestStart → factoryContext() → validation → onStreamStart → handler
206
238
  - Express → `ExpressRPCAppBuilder`
207
239
  - Hono (standard) → `HonoRPCAppBuilder`
208
240
  - Hono (streaming) → `HonoStreamAppBuilder`
241
+ - Hono (REST-style, per-channel input) → `HonoAPIAppBuilder`
209
242
 
210
243
  **Stream mode?**
211
244
  - Browser EventSource → `'sse'` (default)
@@ -223,6 +256,8 @@ onRequestStart → factoryContext() → validation → onStreamStart → handler
223
256
  8. **Never swallow errors without re-throwing** — hides failures
224
257
  9. **Never assume extra params fields survive** — `removeAdditional: true` strips them
225
258
  10. **Never manually parse types AJV coerces** — `coerceTypes: true` handles it
259
+ 11. **Never define both schema.params and schema.input** — mutually exclusive, throws ProcedureRegistrationError
260
+ 12. **Never forget to await HonoAPIAppBuilder.build()** — it's async (resolves query parser)
226
261
 
227
262
  ## Testing
228
263
 
@@ -0,0 +1,102 @@
1
+ import { Hono, Context } from 'hono';
2
+ import { TProcedureRegistration } from '../../../index.js';
3
+ import { ExtractConfig, ExtractContext, ProceduresFactory, APIConfig, APIHttpRouteDoc, APIInput, HttpMethod } from '../../types.js';
4
+ export type { APIConfig, APIHttpRouteDoc, APIInput, HttpMethod };
5
+ export type QueryParser = (queryString: string) => Record<string, unknown>;
6
+ export type HonoAPIAppBuilderConfig = {
7
+ /**
8
+ * An existing Hono application instance to use.
9
+ * If not provided, a new instance will be created.
10
+ */
11
+ app?: Hono;
12
+ /** Optional path prefix for all API routes. */
13
+ pathPrefix?: string;
14
+ /**
15
+ * Custom query string parser. Receives the raw query string (without '?').
16
+ * Default: uses `qs` (optional peer dependency) if available, otherwise native URLSearchParams.
17
+ */
18
+ queryParser?: QueryParser;
19
+ onRequestStart?: (c: Context) => void;
20
+ onRequestEnd?: (c: Context) => void;
21
+ onSuccess?: (procedure: TProcedureRegistration, c: Context) => void;
22
+ /**
23
+ * Error handler called when a procedure throws an error.
24
+ */
25
+ onError?: (procedure: TProcedureRegistration, c: Context, error: Error) => Response | Promise<Response>;
26
+ };
27
+ /**
28
+ * Builder class for creating a Hono application with REST-style API routes.
29
+ *
30
+ * Uses `schema.input` for per-channel type safety:
31
+ * - `input.pathParams` → validated path parameters
32
+ * - `input.query` → validated query string parameters
33
+ * - `input.body` → validated request body
34
+ * - `input.headers` → validated request headers
35
+ *
36
+ * Usage:
37
+ * const API = Procedures<MyContext, APIConfig>()
38
+ *
39
+ * API.Create('GetUser', {
40
+ * path: '/users/:id',
41
+ * method: 'get',
42
+ * schema: {
43
+ * input: {
44
+ * pathParams: Type.Object({ id: Type.String() }),
45
+ * query: Type.Object({ include: Type.Optional(Type.String()) }),
46
+ * },
47
+ * returnType: Type.Object({ id: Type.String(), name: Type.String() }),
48
+ * }
49
+ * }, async (ctx, { pathParams, query }) => {
50
+ * return { id: pathParams.id, name: 'John' }
51
+ * })
52
+ *
53
+ * const apiApp = new HonoAPIAppBuilder()
54
+ * .register(API, (c) => ({ ... }))
55
+ * .build()
56
+ */
57
+ export declare class HonoAPIAppBuilder {
58
+ readonly config?: HonoAPIAppBuilderConfig | undefined;
59
+ constructor(config?: HonoAPIAppBuilderConfig | undefined);
60
+ private factories;
61
+ private _app;
62
+ private _docs;
63
+ get app(): Hono;
64
+ get docs(): APIHttpRouteDoc[];
65
+ /**
66
+ * Registers a procedure factory with its context.
67
+ * @param factory - The procedure factory created by Procedures<Context, APIConfig>()
68
+ * @param factoryContext - Context for handlers. Direct value, sync function, or async function.
69
+ * @param extendProcedureDoc - Custom function to extend the generated route documentation.
70
+ */
71
+ register<TFactory extends ProceduresFactory>(factory: TFactory, factoryContext: ExtractContext<TFactory> | ((c: Context) => ExtractContext<TFactory> | Promise<ExtractContext<TFactory>>), extendProcedureDoc?: (params: {
72
+ base: APIHttpRouteDoc;
73
+ procedure: TProcedureRegistration<any, ExtractConfig<TFactory>>;
74
+ }) => Record<string, any>): this;
75
+ /**
76
+ * Resolves the full path for a route, combining pathPrefix and the procedure's path.
77
+ */
78
+ private resolveFullPath;
79
+ /**
80
+ * Builds and returns the Hono application with registered API routes.
81
+ * Async because it resolves the query parser (qs optional peer dep) once at build time.
82
+ */
83
+ build(): Promise<Hono>;
84
+ /**
85
+ * Validates that path parameter names in the path template match the schema.input.pathParams declaration.
86
+ */
87
+ private validatePathParamConsistency;
88
+ /**
89
+ * Creates the async route handler for a procedure.
90
+ */
91
+ private createRouteHandler;
92
+ /**
93
+ * Extracts and assembles structured input params from HTTP request sources.
94
+ * Each channel (pathParams, query, body, headers) is extracted from its HTTP source.
95
+ * The core validates each channel independently via schema.input validators.
96
+ */
97
+ private extractInputParams;
98
+ /**
99
+ * Generates the API HTTP route documentation for the given procedure.
100
+ */
101
+ private buildApiHttpRouteDoc;
102
+ }