ts-procedures 2.1.0 → 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 (70) 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.js +40 -0
  5. package/build/errors.test.js.map +1 -0
  6. package/build/implementations/http/express-rpc/index.d.ts +36 -35
  7. package/build/implementations/http/express-rpc/index.js +29 -13
  8. package/build/implementations/http/express-rpc/index.js.map +1 -1
  9. package/build/implementations/http/express-rpc/index.test.js +146 -92
  10. package/build/implementations/http/express-rpc/index.test.js.map +1 -1
  11. package/build/implementations/http/hono-rpc/index.d.ts +83 -0
  12. package/build/implementations/http/hono-rpc/index.js +148 -0
  13. package/build/implementations/http/hono-rpc/index.js.map +1 -0
  14. package/build/implementations/http/hono-rpc/index.test.js +647 -0
  15. package/build/implementations/http/hono-rpc/index.test.js.map +1 -0
  16. package/build/implementations/http/hono-rpc/types.d.ts +28 -0
  17. package/build/implementations/http/hono-rpc/types.js.map +1 -0
  18. package/build/implementations/types.d.ts +1 -1
  19. package/build/index.d.ts +12 -0
  20. package/build/index.js +29 -7
  21. package/build/index.js.map +1 -1
  22. package/build/index.test.js +65 -0
  23. package/build/index.test.js.map +1 -1
  24. package/build/schema/parser.js +3 -0
  25. package/build/schema/parser.js.map +1 -1
  26. package/build/schema/parser.test.js +18 -0
  27. package/build/schema/parser.test.js.map +1 -1
  28. package/package.json +8 -2
  29. package/src/errors.test.ts +53 -0
  30. package/src/errors.ts +4 -2
  31. package/src/implementations/http/README.md +172 -0
  32. package/src/implementations/http/express-rpc/README.md +151 -228
  33. package/src/implementations/http/express-rpc/index.test.ts +167 -93
  34. package/src/implementations/http/express-rpc/index.ts +67 -38
  35. package/src/implementations/http/hono-rpc/README.md +293 -0
  36. package/src/implementations/http/hono-rpc/index.test.ts +847 -0
  37. package/src/implementations/http/hono-rpc/index.ts +202 -0
  38. package/src/implementations/http/hono-rpc/types.ts +33 -0
  39. package/src/implementations/types.ts +2 -1
  40. package/src/index.test.ts +83 -0
  41. package/src/index.ts +34 -8
  42. package/src/schema/parser.test.ts +26 -0
  43. package/src/schema/parser.ts +5 -1
  44. package/build/implementations/http/client/index.js +0 -2
  45. package/build/implementations/http/client/index.js.map +0 -1
  46. package/build/implementations/http/express/example/factories.d.ts +0 -97
  47. package/build/implementations/http/express/example/factories.js +0 -4
  48. package/build/implementations/http/express/example/factories.js.map +0 -1
  49. package/build/implementations/http/express/example/procedures/auth.d.ts +0 -1
  50. package/build/implementations/http/express/example/procedures/auth.js +0 -22
  51. package/build/implementations/http/express/example/procedures/auth.js.map +0 -1
  52. package/build/implementations/http/express/example/procedures/users.d.ts +0 -1
  53. package/build/implementations/http/express/example/procedures/users.js +0 -30
  54. package/build/implementations/http/express/example/procedures/users.js.map +0 -1
  55. package/build/implementations/http/express/example/server.d.ts +0 -3
  56. package/build/implementations/http/express/example/server.js +0 -49
  57. package/build/implementations/http/express/example/server.js.map +0 -1
  58. package/build/implementations/http/express/example/server.test.d.ts +0 -1
  59. package/build/implementations/http/express/example/server.test.js +0 -110
  60. package/build/implementations/http/express/example/server.test.js.map +0 -1
  61. package/build/implementations/http/express/index.d.ts +0 -35
  62. package/build/implementations/http/express/index.js +0 -75
  63. package/build/implementations/http/express/index.js.map +0 -1
  64. package/build/implementations/http/express/index.test.js +0 -329
  65. package/build/implementations/http/express/index.test.js.map +0 -1
  66. package/build/implementations/http/express/types.d.ts +0 -17
  67. package/build/implementations/http/express/types.js.map +0 -1
  68. /package/build/{implementations/http/client/index.d.ts → errors.test.d.ts} +0 -0
  69. /package/build/implementations/http/{express → hono-rpc}/index.test.d.ts +0 -0
  70. /package/build/implementations/http/{express → hono-rpc}/types.js +0 -0
@@ -10,7 +10,7 @@ import { RPCConfig } from '../../types.js'
10
10
  * ExpressRPCAppBuilder Test Suite
11
11
  *
12
12
  * Tests the RPC-style Express integration for ts-procedures.
