ts-procedures 2.1.1 → 3.0.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 (46) hide show
  1. package/build/errors.d.ts +2 -1
  2. package/build/errors.js +3 -2
  3. package/build/errors.js.map +1 -1
  4. package/build/errors.test.d.ts +1 -0
  5. package/build/errors.test.js +40 -0
  6. package/build/errors.test.js.map +1 -0
  7. package/build/implementations/http/express-rpc/index.d.ts +3 -2
  8. package/build/implementations/http/express-rpc/index.js +6 -6
  9. package/build/implementations/http/express-rpc/index.js.map +1 -1
  10. package/build/implementations/http/express-rpc/index.test.js +93 -93
  11. package/build/implementations/http/express-rpc/index.test.js.map +1 -1
  12. package/build/implementations/http/hono-rpc/index.d.ts +83 -0
  13. package/build/implementations/http/hono-rpc/index.js +148 -0
  14. package/build/implementations/http/hono-rpc/index.js.map +1 -0
  15. package/build/implementations/http/hono-rpc/index.test.d.ts +1 -0
  16. package/build/implementations/http/hono-rpc/index.test.js +647 -0
  17. package/build/implementations/http/hono-rpc/index.test.js.map +1 -0
  18. package/build/implementations/http/hono-rpc/types.d.ts +28 -0
  19. package/build/implementations/http/hono-rpc/types.js +2 -0
  20. package/build/implementations/http/hono-rpc/types.js.map +1 -0
  21. package/build/implementations/types.d.ts +1 -1
  22. package/build/index.d.ts +12 -0
  23. package/build/index.js +29 -7
  24. package/build/index.js.map +1 -1
  25. package/build/index.test.js +65 -0
  26. package/build/index.test.js.map +1 -1
  27. package/build/schema/parser.js +3 -0
  28. package/build/schema/parser.js.map +1 -1
  29. package/build/schema/parser.test.js +18 -0
  30. package/build/schema/parser.test.js.map +1 -1
  31. package/package.json +8 -2
  32. package/src/errors.test.ts +53 -0
  33. package/src/errors.ts +4 -2
  34. package/src/implementations/http/README.md +172 -0
  35. package/src/implementations/http/express-rpc/README.md +152 -243
  36. package/src/implementations/http/express-rpc/index.test.ts +93 -93
  37. package/src/implementations/http/express-rpc/index.ts +15 -7
  38. package/src/implementations/http/hono-rpc/README.md +293 -0
  39. package/src/implementations/http/hono-rpc/index.test.ts +847 -0
  40. package/src/implementations/http/hono-rpc/index.ts +202 -0
  41. package/src/implementations/http/hono-rpc/types.ts +33 -0
  42. package/src/implementations/types.ts +2 -1
  43. package/src/index.test.ts +83 -0
  44. package/src/index.ts +34 -8
  45. package/src/schema/parser.test.ts +26 -0
  46. package/src/schema/parser.ts +5 -1
