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.
- package/build/errors.d.ts +2 -1
- package/build/errors.js +3 -2
- package/build/errors.js.map +1 -1
- package/build/errors.test.d.ts +1 -0
- package/build/errors.test.js +40 -0
- package/build/errors.test.js.map +1 -0
- package/build/implementations/http/express-rpc/index.d.ts +3 -2
- package/build/implementations/http/express-rpc/index.js +6 -6
- package/build/implementations/http/express-rpc/index.js.map +1 -1
- package/build/implementations/http/express-rpc/index.test.js +93 -93
- package/build/implementations/http/express-rpc/index.test.js.map +1 -1
- package/build/implementations/http/hono-rpc/index.d.ts +83 -0
- package/build/implementations/http/hono-rpc/index.js +148 -0
- package/build/implementations/http/hono-rpc/index.js.map +1 -0
- package/build/implementations/http/hono-rpc/index.test.d.ts +1 -0
- package/build/implementations/http/hono-rpc/index.test.js +647 -0
- package/build/implementations/http/hono-rpc/index.test.js.map +1 -0
- package/build/implementations/http/hono-rpc/types.d.ts +28 -0
- package/build/implementations/http/hono-rpc/types.js +2 -0
- package/build/implementations/http/hono-rpc/types.js.map +1 -0
- package/build/implementations/types.d.ts +1 -1
- package/build/index.d.ts +12 -0
- package/build/index.js +29 -7
- package/build/index.js.map +1 -1
- package/build/index.test.js +65 -0
- package/build/index.test.js.map +1 -1
- package/build/schema/parser.js +3 -0
- package/build/schema/parser.js.map +1 -1
- package/build/schema/parser.test.js +18 -0
- package/build/schema/parser.test.js.map +1 -1
- package/package.json +8 -2
- package/src/errors.test.ts +53 -0
- package/src/errors.ts +4 -2
- package/src/implementations/http/README.md +172 -0
- package/src/implementations/http/express-rpc/README.md +152 -243
- package/src/implementations/http/express-rpc/index.test.ts +93 -93
- package/src/implementations/http/express-rpc/index.ts +15 -7
- package/src/implementations/http/hono-rpc/README.md +293 -0
- package/src/implementations/http/hono-rpc/index.test.ts +847 -0
- package/src/implementations/http/hono-rpc/index.ts +202 -0
- package/src/implementations/http/hono-rpc/types.ts +33 -0
- package/src/implementations/types.ts +2 -1
- package/src/index.test.ts +83 -0
- package/src/index.ts +34 -8
- package/src/schema/parser.test.ts +26 -0
- 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
|
-
#
|
|
1
|
+
# ExpressRPCAppBuilder
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
|
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) => ({
|
|
68
|
-
|
|
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
|
-
|
|
47
|
+
const app = builder.build()
|
|
48
|
+
app.listen(3000)
|
|
76
49
|
|
|
77
|
-
|
|
50
|
+
// POST /rpc/users/profile/get-user/1 → { id: "123", name: "John Doe" }
|
|
51
|
+
```
|
|
78
52
|
|
|
79
|
-
|
|
53
|
+
## Configuration
|
|
80
54
|
|
|
81
55
|
```typescript
|
|
82
|
-
|
|
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` |
|
|
95
|
-
| `pathPrefix` | `string` |
|
|
96
|
-
| `onRequestStart` | `(req) => void` | Called at
|
|
97
|
-
| `onRequestEnd` | `(req, res) => void` | Called after
|
|
98
|
-
| `onSuccess` | `(
|
|
99
|
-
| `error` | `(
|
|
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
|
-
|
|
75
|
+
## Context Resolution
|
|
104
76
|
|
|
105
|
-
|
|
77
|
+
The context resolver receives the Express `Request` object:
|
|
106
78
|
|
|
107
79
|
```typescript
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
|
86
|
+
// Async context resolution
|
|
115
87
|
builder.register(RPC, async (req) => {
|
|
116
|
-
const
|
|
117
|
-
|
|
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
|
-
|
|
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
|
-
|
|
96
|
+
Custom error handler receives the procedure, request, response, and error:
|
|
131
97
|
|
|
132
98
|
```typescript
|
|
133
|
-
const
|
|
134
|
-
|
|
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
|
-
|
|
103
|
+
if (error instanceof ValidationError) {
|
|
104
|
+
res.status(400).json({ error: error.message, code: 'VALIDATION_ERROR' })
|
|
105
|
+
return
|
|
106
|
+
}
|
|
144
107
|
|
|
145
|
-
|
|
108
|
+
if (error instanceof AuthError) {
|
|
109
|
+
res.status(401).json({ error: 'Unauthorized', code: 'AUTH_ERROR' })
|
|
110
|
+
return
|
|
111
|
+
}
|
|
146
112
|
|
|
147
|
-
|
|
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
|
-
|
|
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
|
-
|
|
120
|
+
## Using Existing Express App
|
|
163
121
|
|
|
164
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
userId: ctx.userId,
|
|
199
|
-
}))
|
|
130
|
+
const builder = new ExpressRPCAppBuilder({ app })
|
|
131
|
+
.register(RPC, contextResolver)
|
|
200
132
|
|
|
201
|
-
//
|
|
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
|
-
|
|
136
|
+
When no `app` is provided, `express.json()` middleware is added automatically.
|
|
212
137
|
|
|
213
|
-
|
|
214
|
-
const app = new ExpressRPCAppBuilder({
|
|
215
|
-
onRequestStart: (req) => {
|
|
216
|
-
console.log(`[${req.method}] ${req.path}`)
|
|
217
|
-
},
|
|
138
|
+
## API Reference
|
|
218
139
|
|
|
219
|
-
|
|
220
|
-
console.log(`Response: ${res.statusCode}`)
|
|
221
|
-
},
|
|
140
|
+
### Constructor
|
|
222
141
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
},
|
|
226
|
-
})
|
|
142
|
+
```typescript
|
|
143
|
+
new ExpressRPCAppBuilder(config?: ExpressRPCAppBuilderConfig)
|
|
227
144
|
```
|
|
228
145
|
|
|
229
|
-
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
###
|
|
154
|
+
### Static Methods
|
|
254
155
|
|
|
255
|
-
|
|
156
|
+
| Method | Signature | Description |
|
|
157
|
+
|--------|-----------|-------------|
|
|
158
|
+
| `makeRPCHttpRoutePath` | `static makeRPCHttpRoutePath({ config, prefix }): string` | Generate route path with custom prefix |
|
|
256
159
|
|
|
257
|
-
|
|
258
|
-
{ "error": "Error message here" }
|
|
259
|
-
```
|
|
160
|
+
### Properties
|
|
260
161
|
|
|
261
|
-
|
|
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
|
-
|
|
168
|
+
## TypeScript Types
|
|
264
169
|
|
|
265
170
|
```typescript
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
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
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
307
|
-
|
|
191
|
+
// Create factories
|
|
192
|
+
const PublicRPC = Procedures<PublicContext, RPCConfig>()
|
|
193
|
+
const AuthRPC = Procedures<AuthContext, RPCConfig>()
|
|
308
194
|
|
|
309
|
-
|
|
195
|
+
// Public procedures
|
|
196
|
+
PublicRPC.Create('HealthCheck', { scope: 'health', version: 1 }, async () => ({
|
|
197
|
+
status: 'ok'
|
|
198
|
+
}))
|
|
310
199
|
|
|
311
|
-
|
|
200
|
+
PublicRPC.Create('GetVersion', { scope: ['system', 'version'], version: 1 }, async () => ({
|
|
201
|
+
version: '1.0.0'
|
|
202
|
+
}))
|
|
312
203
|
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
//
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
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
|
-
|
|
244
|
+
const app = builder.build()
|
|
336
245
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
|
|
344
|
-
|
|
252
|
+
console.log('Routes:', builder.docs.map(d => d.path))
|
|
253
|
+
app.listen(3000)
|
|
345
254
|
```
|