13
- * This builder creates POST routes at `/rpc/{name}/{version}` paths.
13
+ * This builder creates POST routes at `/{name}/{version}` paths (with optional pathPrefix).
14
14
  */
15
15
  describe('ExpressRPCAppBuilder', () => {
16
16
  // --------------------------------------------------------------------------
@@ -23,7 +23,7 @@ describe('ExpressRPCAppBuilder', () => {
23
23
 
24
24
  RPC.Create(
25
25
  'Echo',
26
- { name: 'echo', version: 1, schema: { params: v.object({ message: v.string() }) } },
26
+ { scope: 'echo', version: 1, schema: { params: v.object({ message: v.string() }) } },
27
27
  async (ctx, params) => params
28
28
  )
29
29
 
@@ -31,7 +31,7 @@ describe('ExpressRPCAppBuilder', () => {
31
31
  const app = builder.build()
32
32
 
33
33
  // JSON body should be parsed automatically
34
- const res = await request(app).post('/rpc/echo/1').send({ message: 'hello' })
34
+ const res = await request(app).post('/echo/echo/1').send({ message: 'hello' })
35
35
 
36
36
  expect(res.status).toBe(200)
37
37
  expect(res.body).toEqual({ message: 'hello' })
@@ -45,7 +45,7 @@ describe('ExpressRPCAppBuilder', () => {
45
45
 
46
46
  RPC.Create(
47
47
  'Echo',
48
- { name: 'echo', version: 1, schema: { params: v.object({ message: v.string() }) } },
48
+ { scope: 'echo', version: 1, schema: { params: v.object({ message: v.string() }) } },
49
49
  async (ctx, params) => ({ received: params })
50
50
  )
51
51
 
@@ -54,7 +54,7 @@ describe('ExpressRPCAppBuilder', () => {
54
54
 
55
55
  // Without json middleware, body won't be parsed (req.body is undefined)
56
56
  const res = await request(app)
57
- .post('/rpc/echo/1')
57
+ .post('/echo/echo/1')
58
58
  .set('Content-Type', 'application/json')
59
59
  .send(JSON.stringify({ message: 'hello' }))
60
60
 
@@ -76,6 +76,77 @@ describe('ExpressRPCAppBuilder', () => {
76
76
  })
77
77
  })
78
78
 
79
+ // --------------------------------------------------------------------------
80
+ // pathPrefix Option Tests
81
+ // --------------------------------------------------------------------------
82
+ describe('pathPrefix option', () => {
83
+ test('uses custom pathPrefix for all routes', async () => {
84
+ const builder = new ExpressRPCAppBuilder({ pathPrefix: '/api/v1' })
85
+ const RPC = Procedures<{}, RPCConfig>()
86
+
87
+ RPC.Create('Test', { scope: 'test', version: 1 }, async () => ({ ok: true }))
88
+
89
+ builder.register(RPC, () => ({}))
90
+ const app = builder.build()
91
+
92
+ const res = await request(app).post('/api/v1/test/test/1').send({})
93
+ expect(res.status).toBe(200)
94
+ expect(res.body).toEqual({ ok: true })
95
+ })
96
+
97
+ test('pathPrefix without leading slash gets normalized', async () => {
98
+ const builder = new ExpressRPCAppBuilder({ pathPrefix: 'custom' })
99
+ const RPC = Procedures<{}, RPCConfig>()
100
+
101
+ RPC.Create('Test', { scope: 'test', version: 1 }, async () => ({ ok: true }))
102
+
103
+ builder.register(RPC, () => ({}))
104
+ const app = builder.build()
105
+
106
+ const res = await request(app).post('/custom/test/test/1').send({})
107
+ expect(res.status).toBe(200)
108
+ })
109
+
110
+ test('no prefix when pathPrefix not specified', async () => {
111
+ const builder = new ExpressRPCAppBuilder()
112
+ const RPC = Procedures<{}, RPCConfig>()
113
+
114
+ RPC.Create('Test', { scope: 'test', version: 1 }, async () => ({ ok: true }))
115
+
116
+ builder.register(RPC, () => ({}))
117
+ const app = builder.build()
118
+
119
+ const res = await request(app).post('/test/test/1').send({})
120
+ expect(res.status).toBe(200)
121
+ })
122
+
123
+ test('pathPrefix appears in generated docs', () => {
124
+ const builder = new ExpressRPCAppBuilder({ pathPrefix: '/api' })
125
+ const RPC = Procedures<{}, RPCConfig>()
126
+
127
+ RPC.Create('Test', { scope: 'test', version: 1 }, async () => ({}))
128
+
129
+ builder.register(RPC, () => ({}))
130
+ builder.build()
131
+
132
+ expect(builder.docs[0]!.path).toBe('/api/test/test/1')
133
+ })
134
+
135
+ test('pathPrefix /rpc restores original behavior', async () => {
136
+ const builder = new ExpressRPCAppBuilder({ pathPrefix: '/rpc' })
137
+ const RPC = Procedures<{}, RPCConfig>()
138
+
139
+ RPC.Create('Users', { scope: 'users', version: 1 }, async () => ({ users: [] }))
140
+
141
+ builder.register(RPC, () => ({}))
142
+ const app = builder.build()
143
+
144
+ const res = await request(app).post('/rpc/users/users/1').send({})
145
+ expect(res.status).toBe(200)
146
+ expect(builder.docs[0]!.path).toBe('/rpc/users/users/1')
147
+ })
148
+ })
149
+
79
150
  // --------------------------------------------------------------------------
80
151
  // Lifecycle Hooks Tests
81
152
  // --------------------------------------------------------------------------
@@ -85,16 +156,16 @@ describe('ExpressRPCAppBuilder', () => {
85
156
  const builder = new ExpressRPCAppBuilder({ onRequestStart })
86
157
  const RPC = Procedures<{}, RPCConfig>()
87
158
 
88
- RPC.Create('Test', { name: 'test', version: 1 }, async () => ({ ok: true }))
159
+ RPC.Create('Test', { scope: 'test', version: 1 }, async () => ({ ok: true }))
89
160
 
90
161
  builder.register(RPC, () => ({}))
91
162
  const app = builder.build()
92
163
 
93
- await request(app).post('/rpc/test/1').send({})
164
+ await request(app).post('/test/test/1').send({})
94
165
 
95
166
  expect(onRequestStart).toHaveBeenCalledTimes(1)
96
167
  expect(onRequestStart.mock.calls[0]![0]).toHaveProperty('method', 'POST')
97
- expect(onRequestStart.mock.calls[0]![0]).toHaveProperty('path', '/rpc/test/1')
168
+ expect(onRequestStart.mock.calls[0]![0]).toHaveProperty('path', '/test/test/1')
98
169
  })
99
170
 
100
171
  test('onRequestEnd is called after response finishes', async () => {
@@ -102,12 +173,12 @@ describe('ExpressRPCAppBuilder', () => {
102
173
  const builder = new ExpressRPCAppBuilder({ onRequestEnd })
103
174
  const RPC = Procedures<{}, RPCConfig>()
104
175
 
105
- RPC.Create('Test', { name: 'test', version: 1 }, async () => ({ ok: true }))
176
+ RPC.Create('Test', { scope: 'test', version: 1 }, async () => ({ ok: true }))
106
177
 
107
178
  builder.register(RPC, () => ({}))
108
179
  const app = builder.build()
109
180
 
110
- await request(app).post('/rpc/test/1').send({})
181
+ await request(app).post('/test/test/1').send({})
111
182
 
112
183
  expect(onRequestEnd).toHaveBeenCalledTimes(1)
113
184
  expect(onRequestEnd.mock.calls[0]![0]).toHaveProperty('method', 'POST')
@@ -119,12 +190,12 @@ describe('ExpressRPCAppBuilder', () => {
119
190
  const builder = new ExpressRPCAppBuilder({ onSuccess })
120
191
  const RPC = Procedures<{}, RPCConfig>()
121
192
 
122
- RPC.Create('Test', { name: 'test', version: 1 }, async () => ({ ok: true }))
193
+ RPC.Create('Test', { scope: 'test', version: 1 }, async () => ({ ok: true }))
123
194
 
124
195
  builder.register(RPC, () => ({}))
125
196
  const app = builder.build()
126
197
 
127
- await request(app).post('/rpc/test/1').send({})
198
+ await request(app).post('/test/test/1').send({})
128
199
 
129
200
  expect(onSuccess).toHaveBeenCalledTimes(1)
130
201
  expect(onSuccess.mock.calls[0]![0]).toHaveProperty('name', 'Test')
@@ -135,14 +206,14 @@ describe('ExpressRPCAppBuilder', () => {
135
206
  const builder = new ExpressRPCAppBuilder({ onSuccess })
136
207
  const RPC = Procedures<{}, RPCConfig>()
137
208
 
138
- RPC.Create('Test', { name: 'test', version: 1 }, async () => {
209
+ RPC.Create('Test', { scope: 'test', version: 1 }, async () => {
139
210
  throw new Error('Handler error')
140
211
  })
141
212
 
142
213
  builder.register(RPC, () => ({}))
143
214
  const app = builder.build()
144
215
 
145
- await request(app).post('/rpc/test/1').send({})
216
+ await request(app).post('/test/test/1').send({})
146
217
 
147
218
  expect(onSuccess).not.toHaveBeenCalled()
148
219
  })
@@ -157,7 +228,7 @@ describe('ExpressRPCAppBuilder', () => {
157
228
  })
158
229
  const RPC = Procedures<{}, RPCConfig>()
159
230
 
160
- RPC.Create('Test', { name: 'test', version: 1 }, async () => {
231
+ RPC.Create('Test', { scope: 'test', version: 1 }, async () => {
161
232
  order.push('handler')
162
233
  return { ok: true }
163
234
  })
@@ -165,7 +236,7 @@ describe('ExpressRPCAppBuilder', () => {
165
236
  builder.register(RPC, () => ({}))
166
237
  const app = builder.build()
167
238
 
168
- await request(app).post('/rpc/test/1').send({})
239
+ await request(app).post('/test/test/1').send({})
169
240
 
170
241
  expect(order).toEqual(['start', 'handler', 'success', 'end'])
171
242
  })
@@ -183,14 +254,14 @@ describe('ExpressRPCAppBuilder', () => {
183
254
  const builder = new ExpressRPCAppBuilder({ error: errorHandler })
184
255
  const RPC = Procedures<{}, RPCConfig>()
185
256
 
186
- RPC.Create('Test', { name: 'test', version: 1 }, async () => {
257
+ RPC.Create('Test', { scope: 'test', version: 1 }, async () => {
187
258
  throw new Error('Test error')
188
259
  })
189
260
 
190
261
  builder.register(RPC, () => ({}))
191
262
  const app = builder.build()
192
263
 
193
- const res = await request(app).post('/rpc/test/1').send({})
264
+ const res = await request(app).post('/test/test/1').send({})
194
265
 
195
266
  expect(errorHandler).toHaveBeenCalledTimes(1)
196
267
  expect(errorHandler.mock.calls[0]![0]).toHaveProperty('name', 'Test')
@@ -204,14 +275,14 @@ describe('ExpressRPCAppBuilder', () => {
204
275
  const builder = new ExpressRPCAppBuilder()
205
276
  const RPC = Procedures<{}, RPCConfig>()
206
277
 
207
- RPC.Create('Test', { name: 'test', version: 1 }, async () => {
278
+ RPC.Create('Test', { scope: 'test', version: 1 }, async () => {
208
279
  throw new Error('Something went wrong')
209
280
  })
210
281
 
211
282
  builder.register(RPC, () => ({}))
212
283
  const app = builder.build()
213
284
 
214
- const res = await request(app).post('/rpc/test/1').send({})
285
+ const res = await request(app).post('/test/test/1').send({})
215
286
 
216
287
  // Default error handler returns error message in JSON body
217
288
  expect(res.body).toHaveProperty('error')
@@ -222,7 +293,7 @@ describe('ExpressRPCAppBuilder', () => {
222
293
  const builder = new ExpressRPCAppBuilder()
223
294
  const RPC = Procedures<{}, RPCConfig>()
224
295
 
225
- RPC.Create('Test', { name: 'test', version: 1 }, async () => {
296
+ RPC.Create('Test', { scope: 'test', version: 1 }, async () => {
226
297
  // Simulate unhandled exception
227
298
  const obj: any = null
228
299
  return obj.property // This will throw
@@ -231,7 +302,7 @@ describe('ExpressRPCAppBuilder', () => {
231
302
  builder.register(RPC, () => ({}))
232
303
  const app = builder.build()
233
304
 
234
- const res = await request(app).post('/rpc/test/1').send({})
305
+ const res = await request(app).post('/test/test/1').send({})
235
306
 
236
307
  // Unhandled exceptions are caught and returned as error response
237
308
  expect(res.body).toHaveProperty('error')
@@ -262,11 +333,11 @@ describe('ExpressRPCAppBuilder', () => {
262
333
  const PublicRPC = Procedures<{ public: true }, RPCConfig>()
263
334
  const PrivateRPC = Procedures<{ private: true }, RPCConfig>()
264
335
 
265
- PublicRPC.Create('PublicMethod', { name: 'public', version: 1 }, async (ctx) => ({
336
+ PublicRPC.Create('PublicMethod', { scope: 'public', version: 1 }, async (ctx) => ({
266
337
  isPublic: ctx.public,
267
338
  }))
268
339
 
269
- PrivateRPC.Create('PrivateMethod', { name: 'private', version: 1 }, async (ctx) => ({
340
+ PrivateRPC.Create('PrivateMethod', { scope: 'private', version: 1 }, async (ctx) => ({
270
341
  isPrivate: ctx.private,
271
342
  }))
272
343
 
@@ -276,8 +347,8 @@ describe('ExpressRPCAppBuilder', () => {
276
347
 
277
348
  const app = builder.build()
278
349
 
279
- const publicRes = await request(app).post('/rpc/public/1').send({})
280
- const privateRes = await request(app).post('/rpc/private/1').send({})
350
+ const publicRes = await request(app).post('/public/public-method/1').send({})
351
+ const privateRes = await request(app).post('/private/private-method/1').send({})
281
352
 
282
353
  expect(publicRes.body).toEqual({ isPublic: true })
283
354
  expect(privateRes.body).toEqual({ isPrivate: true })
@@ -289,14 +360,14 @@ describe('ExpressRPCAppBuilder', () => {
289
360
  const builder = new ExpressRPCAppBuilder()
290
361
  const RPC = Procedures<{ requestId: string }, RPCConfig>()
291
362
 
292
- RPC.Create('GetRequestId', { name: 'get-request-id', version: 1 }, async (ctx) => ({
363
+ RPC.Create('GetRequestId', { scope: 'get-request-id', version: 1 }, async (ctx) => ({
293
364
  id: ctx.requestId,
294
365
  }))
295
366
 
296
367
  builder.register(RPC, factoryContext)
297
368
  const app = builder.build()
298
369
 
299
- const res = await request(app).post('/rpc/get-request-id/1').send({})
370
+ const res = await request(app).post('/get-request-id/get-request-id/1').send({})
300
371
 
301
372
  expect(res.body).toEqual({ id: 'req-123' })
302
373
  })
@@ -309,14 +380,14 @@ describe('ExpressRPCAppBuilder', () => {
309
380
  const builder = new ExpressRPCAppBuilder()
310
381
  const RPC = Procedures<{ requestId: string }, RPCConfig>()
311
382
 
312
- RPC.Create('GetRequestId', { name: 'get-request-id', version: 1 }, async (ctx) => ({
383
+ RPC.Create('GetRequestId', { scope: 'get-request-id', version: 1 }, async (ctx) => ({
313
384
  id: ctx.requestId,
314
385
  }))
315
386
 
316
387
  builder.register(RPC, factoryContext)
317
388
  const app = builder.build()
318
389
 
319
- await request(app).post('/rpc/get-request-id/1').send({})
390
+ await request(app).post('/get-request-id/get-request-id/1').send({})
320
391
 
321
392
  expect(factoryContext).toHaveBeenCalledTimes(1)
322
393
  })
@@ -329,14 +400,14 @@ describe('ExpressRPCAppBuilder', () => {
329
400
  const builder = new ExpressRPCAppBuilder()
330
401
  const RPC = Procedures<{ authHeader?: string }, RPCConfig>()
331
402
 
332
- RPC.Create('GetAuth', { name: 'get-auth', version: 1 }, async (ctx) => ({
403
+ RPC.Create('GetAuth', { scope: 'get-auth', version: 1 }, async (ctx) => ({
333
404
  auth: ctx.authHeader,
334
405
  }))
335
406
 
336
407
  builder.register(RPC, factoryContext)
337
408
  const app = builder.build()
338
409
 
339
- await request(app).post('/rpc/get-auth/1').set('Authorization', 'Bearer token123').send({})
410
+ await request(app).post('/get-auth/get-auth/1').set('Authorization', 'Bearer token123').send({})
340
411
 
341
412
  expect(factoryContext).toHaveBeenCalledTimes(1)
342
413
  expect(factoryContext.mock.calls[0]![0]).toHaveProperty('headers')
@@ -355,14 +426,14 @@ describe('ExpressRPCAppBuilder', () => {
355
426
  const builder = new ExpressRPCAppBuilder()
356
427
  const RPC = Procedures<{}, RPCConfig>()
357
428
 
358
- RPC.Create('MethodOne', { name: 'method-one', version: 1 }, async () => ({ m: 1 }))
359
- RPC.Create('MethodTwo', { name: 'method-two', version: 2 }, async () => ({ m: 2 }))
429
+ RPC.Create('MethodOne', { scope: 'method-one', version: 1 }, async () => ({ m: 1 }))
430
+ RPC.Create('MethodTwo', { scope: 'method-two', version: 2 }, async () => ({ m: 2 }))
360
431
 
361
432
  builder.register(RPC, () => ({}))
362
433
  const app = builder.build()
363
434
 
364
- const res1 = await request(app).post('/rpc/method-one/1').send({})
365
- const res2 = await request(app).post('/rpc/method-two/2').send({})
435
+ const res1 = await request(app).post('/method-one/method-one/1').send({})
436
+ const res2 = await request(app).post('/method-two/method-two/2').send({})
366
437
 
367
438
  expect(res1.status).toBe(200)
368
439
  expect(res2.status).toBe(200)
@@ -382,8 +453,8 @@ describe('ExpressRPCAppBuilder', () => {
382
453
  const builder = new ExpressRPCAppBuilder()
383
454
  const RPC = Procedures<{}, RPCConfig>()
384
455
 
385
- RPC.Create('MethodOne', { name: 'method-one', version: 1 }, async () => ({}))
386
- RPC.Create('MethodTwo', { name: ['nested', 'method'], version: 2 }, async () => ({}))
456
+ RPC.Create('MethodOne', { scope: 'method-one', version: 1 }, async () => ({}))
457
+ RPC.Create('MethodTwo', { scope: ['nested', 'method'], version: 2 }, async () => ({}))
387
458
 
388
459
  expect(builder.docs).toHaveLength(0)
389
460
 
@@ -391,8 +462,8 @@ describe('ExpressRPCAppBuilder', () => {
391
462
  builder.build()
392
463
 
393
464
  expect(builder.docs).toHaveLength(2)
394
- expect(builder.docs[0]!.path).toBe('/rpc/method-one/1')
395
- expect(builder.docs[1]!.path).toBe('/rpc/nested/method/2')
465
+ expect(builder.docs[0]!.path).toBe('/method-one/method-one/1')
466
+ expect(builder.docs[1]!.path).toBe('/nested/method/method-two/2')
396
467
  })
397
468
 
398
469
  test('passes request body to handler as params', async () => {
@@ -401,14 +472,14 @@ describe('ExpressRPCAppBuilder', () => {
401
472
 
402
473
  RPC.Create(
403
474
  'Echo',
404
- { name: 'echo', version: 1, schema: { params: v.object({ data: v.string() }) } },
475
+ { scope: 'echo', version: 1, schema: { params: v.object({ data: v.string() }) } },
405
476
  async (ctx, params) => ({ received: params.data })
406
477
  )
407
478
 
408
479
  builder.register(RPC, () => ({}))
409
480
  const app = builder.build()
410
481
 
411
- const res = await request(app).post('/rpc/echo/1').send({ data: 'test-data' })
482
+ const res = await request(app).post('/echo/echo/1').send({ data: 'test-data' })
412
483
 
413
484
  expect(res.body).toEqual({ received: 'test-data' })
414
485
  })
@@ -417,12 +488,12 @@ describe('ExpressRPCAppBuilder', () => {
417
488
  const builder = new ExpressRPCAppBuilder()
418
489
  const RPC = Procedures<{}, RPCConfig>()
419
490
 
420
- RPC.Create('Test', { name: 'test', version: 1 }, async () => ({ ok: true }))
491
+ RPC.Create('Test', { scope: 'test', version: 1 }, async () => ({ ok: true }))
421
492
 
422
493
  builder.register(RPC, () => ({}))
423
494
  const app = builder.build()
424
495
 
425
- const res = await request(app).get('/rpc/test/1')
496
+ const res = await request(app).get('/test/test/1')
426
497
 
427
498
  expect(res.status).toBe(404)
428
499
  })
@@ -438,42 +509,42 @@ describe('ExpressRPCAppBuilder', () => {
438
509
  builder = new ExpressRPCAppBuilder()
439
510
  })
440
511
 
441
- test("simple string: 'users' → /rpc/users/1", () => {
442
- const path = builder.makeRPCHttpRoutePath({ name: 'users', version: 1 })
443
- expect(path).toBe('/rpc/users/1')
512
+ test("simple scope with procedure name: 'users' + 'GetUser' → /users/get-user/1", () => {
513
+ const path = builder.makeRPCHttpRoutePath('GetUser', { scope: 'users', version: 1 })
514
+ expect(path).toBe('/users/get-user/1')
444
515
  })
445
516
 
446
- test("array name: ['users', 'get-by-id'] → /rpc/users/get-by-id/1", () => {
447
- const path = builder.makeRPCHttpRoutePath({ name: ['users', 'get-by-id'], version: 1 })
448
- expect(path).toBe('/rpc/users/get-by-id/1')
517
+ test("array scope with procedure name: ['users', 'profile'] + 'GetById' → /users/profile/get-by-id/1", () => {
518
+ const path = builder.makeRPCHttpRoutePath('GetById', { scope: ['users', 'profile'], version: 1 })
519
+ expect(path).toBe('/users/profile/get-by-id/1')
449
520
  })
450
521
 
451
- test("camelCase: 'getUserById' → /rpc/get-user-by-id/1", () => {
452
- const path = builder.makeRPCHttpRoutePath({ name: 'getUserById', version: 1 })
453
- expect(path).toBe('/rpc/get-user-by-id/1')
522
+ test("camelCase procedure name: 'users' + 'getProfile' → /users/get-profile/1", () => {
523
+ const path = builder.makeRPCHttpRoutePath('getProfile', { scope: 'users', version: 1 })
524
+ expect(path).toBe('/users/get-profile/1')
454
525
  })
455
526
 
456
- test("PascalCase: 'GetUserById' → /rpc/get-user-by-id/1", () => {
457
- const path = builder.makeRPCHttpRoutePath({ name: 'GetUserById', version: 1 })
458
- expect(path).toBe('/rpc/get-user-by-id/1')
527
+ test("PascalCase procedure name: 'users' + 'UpdateProfile' → /users/update-profile/1", () => {
528
+ const path = builder.makeRPCHttpRoutePath('UpdateProfile', { scope: 'users', version: 1 })
529
+ expect(path).toBe('/users/update-profile/1')
459
530
  })
460
531
 
461
532
  test('version number included in path', () => {
462
- const pathV1 = builder.makeRPCHttpRoutePath({ name: 'test', version: 1 })
463
- const pathV2 = builder.makeRPCHttpRoutePath({ name: 'test', version: 2 })
464
- const pathV99 = builder.makeRPCHttpRoutePath({ name: 'test', version: 99 })
533
+ const pathV1 = builder.makeRPCHttpRoutePath('Test', { scope: 'test', version: 1 })
534
+ const pathV2 = builder.makeRPCHttpRoutePath('Test', { scope: 'test', version: 2 })
535
+ const pathV99 = builder.makeRPCHttpRoutePath('Test', { scope: 'test', version: 99 })
465
536
 
466
- expect(pathV1).toBe('/rpc/test/1')
467
- expect(pathV2).toBe('/rpc/test/2')
468
- expect(pathV99).toBe('/rpc/test/99')
537
+ expect(pathV1).toBe('/test/test/1')
538
+ expect(pathV2).toBe('/test/test/2')
539
+ expect(pathV99).toBe('/test/test/99')
469
540
  })
470
541
 
471
542
  test('handles mixed case in array segments', () => {
472
- const path = builder.makeRPCHttpRoutePath({
473
- name: ['UserModule', 'getActiveUsers'],
543
+ const path = builder.makeRPCHttpRoutePath('ListUsers', {
544
+ scope: ['UserModule', 'getActiveUsers'],
474
545
  version: 1,
475
546
  })
476
- expect(path).toBe('/rpc/user-module/get-active-users/1')
547
+ expect(path).toBe('/user-module/get-active-users/list-users/1')
477
548
  })
478
549
  })
479
550
 
@@ -492,16 +563,17 @@ describe('ExpressRPCAppBuilder', () => {
492
563
  const returnSchema = v.object({ name: v.string() })
493
564
 
494
565
  const RPC = Procedures<{}, RPCConfig>()
495
- const { info } = RPC.Create(
566
+ RPC.Create(
496
567
  'GetUser',
497
- { name: 'users', version: 1, schema: { params: paramsSchema, returnType: returnSchema } },
568
+ { scope: 'users', version: 1, schema: { params: paramsSchema, returnType: returnSchema } },
498
569
  async () => ({ name: 'test' })
499
570
  )
500
571
 
501
- const procedure = RPC.getProcedures()[0]!
502
- const doc = builder.buildRpcHttpRouteDoc(procedure)
572
+ builder.register(RPC, () => ({}))
573
+ builder.build()
503
574
 
504
- expect(doc.path).toBe('/rpc/users/1')
575
+ const doc = builder.docs[0]!
576
+ expect(doc.path).toBe('/users/get-user/1')
505
577
  expect(doc.method).toBe('post')
506
578
  expect(doc.jsonSchema.body).toBeDefined()
507
579
  expect(doc.jsonSchema.response).toBeDefined()
@@ -509,11 +581,12 @@ describe('ExpressRPCAppBuilder', () => {
509
581
 
510
582
  test('omits body schema when no params defined', () => {
511
583
  const RPC = Procedures<{}, RPCConfig>()
512
- RPC.Create('NoParams', { name: 'no-params', version: 1 }, async () => ({ ok: true }))
584
+ RPC.Create('NoParams', { scope: 'no-params', version: 1 }, async () => ({ ok: true }))
513
585
 
514
- const procedure = RPC.getProcedures()[0]!
515
- const doc = builder.buildRpcHttpRouteDoc(procedure)
586
+ builder.register(RPC, () => ({}))
587
+ builder.build()
516
588
 
589
+ const doc = builder.docs[0]!
517
590
  expect(doc.jsonSchema.body).toBeUndefined()
518
591
  })
519
592
 
@@ -521,26 +594,27 @@ describe('ExpressRPCAppBuilder', () => {
521
594
  const RPC = Procedures<{}, RPCConfig>()
522
595
  RPC.Create(
523
596
  'NoReturn',
524
- { name: 'no-return', version: 1, schema: { params: v.object({ x: v.number() }) } },
597
+ { scope: 'no-return', version: 1, schema: { params: v.object({ x: v.number() }) } },
525
598
  async () => ({})
526
599
  )
527
600
 
528
- const procedure = RPC.getProcedures()[0]!
529
- const doc = builder.buildRpcHttpRouteDoc(procedure)
601
+ builder.register(RPC, () => ({}))
602
+ builder.build()
530
603
 
604
+ const doc = builder.docs[0]!
531
605
  expect(doc.jsonSchema.body).toBeDefined()
532
606
  expect(doc.jsonSchema.response).toBeUndefined()
533
607
  })
534
608
 
535
609
  test("method is always 'post'", () => {
536
610
  const RPC = Procedures<{}, RPCConfig>()
537
- RPC.Create('Test1', { name: 't1', version: 1 }, async () => ({}))
538
- RPC.Create('Test2', { name: 't2', version: 2 }, async () => ({}))
611
+ RPC.Create('Test1', { scope: 't1', version: 1 }, async () => ({}))
612
+ RPC.Create('Test2', { scope: 't2', version: 2 }, async () => ({}))
539
613
 
540
- const procedures = RPC.getProcedures()
614
+ builder.register(RPC, () => ({}))
615
+ builder.build()
541
616
 
542
- procedures.forEach((proc) => {
543
- const doc = builder.buildRpcHttpRouteDoc(proc)
617
+ builder.docs.forEach((doc) => {
544
618
  expect(doc.method).toBe('post')
545
619
  })
546
620
  })
@@ -560,11 +634,11 @@ describe('ExpressRPCAppBuilder', () => {
560
634
  const AuthRPC = Procedures<AuthContext, RPCConfig>()
561
635
 
562
636
  // Create public procedures
563
- PublicRPC.Create('GetVersion', { name: ['system', 'version'], version: 1 }, async () => ({
637
+ PublicRPC.Create('GetVersion', { scope: ['system', 'version'], version: 1 }, async () => ({
564
638
  version: '1.0.0',
565
639
  }))
566
640
 
567
- PublicRPC.Create('HealthCheck', { name: 'health', version: 1 }, async () => ({
641
+ PublicRPC.Create('HealthCheck', { scope: 'health', version: 1 }, async () => ({
568
642
  status: 'ok',
569
643
  }))
570
644
 
@@ -572,7 +646,7 @@ describe('ExpressRPCAppBuilder', () => {
572
646
  AuthRPC.Create(
573
647
  'GetProfile',
574
648
  {
575
- name: ['users', 'profile'],
649
+ scope: ['users', 'profile'],
576
650
  version: 1,
577
651
  schema: { returnType: v.object({ userId: v.string(), source: v.string() }) },
578
652
  },
@@ -582,7 +656,7 @@ describe('ExpressRPCAppBuilder', () => {
582
656
  AuthRPC.Create(
583
657
  'UpdateProfile',
584
658
  {
585
- name: ['users', 'profile'],
659
+ scope: ['users', 'profile'],
586
660
  version: 2,
587
661
  schema: { params: v.object({ name: v.string() }) },
588
662
  },
@@ -608,24 +682,24 @@ describe('ExpressRPCAppBuilder', () => {
608
682
  const app = builder.build()
609
683
 
610
684
  // Test public endpoints
611
- const versionRes = await request(app).post('/rpc/system/version/1').send({})
685
+ const versionRes = await request(app).post('/system/version/get-version/1').send({})
612
686
  expect(versionRes.status).toBe(200)
613
687
  expect(versionRes.body).toEqual({ version: '1.0.0' })
614
688
 
615
- const healthRes = await request(app).post('/rpc/health/1').send({})
689
+ const healthRes = await request(app).post('/health/health-check/1').send({})
616
690
  expect(healthRes.status).toBe(200)
617
691
  expect(healthRes.body).toEqual({ status: 'ok' })
618
692
 
619
693
  // Test authenticated endpoints
620
694
  const profileRes = await request(app)
621
- .post('/rpc/users/profile/1')
695
+ .post('/users/profile/get-profile/1')
622
696
  .set('X-User-Id', 'user-123')
623
697
  .send({})
624
698
  expect(profileRes.status).toBe(200)
625
699
  expect(profileRes.body).toEqual({ userId: 'user-123', source: 'auth' })
626
700
 
627
701
  const updateRes = await request(app)
628
- .post('/rpc/users/profile/2')
702
+ .post('/users/profile/update-profile/2')
629
703
  .set('X-User-Id', 'user-456')
630
704
  .send({ name: 'John Doe' })
631
705
  expect(updateRes.status).toBe(200)
@@ -635,10 +709,10 @@ describe('ExpressRPCAppBuilder', () => {
635
709
  expect(builder.docs).toHaveLength(4)
636
710
 
637
711
  const paths = builder.docs.map((d) => d.path)
638
- expect(paths).toContain('/rpc/system/version/1')
639
- expect(paths).toContain('/rpc/health/1')
640
- expect(paths).toContain('/rpc/users/profile/1')
641
- expect(paths).toContain('/rpc/users/profile/2')
712
+ expect(paths).toContain('/system/version/get-version/1')
713
+ expect(paths).toContain('/health/health-check/1')
714
+ expect(paths).toContain('/users/profile/get-profile/1')
715
+ expect(paths).toContain('/users/profile/update-profile/2')
642
716
 
643
717
  // Verify hooks were called
644
718
  expect(events).toContain('request-start')