@@ -0,0 +1,172 @@
1
+ # HTTP-RPC Implementations
2
+
3
+ HTTP-RPC builders for `ts-procedures` that create type-safe, versioned RPC endpoints with automatic path generation, schema-based validation, and route documentation.
4
+
5
+ ## Available Implementations
6
+
7
+ | Framework | Package | Description |
8
+ |-----------|---------|-------------|
9
+ | [Express](./express-rpc/README.md) | `express-rpc` | Express.js integration |
10
+ | [Hono](./hono-rpc/README.md) | `hono-rpc` | Hono integration (Bun, Deno, Cloudflare Workers, Node.js) |
11
+
12
+ ## Core Concepts
13
+
14
+ ### RPCConfig Interface
15
+
16
+ All HTTP-RPC implementations use a shared configuration interface:
17
+
18
+ ```typescript
19
+ interface RPCConfig {
20
+ scope: string | string[] // Route path segment(s)
21
+ version: number // API version number
22
+ }
23
+ ```
24
+
25
+ ### Path Generation
26
+
27
+ Routes are generated using kebab-case conversion with the formula:
28
+
29
+ ```
30
+ /{pathPrefix}/{scope...}/{procedureName}/{version}
31
+ ```
32
+
33
+ **Conversion Examples:**
34
+
35
+ | Scope | Procedure Name | Version | Generated Path |
36
+ |-------|----------------|---------|----------------|
37
+ | `'users'` | `'Create'` | `1` | `/users/create/1` |
38
+ | `'users'` | `'GetById'` | `1` | `/users/get-by-id/1` |
39
+ | `['users', 'admin']` | `'List'` | `1` | `/users/admin/list/1` |
40
+ | `['UserModule', 'permissions']` | `'Update'` | `2` | `/user-module/permissions/update/2` |
41
+
42
+ **With pathPrefix `/api/v1`:**
43
+
44
+ | Scope | Procedure Name | Version | Generated Path |
45
+ |-------|----------------|---------|----------------|
46
+ | `'users'` | `'Create'` | `1` | `/api/v1/users/create/1` |
47
+ | `['users', 'admin']` | `'Delete'` | `2` | `/api/v1/users/admin/delete/2` |
48
+
49
+ ### Context Resolution Patterns
50
+
51
+ The `factoryContext` parameter supports three patterns:
52
+
53
+ ```typescript
54
+ // 1. Static object
55
+ builder.register(RPC, { userId: 'static-123' })
56
+
57
+ // 2. Sync function
58
+ builder.register(RPC, (req) => ({
59
+ userId: req.headers['x-user-id']
60
+ }))
61
+
62
+ // 3. Async function
63
+ builder.register(RPC, async (req) => {
64
+ const user = await validateToken(req.headers.authorization)
65
+ return { userId: user.id }
66
+ })
67
+ ```
68
+
69
+ ### Lifecycle Hooks
70
+
71
+ Hooks execute in the following order:
72
+
73
+ ```
74
+ onRequestStart → handler → onSuccess → onRequestEnd
75
+
76
+ (on error)
77
+
78
+ error handler
79
+
80
+ onRequestEnd
81
+ ```
82
+
83
+ | Hook | Trigger | Use Case |
84
+ |------|---------|----------|
85
+ | `onRequestStart` | Before route handler | Logging, request tracking |
86
+ | `onSuccess` | After successful handler execution | Metrics, audit logging |
87
+ | `onRequestEnd` | After response sent | Cleanup, timing metrics |
88
+ | `error` | On handler error | Custom error responses |
89
+
90
+ ### Route Documentation
91
+
92
+ Each registered procedure generates an `RPCHttpRouteDoc`:
93
+
94
+ ```typescript
95
+ interface RPCHttpRouteDoc {
96
+ path: string // Generated route path
97
+ method: 'post' // Always POST for RPC
98
+ jsonSchema: {
99
+ body?: object // JSON Schema from schema.params
100
+ response?: object // JSON Schema from schema.returnType
101
+ }
102
+ }
103
+ ```
104
+
105
+ Access documentation via `builder.docs` after calling `build()`:
106
+
107
+ ```typescript
108
+ const builder = new ExpressRPCAppBuilder()
109
+ builder.register(RPC, () => ({}))
110
+ builder.build()
111
+
112
+ // Generate OpenAPI documentation
113
+ const openApiPaths = builder.docs.reduce((acc, doc) => {
114
+ acc[doc.path] = {
115
+ post: {
116
+ requestBody: doc.jsonSchema.body ? {
117
+ content: { 'application/json': { schema: doc.jsonSchema.body } }
118
+ } : undefined,
119
+ responses: {
120
+ 200: doc.jsonSchema.response ? {
121
+ content: { 'application/json': { schema: doc.jsonSchema.response } }
122
+ } : undefined
123
+ }
124
+ }
125
+ }
126
+ return acc
127
+ }, {})
128
+ ```
129
+
130
+ ### Builder Pattern
131
+
132
+ All implementations follow the same builder pattern:
133
+
134
+ ```typescript
135
+ const builder = new RPCAppBuilder(config)
136
+ .register(PublicRPC, publicContextResolver)
137
+ .register(ProtectedRPC, protectedContextResolver)
138
+
139
+ const app = builder.build()
140
+ const docs = builder.docs
141
+ ```
142
+
143
+ **Key methods:**
144
+
145
+ | Method | Returns | Description |
146
+ |--------|---------|-------------|
147
+ | `register(factory, context)` | `this` | Register a procedure factory with context resolver |
148
+ | `build()` | Framework app | Create routes and return the application |
149
+ | `makeRPCHttpRoutePath(config)` | `string` | Generate path for an RPCConfig |
150
+
151
+ **Properties:**
152
+
153
+ | Property | Type | Description |
154
+ |----------|------|-------------|
155
+ | `app` | Framework app | The underlying framework application |
156
+ | `docs` | `RPCHttpRouteDoc[]` | Route documentation (populated after `build()`) |
157
+
158
+ ## Framework Comparison
159
+
160
+ | Aspect | Express | Hono |
161
+ |--------|---------|------|
162
+ | Context param | `req: express.Request` | `c: Context` |
163
+ | Error handler return | `void` (mutates res) | `Response` |
164
+ | Body access | `req.body` | `await c.req.json()` |
165
+ | Header access | `req.headers['x-id']` | `c.req.header('x-id')` |
166
+ | JSON middleware | Auto-added (or manual) | Built-in |
167
+
168
+ ## TypeScript Types
169
+
170
+ ```typescript
171
+ import { RPCConfig, RPCHttpRouteDoc } from 'ts-procedures/implementations/types'
172
+ ```
@@ -1,6 +1,6 @@
1
- # Express RPC Integration
1
+ # ExpressRPCAppBuilder
2
2
 
3
- RPC-style HTTP integration for `ts-procedures` using Express. Creates POST routes at `/{name}/{version}` paths (or `/{pathPrefix}/{name}/{version}` when configured) with automatic JSON schema documentation.
3
+ Express.js integration for `ts-procedures` that creates type-safe RPC endpoints as POST routes.
4
4
 
5
5
  ## Installation
6
6
 
@@ -8,25 +8,17 @@ RPC-style HTTP integration for `ts-procedures` using Express. Creates POST route
8
8
  npm install ts-procedures express
9
9
  ```
10
10
 
11
- ## Import
12
-
13
- ```typescript
14
- import { ExpressRPCAppBuilder } from 'ts-procedures/express-rpc'
15
- ```
16
-
17
11
  ## Quick Start
18
12
 
19
13
  ```typescript
14
+ import express from 'express'
20
15
  import { Procedures } from 'ts-procedures'
21
- import { ExpressRPCAppBuilder, RPCConfig } from 'ts-procedures/express-rpc'
16
+ import { ExpressRPCAppBuilder, RPCConfig } from 'ts-procedures/implementations/http/express-rpc'
22
17
  import { v } from 'suretype'
23
18
 
24
19
  // Define your context type
25
20
  type AppContext = { userId: string }
26
21
 
27
- // RPC config type
28
- // type RPCConfig = { name: string | string[]; version: number }
29
-
30
22
  // Create a procedure factory
31
23
  const RPC = Procedures<AppContext, RPCConfig>()
32
24
 
@@ -34,312 +26,229 @@ const RPC = Procedures<AppContext, RPCConfig>()
34
26
  RPC.Create(
35
27
  'GetUser',
36
28
  {
37
- name: ['users', 'get'],
29
+ scope: ['users', 'profile'],
38
30
  version: 1,
39
31
  schema: {
40
32
  params: v.object({ id: v.string() }),
41
- returnType: v.object({ id: v.string(), name: v.string() }),
42
- },
33
+ returnType: v.object({ id: v.string(), name: v.string() })
34
+ }
43
35
  },
44
36
  async (ctx, params) => {
45
37
  return { id: params.id, name: 'John Doe' }
46
38
  }
47
39
  )
48
40
 
49
- // Build the Express app (routes at /{name}/{version})
50
- const builder = new ExpressRPCAppBuilder()
51
- .register(RPC, (req) => ({ userId: req.headers['x-user-id'] as string }))
52
- .build()
53
-
54
- // Start the server
55
- builder.listen(3000, () => {
56
- console.log('RPC server running on http://localhost:3000')
57
- })
58
-
59
- // Route created: POST /users/get/1
60
- ```
61
-
62
- ### With Path Prefix
63
-
64
- ```typescript
65
- // Build with a custom path prefix (routes at /rpc/{name}/{version})
41
+ // Build the Express app
66
42
  const builder = new ExpressRPCAppBuilder({ pathPrefix: '/rpc' })
67
- .register(RPC, (req) => ({ userId: req.headers['x-user-id'] as string }))
68
- .build()
69
-
70
- // Route created: POST /rpc/users/get/1
71
- ```
72
-
73
- ## API Reference
43
+ .register(RPC, (req) => ({
44
+ userId: req.headers['x-user-id'] as string
45
+ }))
74
46
 
75
- ### `ExpressRPCAppBuilder`
47
+ const app = builder.build()
48
+ app.listen(3000)
76
49
 
77
- Builder class for creating an Express application with RPC routes.
50
+ // POST /rpc/users/profile/get-user/1 { id: "123", name: "John Doe" }
51
+ ```
78
52
 
79
- #### Constructor
53
+ ## Configuration
80
54
 
81
55
  ```typescript
82
- new ExpressRPCAppBuilder(config?: {
83
- app?: express.Express
84
- pathPrefix?: string
56
+ type ExpressRPCAppBuilderConfig = {
57
+ app?: express.Express // Existing Express app (optional)
58
+ pathPrefix?: string // Prefix for all routes (e.g., '/rpc/v1')
85
59
  onRequestStart?: (req: express.Request) => void
86
60
  onRequestEnd?: (req: express.Request, res: express.Response) => void
87
61
  onSuccess?: (procedure: TProcedureRegistration, req: express.Request, res: express.Response) => void
88
62
  error?: (procedure: TProcedureRegistration, req: express.Request, res: express.Response, error: Error) => void
89
- })
63
+ }
90
64
  ```
91
65
 
92
- | Option | Type | Description |
93
- |--------|------|-------------|
94
- | `app` | `express.Express` | Existing Express app to use. When provided, you must configure middleware (e.g., `express.json()`) yourself. If omitted, a new app with JSON middleware is created. |
95
- | `pathPrefix` | `string` | Optional path prefix for all RPC routes. When not specified, routes are created at `/{name}/{version}`. When specified (e.g., `/rpc`), routes are created at `/{pathPrefix}/{name}/{version}`. |
96
- | `onRequestStart` | `(req) => void` | Called at the start of each request. |
97
- | `onRequestEnd` | `(req, res) => void` | Called after the response finishes (via `res.on('finish')`). |
98
- | `onSuccess` | `(procedure, req, res) => void` | Called after successful procedure execution. |
99
- | `error` | `(procedure, req, res, error) => void` | Custom error handler. When provided, you control the response. |
100
-
101
- #### Methods
66
+ | Option | Type | Description |
67
+ |--------|------|------------------------------------------------------|
68
+ | `app` | `express.Express` | Use existing Express app instead of creating new one |
69
+ | `pathPrefix` | `string` | Prefix all routes (e.g., `/rpc/v1`) |
70
+ | `onRequestStart` | `(req) => void` | Called at start of each request |
71
+ | `onRequestEnd` | `(req, res) => void` | Called after response finishes |
72
+ | `onSuccess` | `(proc, req, res) => void` | Called on successful handler execution |
73
+ | `error` | `(proc, req, res, err) => void` | Custom error handler |
102
74
 
103
- ##### `register<C>(factory, factoryContext): this`
75
+ ## Context Resolution
104
76
 
105
- Registers a procedure factory with its context.
77
+ The context resolver receives the Express `Request` object:
106
78
 
107
79
  ```typescript
108
- // Direct value
109
- builder.register(RPC, { userId: 'system' })
110
-
111
- // Sync function
112
- builder.register(RPC, (req) => ({ userId: req.user.id }))
80
+ builder.register(RPC, (req: express.Request) => ({
81
+ userId: req.headers['x-user-id'] as string,
82
+ sessionId: req.cookies?.sessionId,
83
+ ip: req.ip
84
+ }))
113
85
 
114
- // Async function
86
+ // Async context resolution
115
87
  builder.register(RPC, async (req) => {
116
- const user = await getUser(req.headers.authorization)
117
- return { userId: user.id }
88
+ const token = req.headers.authorization?.replace('Bearer ', '')
89
+ const user = await verifyToken(token)
90
+ return { userId: user.id, roles: user.roles }
118
91
  })
119
92
  ```
120
93
 
121
- - **factory**: The procedure factory created by `Procedures<Context, RPCConfig>()`
122
- - **factoryContext**: The context for procedure handlers. Can be:
123
- - A direct value matching the factory's context type
124
- - A sync function `(req: express.Request) => Context`
125
- - An async function `(req: express.Request) => Promise<Context>`
126
- - **Returns**: `this` for method chaining
127
-
128
- ##### `build(): express.Application`
94
+ ## Error Handling
129
95
 
130
- Builds and returns the Express application with all registered RPC routes.
96
+ Custom error handler receives the procedure, request, response, and error:
131
97
 
132
98
  ```typescript
133
- const app = builder.build()
134
- app.listen(3000)
135
- ```
136
-
137
- #### Properties
138
-
139
- ##### `app: express.Express`
140
-
141
- The underlying Express application instance.
99
+ const builder = new ExpressRPCAppBuilder({
100
+ error: (procedure, req, res, error) => {
101
+ console.error(`Error in ${procedure.name}:`, error)
142
102
 
143
- ##### `docs: RPCHttpRouteDoc[]`
103
+ if (error instanceof ValidationError) {
104
+ res.status(400).json({ error: error.message, code: 'VALIDATION_ERROR' })
105
+ return
106
+ }
144
107
 
145
- Array of route documentation objects, populated after `build()` is called.
108
+ if (error instanceof AuthError) {
109
+ res.status(401).json({ error: 'Unauthorized', code: 'AUTH_ERROR' })
110
+ return
111
+ }
146
112
 
147
- ```typescript
148
- interface RPCHttpRouteDoc {
149
- path: string // e.g., '/users/get/1' or '/rpc/users/get/1' with pathPrefix
150
- method: 'post'
151
- jsonSchema: {
152
- body?: object // JSON Schema for request params
153
- response?: object // JSON Schema for return type
113
+ res.status(500).json({ error: 'Internal server error' })
154
114
  }
155
- }
115
+ })
156
116
  ```
157
117
 
158
- ### `makeRPCHttpRoutePath(config: RPCConfig): string`
159
-
160
- Generates the RPC route path from config. Exposed for testing/utilities.
118
+ **Default error handling:** Returns `{ error: message }` with status 500.
161
119
 
162
- ### `buildRpcHttpRouteDoc(procedure): RPCHttpRouteDoc`
120
+ ## Using Existing Express App
163
121
 
164
- Generates route documentation for a procedure. Exposed for testing/utilities.
165
-
166
- ## Route Path Generation
167
-
168
- Routes are generated at `/{name-segments}/{version}` (or `/{pathPrefix}/{name-segments}/{version}` when configured):
169
-
170
- | Config Name | Version | Generated Path (no prefix) | With `pathPrefix: '/rpc'` |
171
- |-------------|---------|---------------------------|---------------------------|
172
- | `'users'` | `1` | `/users/1` | `/rpc/users/1` |
173
- | `['users', 'get-by-id']` | `1` | `/users/get-by-id/1` | `/rpc/users/get-by-id/1` |
174
- | `'getUserById'` | `2` | `/get-user-by-id/2` | `/rpc/get-user-by-id/2` |
175
- | `'GetUserById'` | `1` | `/get-user-by-id/1` | `/rpc/get-user-by-id/1` |
176
-
177
- - Names are converted to kebab-case
178
- - Array names create nested path segments
179
- - Version is appended as the final segment (raw number, not `v1`)
180
- - The `pathPrefix` option adds a prefix to all routes when specified
181
-
182
- ## Multiple Factories
183
-
184
- Register multiple factories with different contexts:
122
+ When providing an existing Express app, **you must set up JSON parsing middleware**:
185
123
 
186
124
  ```typescript
187
- type PublicContext = { source: 'public' }
188
- type AuthContext = { source: 'auth'; userId: string }
189
-
190
- const PublicRPC = Procedures<PublicContext, RPCConfig>()
191
- const AuthRPC = Procedures<AuthContext, RPCConfig>()
192
-
193
- // Define public procedures
194
- PublicRPC.Create('HealthCheck', { name: 'health', version: 1 }, async () => ({ status: 'ok' }))
125
+ const app = express()
126
+ app.use(express.json()) // Required!
127
+ app.use(cors())
128
+ app.use(helmet())
195
129
 
196
- // Define authenticated procedures
197
- AuthRPC.Create('GetProfile', { name: ['users', 'profile'], version: 1 }, async (ctx) => ({
198
- userId: ctx.userId,
199
- }))
130
+ const builder = new ExpressRPCAppBuilder({ app })
131
+ .register(RPC, contextResolver)
200
132
 
201
- // Build with different factoryContext forms
202
- const app = new ExpressRPCAppBuilder()
203
- .register(PublicRPC, { source: 'public' }) // Direct value
204
- .register(AuthRPC, (req) => ({ // Sync function
205
- source: 'auth',
206
- userId: req.headers['x-user-id'] as string,
207
- }))
208
- .build()
133
+ builder.build() // Adds RPC routes to existing app
209
134
  ```
210
135
 
211
- ## Lifecycle Hooks
136
+ When no `app` is provided, `express.json()` middleware is added automatically.
212
137
 
213
- ```typescript
214
- const app = new ExpressRPCAppBuilder({
215
- onRequestStart: (req) => {
216
- console.log(`[${req.method}] ${req.path}`)
217
- },
138
+ ## API Reference
218
139
 
219
- onRequestEnd: (req, res) => {
220
- console.log(`Response: ${res.statusCode}`)
221
- },
140
+ ### Constructor
222
141
 
223
- onSuccess: (procedure, req, res) => {
224
- console.log(`Procedure ${procedure.name} succeeded`)
225
- },
226
- })
142
+ ```typescript
143
+ new ExpressRPCAppBuilder(config?: ExpressRPCAppBuilderConfig)
227
144
  ```
228
145
 
229
- **Execution order:** `onRequestStart` → handler → `onSuccess` → `onRequestEnd`
230
-
231
- Note: `onSuccess` is only called on successful execution. It is NOT called when the handler throws.
232
-
233
- ## Error Handling
234
-
235
- ### Custom Error Handler
146
+ ### Methods
236
147
 
237
- ```typescript
238
- const app = new ExpressRPCAppBuilder({
239
- error: (procedure, req, res, error) => {
240
- console.error(`Error in ${procedure.name}:`, error.message)
241
-
242
- if (error instanceof ProcedureValidationError) {
243
- res.status(400).json({ error: 'Validation failed', details: error.errors })
244
- } else if (error instanceof ProcedureError) {
245
- res.status(422).json({ error: error.message })
246
- } else {
247
- res.status(500).json({ error: 'Internal server error' })
248
- }
249
- },
250
- })
251
- ```
148
+ | Method | Signature | Description |
149
+ |--------|-----------|-------------|
150
+ | `register` | `register<T>(factory, context): this` | Register procedure factory with context |
151
+ | `build` | `build(): express.Application` | Build routes and return app |
152
+ | `makeRPCHttpRoutePath` | `makeRPCHttpRoutePath(config: RPCConfig): string` | Generate route path |
252
153
 
253
- ### Default Error Handling
154
+ ### Static Methods
254
155
 
255
- Without a custom error handler, errors return a JSON response with the error message:
156
+ | Method | Signature | Description |
157
+ |--------|-----------|-------------|
158
+ | `makeRPCHttpRoutePath` | `static makeRPCHttpRoutePath({ config, prefix }): string` | Generate route path with custom prefix |
256
159
 
257
- ```json
258
- { "error": "Error message here" }
259
- ```
160
+ ### Properties
260
161
 
261
- ## Route Documentation
162
+ | Property | Type | Description |
163
+ |----------|------|-------------|
164
+ | `app` | `express.Express` | The Express application instance |
165
+ | `docs` | `RPCHttpRouteDoc[]` | Route documentation (after `build()`) |
166
+ | `config` | `ExpressRPCAppBuilderConfig` | The configuration object |
262
167
 
263
- Access generated documentation after building:
168
+ ## TypeScript Types
264
169
 
265
170
  ```typescript
266
- const builder = new ExpressRPCAppBuilder()
267
- builder.register(RPC, factoryContext)
268
- builder.build()
269
-
270
- // Documentation is now available
271
- console.log(builder.docs)
272
- // [
273
- // {
274
- // path: '/users/get/1',
275
- // method: 'post',
276
- // jsonSchema: {
277
- // body: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] },
278
- // response: { type: 'object', properties: { id: { type: 'string' }, name: { type: 'string' } } }
279
- // }
280
- // }
281
- // ]
171
+ import {
172
+ ExpressRPCAppBuilder,
173
+ ExpressRPCAppBuilderConfig,
174
+ RPCConfig,
175
+ RPCHttpRouteDoc
176
+ } from 'ts-procedures/implementations/http/express-rpc'
282
177
  ```
283
178
 
284
- Use `docs` to generate OpenAPI specs, API documentation, or client SDKs.
285
-
286
- ## Using an Existing Express App
179
+ ## Full Example
287
180
 
288
181
  ```typescript
289
182
  import express from 'express'
183
+ import { Procedures } from 'ts-procedures'
184
+ import { ExpressRPCAppBuilder, RPCConfig } from 'ts-procedures/implementations/http/express-rpc'
185
+ import { v } from 'suretype'
290
186
 
291
- const app = express()
292
-
293
- // Add your own middleware
294
- app.use(express.json())
295
- app.use(cors())
296
- app.use(helmet())
297
-
298
- // Mount RPC routes
299
- const rpcApp = new ExpressRPCAppBuilder({ app })
300
- .register(RPC, factoryContext)
301
- .build()
302
-
303
- // Add other routes
304
- app.get('/health', (req, res) => res.json({ status: 'ok' }))
187
+ // Context types
188
+ type PublicContext = { source: 'public' }
189
+ type AuthContext = { source: 'auth'; userId: string }
305
190
 
306
- app.listen(3000)
307
- ```
191
+ // Create factories
192
+ const PublicRPC = Procedures<PublicContext, RPCConfig>()
193
+ const AuthRPC = Procedures<AuthContext, RPCConfig>()
308
194
 
309
- **Important:** When providing your own Express app, you must set up middleware like `express.json()` yourself.
195
+ // Public procedures
196
+ PublicRPC.Create('HealthCheck', { scope: 'health', version: 1 }, async () => ({
197
+ status: 'ok'
198
+ }))
310
199
 
311
- ## Types
200
+ PublicRPC.Create('GetVersion', { scope: ['system', 'version'], version: 1 }, async () => ({
201
+ version: '1.0.0'
202
+ }))
312
203
 
313
- ```typescript
314
- import type { RPCConfig, RPCHttpRouteDoc } from 'ts-procedures/express-rpc'
204
+ // Authenticated procedures
205
+ AuthRPC.Create(
206
+ 'GetProfile',
207
+ {
208
+ scope: ['users', 'profile'],
209
+ version: 1,
210
+ schema: { returnType: v.object({ userId: v.string() }) }
211
+ },
212
+ async (ctx) => ({ userId: ctx.userId })
213
+ )
315
214
 
316
- // RPCConfig - Required config shape for procedures
317
- interface RPCConfig {
318
- name: string | string[]
319
- version: number
320
- }
215
+ AuthRPC.Create(
216
+ 'UpdateProfile',
217
+ {
218
+ scope: ['users', 'profile'],
219
+ version: 2,
220
+ schema: { params: v.object({ name: v.string() }) }
221
+ },
222
+ async (ctx, params) => ({ userId: ctx.userId, name: params.name })
223
+ )
321
224
 
322
- // RPCHttpRouteDoc - Route documentation
323
- interface RPCHttpRouteDoc {
324
- path: string
325
- method: 'post'
326
- jsonSchema: {
327
- body?: object
328
- response?: object
225
+ // Build app
226
+ const builder = new ExpressRPCAppBuilder({
227
+ pathPrefix: '/rpc',
228
+ onRequestStart: (req) => console.log(`→ ${req.method} ${req.path}`),
229
+ onRequestEnd: (req, res) => console.log(`← ${res.statusCode}`),
230
+ onSuccess: (proc) => console.log(`✓ ${proc.name}`),
231
+ error: (proc, req, res, err) => {
232
+ console.error(`✗ ${proc.name}:`, err.message)
233
+ res.status(500).json({ error: err.message })
329
234
  }
330
- }
331
- ```
235
+ })
332
236
 
333
- ## HTTP Method
237
+ builder
238
+ .register(PublicRPC, () => ({ source: 'public' as const }))
239
+ .register(AuthRPC, (req) => ({
240
+ source: 'auth' as const,
241
+ userId: req.headers['x-user-id'] as string || 'anonymous'
242
+ }))
334
243
 
335
- All RPC routes use **POST** method only. GET requests to RPC paths return 404.
244
+ const app = builder.build()
336
245
 
337
- ```bash
338
- # Works
339
- curl -X POST http://localhost:3000/users/get/1 \
340
- -H "Content-Type: application/json" \
341
- -d '{"id": "123"}'
246
+ // Generated routes:
247
+ // POST /rpc/health/1
248
+ // POST /rpc/system/version/get-version/1
249
+ // POST /rpc/users/profile/get-user/1
250
+ // POST /rpc/users/profile/get-user/2
342
251
 
343
- # Returns 404
344
- curl http://localhost:3000/users/get/1
252
+ console.log('Routes:', builder.docs.map(d => d.path))
253
+ app.listen(3000)
345
254
  ```