raffel 1.0.2 → 1.0.3
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/README.md +513 -270
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/json-server/index.d.ts +119 -0
- package/dist/json-server/index.d.ts.map +1 -0
- package/dist/json-server/index.js +325 -0
- package/dist/json-server/index.js.map +1 -0
- package/dist/json-server/store.d.ts +61 -0
- package/dist/json-server/store.d.ts.map +1 -0
- package/dist/json-server/store.js +170 -0
- package/dist/json-server/store.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
# Raffel
|
|
4
4
|
|
|
5
|
-
###
|
|
5
|
+
### One server. HTTP, WebSocket, gRPC, TCP, UDP — all at once.
|
|
6
6
|
|
|
7
7
|
[](https://www.npmjs.com/package/raffel)
|
|
8
8
|
[](https://www.typescriptlang.org/)
|
|
9
9
|
[](https://nodejs.org/)
|
|
10
10
|
[](LICENSE)
|
|
11
11
|
|
|
12
|
-
[Quick Start](#quick-start) · [Full Documentation](https://forattini-dev.github.io/raffel) · [Examples](./examples) · [Migration from
|
|
12
|
+
[Quick Start](#quick-start) · [Full Documentation](https://forattini-dev.github.io/raffel) · [Examples](./examples) · [Migration from Hono](#migration-from-hono)
|
|
13
13
|
|
|
14
14
|
</div>
|
|
15
15
|
|
|
@@ -18,32 +18,29 @@
|
|
|
18
18
|
## If You Know Express, You Know Raffel
|
|
19
19
|
|
|
20
20
|
```typescript
|
|
21
|
-
import {
|
|
21
|
+
import { HttpApp, serve } from 'raffel'
|
|
22
22
|
|
|
23
|
-
const app =
|
|
23
|
+
const app = new HttpApp()
|
|
24
24
|
|
|
25
|
-
app.get('/users', async () => {
|
|
26
|
-
return db.users.findMany()
|
|
25
|
+
app.get('/users', async (c) => {
|
|
26
|
+
return c.json(await db.users.findMany())
|
|
27
27
|
})
|
|
28
28
|
|
|
29
|
-
app.get('/users/:id', async (
|
|
30
|
-
|
|
29
|
+
app.get('/users/:id', async (c) => {
|
|
30
|
+
const user = await db.users.findById(c.req.param('id'))
|
|
31
|
+
if (!user) return c.json({ error: 'Not found' }, 404)
|
|
32
|
+
return c.json(user)
|
|
31
33
|
})
|
|
32
34
|
|
|
33
|
-
app.post('/users', async (
|
|
34
|
-
|
|
35
|
+
app.post('/users', async (c) => {
|
|
36
|
+
const body = await c.req.json()
|
|
37
|
+
return c.json(await db.users.create(body), 201)
|
|
35
38
|
})
|
|
36
39
|
|
|
37
|
-
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
```bash
|
|
41
|
-
curl http://localhost:3000/users
|
|
42
|
-
curl http://localhost:3000/users/123
|
|
43
|
-
curl -X POST http://localhost:3000/users -d '{"name":"John"}'
|
|
40
|
+
serve({ fetch: app.fetch, port: 3000 })
|
|
44
41
|
```
|
|
45
42
|
|
|
46
|
-
**
|
|
43
|
+
**Identical to Hono.** Same routes, same context API, same middleware signature. Your muscle memory works.
|
|
47
44
|
|
|
48
45
|
---
|
|
49
46
|
|
|
@@ -56,387 +53,633 @@ pnpm add raffel
|
|
|
56
53
|
### Hello World
|
|
57
54
|
|
|
58
55
|
```typescript
|
|
59
|
-
import {
|
|
56
|
+
import { HttpApp, serve } from 'raffel'
|
|
60
57
|
|
|
61
|
-
const app =
|
|
58
|
+
const app = new HttpApp()
|
|
62
59
|
|
|
63
|
-
app.get('/hello/:name',
|
|
64
|
-
return { message: `Hello, ${name}!` }
|
|
65
|
-
})
|
|
60
|
+
app.get('/hello/:name', (c) => c.text(`Hello, ${c.req.param('name')}!`))
|
|
66
61
|
|
|
67
|
-
|
|
62
|
+
serve({ fetch: app.fetch, port: 3000 })
|
|
68
63
|
```
|
|
69
64
|
|
|
70
|
-
|
|
71
|
-
curl http://localhost:3000/hello/World
|
|
72
|
-
# → {"message":"Hello, World!"}
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
### CRUD API in 30 Seconds
|
|
65
|
+
### CRUD in 30 Seconds
|
|
76
66
|
|
|
77
67
|
```typescript
|
|
78
|
-
import {
|
|
68
|
+
import { HttpApp, serve } from 'raffel'
|
|
79
69
|
|
|
80
|
-
const app =
|
|
70
|
+
const app = new HttpApp()
|
|
71
|
+
const users = new Map<string, unknown>()
|
|
81
72
|
|
|
82
|
-
|
|
73
|
+
app.get('/users', (c) => c.json([...users.values()]))
|
|
83
74
|
|
|
84
|
-
app.get('/users',
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const user = users.get(id)
|
|
88
|
-
if (!user) throw app.errors.notFound('User not found')
|
|
89
|
-
return user
|
|
75
|
+
app.get('/users/:id', (c) => {
|
|
76
|
+
const user = users.get(c.req.param('id'))
|
|
77
|
+
return user ? c.json(user) : c.json({ error: 'Not found' }, 404)
|
|
90
78
|
})
|
|
91
79
|
|
|
92
|
-
app.post('/users', async (
|
|
93
|
-
const user = { id: crypto.randomUUID(), ...
|
|
80
|
+
app.post('/users', async (c) => {
|
|
81
|
+
const user = { id: crypto.randomUUID(), ...(await c.req.json()) }
|
|
94
82
|
users.set(user.id, user)
|
|
95
|
-
return user
|
|
83
|
+
return c.json(user, 201)
|
|
96
84
|
})
|
|
97
85
|
|
|
98
|
-
app.put('/users/:id', async (
|
|
99
|
-
|
|
100
|
-
|
|
86
|
+
app.put('/users/:id', async (c) => {
|
|
87
|
+
const id = c.req.param('id')
|
|
88
|
+
if (!users.has(id)) return c.json({ error: 'Not found' }, 404)
|
|
89
|
+
const user = { id, ...(await c.req.json()) }
|
|
101
90
|
users.set(id, user)
|
|
102
|
-
return user
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
app.delete('/users/:id', async ({ id }) => {
|
|
106
|
-
if (!users.delete(id)) throw app.errors.notFound('User not found')
|
|
107
|
-
return { success: true }
|
|
91
|
+
return c.json(user)
|
|
108
92
|
})
|
|
109
93
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
```typescript
|
|
116
|
-
import { createServer } from 'raffel'
|
|
117
|
-
import { z } from 'zod'
|
|
118
|
-
|
|
119
|
-
const app = createServer({ port: 3000 })
|
|
120
|
-
|
|
121
|
-
app.post('/users', {
|
|
122
|
-
body: z.object({
|
|
123
|
-
name: z.string().min(2),
|
|
124
|
-
email: z.string().email(),
|
|
125
|
-
}),
|
|
126
|
-
handler: async (body) => {
|
|
127
|
-
return db.users.create(body)
|
|
128
|
-
}
|
|
94
|
+
app.delete('/users/:id', (c) => {
|
|
95
|
+
const id = c.req.param('id')
|
|
96
|
+
return users.delete(id)
|
|
97
|
+
? c.json({ success: true })
|
|
98
|
+
: c.json({ error: 'Not found' }, 404)
|
|
129
99
|
})
|
|
130
100
|
|
|
131
|
-
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
Invalid request? Automatic error response:
|
|
135
|
-
|
|
136
|
-
```json
|
|
137
|
-
{
|
|
138
|
-
"error": "VALIDATION_ERROR",
|
|
139
|
-
"message": "Validation failed",
|
|
140
|
-
"details": [
|
|
141
|
-
{ "path": "email", "message": "Invalid email" }
|
|
142
|
-
]
|
|
143
|
-
}
|
|
101
|
+
serve({ fetch: app.fetch, port: 3000 })
|
|
144
102
|
```
|
|
145
103
|
|
|
146
|
-
###
|
|
104
|
+
### Production-Ready `serve()`
|
|
147
105
|
|
|
148
106
|
```typescript
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const start = Date.now()
|
|
156
|
-
const result = await next()
|
|
157
|
-
console.log(`${req.method} ${req.path} - ${Date.now() - start}ms`)
|
|
158
|
-
return result
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
// Auth middleware
|
|
162
|
-
const requireAuth = async (req, next) => {
|
|
163
|
-
const token = req.headers.authorization?.replace('Bearer ', '')
|
|
164
|
-
if (!token) throw app.errors.unauthorized()
|
|
165
|
-
req.user = await verifyToken(token)
|
|
166
|
-
return next()
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
app.get('/profile', requireAuth, async (_, req) => {
|
|
170
|
-
return req.user
|
|
107
|
+
serve({
|
|
108
|
+
fetch: app.fetch,
|
|
109
|
+
port: 3000,
|
|
110
|
+
keepAliveTimeout: 65000, // slightly above load balancer idle timeout
|
|
111
|
+
headersTimeout: 66000,
|
|
112
|
+
onListen: ({ port, hostname }) => console.log(`Listening on ${hostname}:${port}`),
|
|
171
113
|
})
|
|
172
|
-
|
|
173
|
-
await app.start()
|
|
174
114
|
```
|
|
175
115
|
|
|
176
116
|
---
|
|
177
117
|
|
|
178
118
|
## Wait, There's More
|
|
179
119
|
|
|
180
|
-
|
|
120
|
+
Raffel is not just an HTTP framework. It's a **unified multi-protocol runtime**. Every handler you write is protocol-agnostic — the same business logic runs over HTTP, WebSocket, gRPC, JSON-RPC, GraphQL, TCP, and UDP.
|
|
121
|
+
|
|
122
|
+
### The Procedure API
|
|
181
123
|
|
|
182
124
|
```typescript
|
|
183
|
-
|
|
125
|
+
import { createServer } from 'raffel'
|
|
126
|
+
import { z } from 'zod'
|
|
127
|
+
|
|
128
|
+
const server = createServer({
|
|
184
129
|
port: 3000,
|
|
185
130
|
websocket: { path: '/ws' },
|
|
186
131
|
jsonrpc: { path: '/rpc' },
|
|
187
132
|
})
|
|
188
133
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
})
|
|
134
|
+
server
|
|
135
|
+
.procedure('users.create')
|
|
136
|
+
.input(z.object({ name: z.string().min(2), email: z.string().email() }))
|
|
137
|
+
.output(z.object({ id: z.string(), name: z.string(), email: z.string() }))
|
|
138
|
+
.handler(async (input, ctx) => {
|
|
139
|
+
return db.users.create(input)
|
|
140
|
+
})
|
|
192
141
|
|
|
193
|
-
await
|
|
142
|
+
await server.start()
|
|
194
143
|
```
|
|
195
144
|
|
|
196
|
-
**Same handler.
|
|
145
|
+
**Same handler. Every protocol. Zero extra code.**
|
|
197
146
|
|
|
198
147
|
```bash
|
|
199
|
-
# HTTP
|
|
200
|
-
curl http://localhost:3000/users
|
|
148
|
+
# HTTP
|
|
149
|
+
curl -X POST http://localhost:3000/users \
|
|
150
|
+
-d '{"name":"Alice","email":"alice@example.com"}'
|
|
201
151
|
|
|
202
152
|
# WebSocket
|
|
203
153
|
wscat -c ws://localhost:3000/ws
|
|
204
|
-
> {"method":"users.
|
|
154
|
+
> {"method":"users.create","params":{"name":"Alice","email":"alice@example.com"}}
|
|
205
155
|
|
|
206
|
-
# JSON-RPC
|
|
156
|
+
# JSON-RPC 2.0
|
|
207
157
|
curl -X POST http://localhost:3000/rpc \
|
|
208
|
-
-d '{"jsonrpc":"2.0","method":"users.
|
|
158
|
+
-d '{"jsonrpc":"2.0","method":"users.create","params":{...},"id":1}'
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Streaming
|
|
162
|
+
|
|
163
|
+
```typescript
|
|
164
|
+
// Server → client stream
|
|
165
|
+
server
|
|
166
|
+
.stream('logs.tail')
|
|
167
|
+
.handler(async function* ({ file }) {
|
|
168
|
+
for await (const line of readLines(file)) {
|
|
169
|
+
yield { line, timestamp: Date.now() }
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
// Bidirectional stream
|
|
174
|
+
server
|
|
175
|
+
.stream('chat.session')
|
|
176
|
+
.bidi()
|
|
177
|
+
.handler(async (stream, ctx) => {
|
|
178
|
+
for await (const msg of stream) {
|
|
179
|
+
await stream.write({ echo: msg, from: ctx.auth?.userId })
|
|
180
|
+
}
|
|
181
|
+
})
|
|
209
182
|
```
|
|
210
183
|
|
|
211
|
-
###
|
|
184
|
+
### Events with Delivery Guarantees
|
|
212
185
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
186
|
+
```typescript
|
|
187
|
+
server
|
|
188
|
+
.event('emails.send')
|
|
189
|
+
.delivery('at-least-once')
|
|
190
|
+
.handler(async (payload, ctx, ack) => {
|
|
191
|
+
await sendEmail(payload)
|
|
192
|
+
ack()
|
|
193
|
+
})
|
|
194
|
+
```
|
|
216
195
|
|
|
217
196
|
---
|
|
218
197
|
|
|
219
198
|
## The Full Picture
|
|
220
199
|
|
|
221
|
-
|
|
200
|
+
| Module | What it does |
|
|
201
|
+
|--------|-------------|
|
|
202
|
+
| **HTTP** | Hono-compatible router + `serve()` with production timeouts |
|
|
203
|
+
| **WebSocket** | Real-time adapter + Pusher-like channels (public/private/presence) |
|
|
204
|
+
| **gRPC** | Full gRPC adapter with TLS and streaming |
|
|
205
|
+
| **JSON-RPC 2.0** | Batch + notification + error codes per spec |
|
|
206
|
+
| **GraphQL** | Schema-first adapter with subscriptions |
|
|
207
|
+
| **TCP / UDP** | Raw socket handlers with connection filters |
|
|
208
|
+
| **Single-Port** | Sniff protocol on one port — HTTP, WS, gRPC, gRPC-Web all on `:3000` |
|
|
209
|
+
| **Interceptors** | Rate limit, circuit breaker, retry, timeout, cache, bulkhead, and more |
|
|
210
|
+
| **Session Store** | Memory + Redis drivers with lazy load + auto-save |
|
|
211
|
+
| **Proxy Suite** | HTTP forward, CONNECT tunnel (MITM), SOCKS5, transparent |
|
|
212
|
+
| **Metrics** | Prometheus-style counters, gauges, histograms with exporters |
|
|
213
|
+
| **Tracing** | OpenTelemetry spans with Jaeger / Zipkin exporters |
|
|
214
|
+
| **OpenAPI** | Generate spec from schemas + serve ReDoc / Swagger UI |
|
|
215
|
+
| **Channels** | Pusher-like pub/sub with presence and authorization |
|
|
216
|
+
| **MCP Server** | Model Context Protocol for AI-assisted development |
|
|
217
|
+
| **Testing** | Full mock suite: HTTP, WS, TCP, UDP, DNS, SSE, Proxy |
|
|
218
|
+
| **Validation** | Plug in Zod, Yup, Joi, Ajv, or fastest-validator |
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## Interceptors
|
|
222
223
|
|
|
223
|
-
|
|
224
|
-
|----------------|---------------------|
|
|
225
|
-
| `app.get('/users/:id', handler)` | HTTP GET, WS, JSON-RPC, gRPC, GraphQL |
|
|
226
|
-
| `app.post('/users', handler)` | HTTP POST, WS, JSON-RPC, gRPC, GraphQL |
|
|
227
|
-
| Validation schema | Same validation, all protocols |
|
|
228
|
-
| Auth middleware | Same auth, all protocols |
|
|
229
|
-
| Error handling | Protocol-appropriate errors |
|
|
224
|
+
Interceptors are reusable middleware that compose cleanly across any protocol.
|
|
230
225
|
|
|
231
|
-
|
|
226
|
+
```typescript
|
|
227
|
+
import {
|
|
228
|
+
createRateLimitInterceptor,
|
|
229
|
+
createCircuitBreakerInterceptor,
|
|
230
|
+
createRetryInterceptor,
|
|
231
|
+
createTimeoutInterceptor,
|
|
232
|
+
createCacheInterceptor,
|
|
233
|
+
createLoggingInterceptor,
|
|
234
|
+
createTracingInterceptor,
|
|
235
|
+
} from 'raffel'
|
|
236
|
+
|
|
237
|
+
server
|
|
238
|
+
.procedure('users.list')
|
|
239
|
+
.use(createTimeoutInterceptor({ timeout: 5000 }))
|
|
240
|
+
.use(createRateLimitInterceptor({ limit: 100, window: '1m' }))
|
|
241
|
+
.use(createCacheInterceptor({ ttl: 60, store: cacheStore }))
|
|
242
|
+
.use(createLoggingInterceptor())
|
|
243
|
+
.handler(async () => db.users.findMany())
|
|
244
|
+
```
|
|
232
245
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
246
|
+
Apply globally, per-group, or per-procedure:
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
// Global
|
|
250
|
+
server.use(createTracingInterceptor({ tracer }))
|
|
251
|
+
server.use(createLoggingInterceptor())
|
|
252
|
+
|
|
253
|
+
// Group / module
|
|
254
|
+
const adminModule = createRouterModule('admin', [requireAdmin])
|
|
255
|
+
adminModule.procedure('users.delete').handler(...)
|
|
256
|
+
|
|
257
|
+
// Per-procedure
|
|
258
|
+
server.procedure('payments.charge')
|
|
259
|
+
.use(createCircuitBreakerInterceptor({ threshold: 5, timeout: 30000 }))
|
|
260
|
+
.use(createRetryInterceptor({ attempts: 3, backoff: 'exponential' }))
|
|
261
|
+
.handler(...)
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
| Interceptor | Purpose |
|
|
265
|
+
|-------------|---------|
|
|
266
|
+
| `createRateLimitInterceptor` | Token bucket / sliding window (memory, Redis, filesystem) |
|
|
267
|
+
| `createCircuitBreakerInterceptor` | Auto-open after failures, half-open probe |
|
|
268
|
+
| `createBulkheadInterceptor` | Concurrency isolation per procedure |
|
|
269
|
+
| `createRetryInterceptor` | Exponential backoff with jitter |
|
|
270
|
+
| `createTimeoutInterceptor` | Per-phase, cascading, deadline propagation |
|
|
271
|
+
| `createCacheInterceptor` | Read-through / write-through (memory, file, Redis) |
|
|
272
|
+
| `createDedupInterceptor` | In-flight request deduplication |
|
|
273
|
+
| `createSizeLimitInterceptor` | Request / response size guard |
|
|
274
|
+
| `createFallbackInterceptor` | Return default on failure |
|
|
275
|
+
| `createRequestIdInterceptor` | Inject/propagate correlation IDs |
|
|
276
|
+
| `createLoggingInterceptor` | Structured request/response logging |
|
|
277
|
+
| `createMetricsInterceptor` | Auto-instrument with Prometheus metrics |
|
|
278
|
+
| `createTracingInterceptor` | OpenTelemetry span creation |
|
|
279
|
+
| `createSessionInterceptor` | Session load/save via memory or Redis |
|
|
280
|
+
| `createValidationInterceptor` | Schema validation on input/output |
|
|
281
|
+
| `createAuthMiddleware` | Bearer token, API key strategies |
|
|
242
282
|
|
|
243
283
|
---
|
|
244
284
|
|
|
245
|
-
##
|
|
285
|
+
## Channels (Real-Time Pub/Sub)
|
|
246
286
|
|
|
247
|
-
|
|
287
|
+
Pusher-compatible channel model over WebSocket.
|
|
248
288
|
|
|
249
289
|
```typescript
|
|
250
|
-
import {
|
|
251
|
-
|
|
290
|
+
import { createChannelManager } from 'raffel'
|
|
291
|
+
|
|
292
|
+
const channels = createChannelManager(
|
|
293
|
+
{
|
|
294
|
+
authorize: async (socketId, channel, ctx) => {
|
|
295
|
+
// private-* and presence-* channels require auth
|
|
296
|
+
return { authorized: !!ctx.auth }
|
|
297
|
+
},
|
|
298
|
+
presence: {
|
|
299
|
+
onJoin: (channel, member) => broadcastPresence(channel),
|
|
300
|
+
onLeave: (channel, member) => broadcastPresence(channel),
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
(socketId, message) => ws.sendToClient(socketId, message)
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
// Subscribe
|
|
307
|
+
await channels.subscribe(socketId, 'presence-room:42', ctx)
|
|
308
|
+
|
|
309
|
+
// Broadcast to all subscribers
|
|
310
|
+
channels.broadcast('presence-room:42', 'new-message', { text: 'Hello!' })
|
|
311
|
+
|
|
312
|
+
// Get online members
|
|
313
|
+
const members = channels.getMembers('presence-room:42')
|
|
314
|
+
```
|
|
252
315
|
|
|
253
|
-
|
|
316
|
+
Channel types: `public-*` (anyone), `private-*` (authorized), `presence-*` (auth + member tracking).
|
|
254
317
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
## Proxy Suite
|
|
321
|
+
|
|
322
|
+
Full proxy toolkit built into Raffel — no extra dependencies.
|
|
323
|
+
|
|
324
|
+
### HTTP Forward Proxy
|
|
325
|
+
|
|
326
|
+
```typescript
|
|
327
|
+
import { createHttpForwardProxy } from 'raffel'
|
|
328
|
+
|
|
329
|
+
const proxy = createHttpForwardProxy(httpServer, {
|
|
330
|
+
auth: { type: 'basic', credentials: { admin: 'secret' } },
|
|
331
|
+
filter: {
|
|
332
|
+
allowHosts: ['*.trusted.com', 'api.internal'],
|
|
333
|
+
denyHosts: ['*.evil.com'],
|
|
334
|
+
},
|
|
335
|
+
onRequest: (req) => { /* log or modify */ return req },
|
|
258
336
|
})
|
|
337
|
+
```
|
|
259
338
|
|
|
260
|
-
|
|
261
|
-
server.procedure('users.create')
|
|
262
|
-
.description('Create a new user')
|
|
263
|
-
.input(z.object({
|
|
264
|
-
name: z.string().min(2),
|
|
265
|
-
email: z.string().email(),
|
|
266
|
-
}))
|
|
267
|
-
.output(z.object({
|
|
268
|
-
id: z.string().uuid(),
|
|
269
|
-
name: z.string(),
|
|
270
|
-
email: z.string(),
|
|
271
|
-
}))
|
|
272
|
-
.handler(async (input, ctx) => {
|
|
273
|
-
// ctx has auth, tracing, request metadata
|
|
274
|
-
return db.users.create(input)
|
|
275
|
-
})
|
|
339
|
+
### CONNECT Tunnel (with MITM)
|
|
276
340
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
341
|
+
```typescript
|
|
342
|
+
import { createConnectTunnel } from 'raffel'
|
|
343
|
+
|
|
344
|
+
// Transparent tunnel
|
|
345
|
+
const tunnel = createConnectTunnel({ mode: 'pipe' })
|
|
346
|
+
|
|
347
|
+
// MITM: inspect and modify HTTPS traffic
|
|
348
|
+
const mitm = createConnectTunnel({
|
|
349
|
+
mode: 'mitm',
|
|
350
|
+
onRequest: (req) => {
|
|
351
|
+
req.headers['x-intercepted'] = 'true'
|
|
352
|
+
return req
|
|
353
|
+
},
|
|
354
|
+
onResponse: (res) => {
|
|
355
|
+
res.headers['x-inspected'] = 'true'
|
|
356
|
+
return res
|
|
357
|
+
},
|
|
358
|
+
onUpstreamCert: (cert) => trustedCerts.has(cert.fingerprint), // cert pinning
|
|
359
|
+
})
|
|
360
|
+
```
|
|
284
361
|
|
|
285
|
-
|
|
286
|
-
server.event('emails.send')
|
|
287
|
-
.delivery('at-least-once')
|
|
288
|
-
.handler(async (payload, ctx, ack) => {
|
|
289
|
-
await sendEmail(payload)
|
|
290
|
-
ack()
|
|
291
|
-
})
|
|
362
|
+
### SOCKS5 Proxy
|
|
292
363
|
|
|
293
|
-
|
|
364
|
+
```typescript
|
|
365
|
+
import { createSocks5Proxy } from 'raffel'
|
|
366
|
+
|
|
367
|
+
const socks5 = createSocks5Proxy({
|
|
368
|
+
port: 1080,
|
|
369
|
+
auth: { type: 'userpass', users: { alice: 'secret' } },
|
|
370
|
+
})
|
|
371
|
+
await socks5.start()
|
|
294
372
|
```
|
|
295
373
|
|
|
296
|
-
###
|
|
374
|
+
### Transparent Proxy (Linux TPROXY)
|
|
375
|
+
|
|
376
|
+
```typescript
|
|
377
|
+
import { createTransparentProxy } from 'raffel'
|
|
297
378
|
|
|
298
|
-
|
|
379
|
+
const proxy = createTransparentProxy({
|
|
380
|
+
mode: 'tproxy',
|
|
381
|
+
port: 8080,
|
|
382
|
+
upstream: { host: 'backend.internal', port: 8080 },
|
|
383
|
+
})
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
---
|
|
387
|
+
|
|
388
|
+
## Session Store
|
|
299
389
|
|
|
300
390
|
```typescript
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
}
|
|
391
|
+
import { createSessionInterceptor, createRedisSessionDriver } from 'raffel'
|
|
392
|
+
|
|
393
|
+
const sessions = createSessionInterceptor({
|
|
394
|
+
driver: createRedisSessionDriver({ client: redis }),
|
|
395
|
+
cookie: { name: 'sid', httpOnly: true, secure: true, sameSite: 'lax' },
|
|
396
|
+
ttl: 86400,
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
server.use(sessions)
|
|
400
|
+
|
|
401
|
+
server.procedure('auth.me').handler(async (_, ctx) => {
|
|
402
|
+
// ctx.session is loaded lazily, saved automatically
|
|
403
|
+
const { userId } = ctx.session.get()
|
|
404
|
+
return db.users.findById(userId)
|
|
405
|
+
})
|
|
308
406
|
```
|
|
309
407
|
|
|
310
|
-
|
|
408
|
+
Drivers: `createMemorySessionDriver()`, `createRedisSessionDriver({ client })`.
|
|
311
409
|
|
|
312
410
|
---
|
|
313
411
|
|
|
314
|
-
##
|
|
412
|
+
## OpenAPI + Docs UI
|
|
315
413
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
414
|
+
```typescript
|
|
415
|
+
import { generateOpenAPI, mountOpenApiDocs } from 'raffel'
|
|
416
|
+
|
|
417
|
+
// Auto-generate spec from registered schemas
|
|
418
|
+
const spec = generateOpenAPI(server, {
|
|
419
|
+
info: { title: 'My API', version: '1.0.0' },
|
|
420
|
+
servers: [{ url: 'https://api.example.com' }],
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
// Mount /openapi.json + /docs (ReDoc or Swagger UI)
|
|
424
|
+
mountOpenApiDocs(app, {
|
|
425
|
+
spec,
|
|
426
|
+
ui: 'redoc', // or 'swagger'
|
|
427
|
+
path: '/docs',
|
|
428
|
+
})
|
|
429
|
+
```
|
|
327
430
|
|
|
328
431
|
---
|
|
329
432
|
|
|
330
|
-
##
|
|
433
|
+
## Metrics & Tracing
|
|
331
434
|
|
|
332
|
-
|
|
435
|
+
### Prometheus Metrics
|
|
333
436
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
<th>Express</th>
|
|
337
|
-
<th>Raffel</th>
|
|
338
|
-
</tr>
|
|
339
|
-
<tr>
|
|
340
|
-
<td>
|
|
437
|
+
```typescript
|
|
438
|
+
import { createMetricRegistry, createMetricsInterceptor, exportPrometheus } from 'raffel'
|
|
341
439
|
|
|
342
|
-
|
|
343
|
-
const express = require('express')
|
|
344
|
-
const app = express()
|
|
440
|
+
const metrics = createMetricRegistry()
|
|
345
441
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
442
|
+
server.use(createMetricsInterceptor({ registry: metrics }))
|
|
443
|
+
|
|
444
|
+
// Expose /metrics endpoint
|
|
445
|
+
app.get('/metrics', (c) => c.text(exportPrometheus(metrics), 200, {
|
|
446
|
+
'Content-Type': 'text/plain; version=0.0.4',
|
|
447
|
+
}))
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
### OpenTelemetry Tracing
|
|
451
|
+
|
|
452
|
+
```typescript
|
|
453
|
+
import { createTracer, createTracingInterceptor, createJaegerExporter } from 'raffel'
|
|
350
454
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
455
|
+
const tracer = createTracer({
|
|
456
|
+
serviceName: 'my-api',
|
|
457
|
+
exporter: createJaegerExporter({ endpoint: 'http://jaeger:14268/api/traces' }),
|
|
458
|
+
sampler: createProbabilitySampler(0.1), // 10% sampling
|
|
354
459
|
})
|
|
355
460
|
|
|
356
|
-
|
|
461
|
+
server.use(createTracingInterceptor({ tracer }))
|
|
357
462
|
```
|
|
358
463
|
|
|
359
|
-
|
|
360
|
-
|
|
464
|
+
---
|
|
465
|
+
|
|
466
|
+
## Health Checks
|
|
361
467
|
|
|
362
468
|
```typescript
|
|
363
|
-
import {
|
|
469
|
+
import { createHealthCheckProcedures, CommonProbes } from 'raffel'
|
|
470
|
+
|
|
471
|
+
const health = createHealthCheckProcedures({
|
|
472
|
+
probes: [
|
|
473
|
+
CommonProbes.memory({ maxHeapMb: 512 }),
|
|
474
|
+
CommonProbes.uptime(),
|
|
475
|
+
{
|
|
476
|
+
name: 'database',
|
|
477
|
+
check: async () => {
|
|
478
|
+
await db.ping()
|
|
479
|
+
return { status: 'healthy' }
|
|
480
|
+
},
|
|
481
|
+
},
|
|
482
|
+
],
|
|
483
|
+
})
|
|
364
484
|
|
|
365
|
-
|
|
485
|
+
server.mount('/', health)
|
|
486
|
+
// Registers: health.live, health.ready, health.startup
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
---
|
|
366
490
|
|
|
367
|
-
|
|
368
|
-
|
|
491
|
+
## Connection Filters
|
|
492
|
+
|
|
493
|
+
Control who can connect to your TCP, UDP, and WebSocket adapters.
|
|
494
|
+
|
|
495
|
+
```typescript
|
|
496
|
+
import { createTcpAdapter } from 'raffel'
|
|
497
|
+
|
|
498
|
+
const tcp = createTcpAdapter(router, {
|
|
499
|
+
connectionFilter: {
|
|
500
|
+
allowHosts: ['10.0.0.*', 'trusted.internal'],
|
|
501
|
+
denyHosts: ['*.untrusted.net'],
|
|
502
|
+
onDenied: (host, port) => logger.warn(`Blocked connection from ${host}:${port}`),
|
|
503
|
+
},
|
|
369
504
|
})
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
WebSocket adds origin filtering:
|
|
370
508
|
|
|
371
|
-
|
|
372
|
-
|
|
509
|
+
```typescript
|
|
510
|
+
const ws = createWebSocketAdapter(router, {
|
|
511
|
+
connectionFilter: {
|
|
512
|
+
allowOrigins: ['https://app.example.com'],
|
|
513
|
+
denyOrigins: ['*'],
|
|
514
|
+
},
|
|
373
515
|
})
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
---
|
|
374
519
|
|
|
375
|
-
|
|
520
|
+
## Single-Port Multi-Protocol
|
|
521
|
+
|
|
522
|
+
Run HTTP, WebSocket, gRPC, and gRPC-Web all on the same port. Raffel sniffs the protocol from the first bytes.
|
|
523
|
+
|
|
524
|
+
```typescript
|
|
525
|
+
const server = createServer({
|
|
526
|
+
port: 3000,
|
|
527
|
+
singlePort: {
|
|
528
|
+
http: true,
|
|
529
|
+
websocket: true,
|
|
530
|
+
grpc: true,
|
|
531
|
+
grpcWeb: true,
|
|
532
|
+
},
|
|
533
|
+
})
|
|
376
534
|
```
|
|
377
535
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
536
|
+
---
|
|
537
|
+
|
|
538
|
+
## File-Based Routing
|
|
539
|
+
|
|
540
|
+
Drop files into a directory. Raffel discovers and registers them automatically.
|
|
381
541
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
542
|
+
```
|
|
543
|
+
routes/
|
|
544
|
+
users/
|
|
545
|
+
index.ts → GET /users
|
|
546
|
+
[id].ts → GET /users/:id
|
|
547
|
+
[id]/posts.ts → GET /users/:id/posts
|
|
548
|
+
tcp/
|
|
549
|
+
echo.ts → TCP handler "echo"
|
|
550
|
+
udp/
|
|
551
|
+
ping.ts → UDP handler "ping"
|
|
552
|
+
```
|
|
387
553
|
|
|
388
|
-
|
|
554
|
+
```typescript
|
|
555
|
+
const server = createServer({
|
|
556
|
+
port: 3000,
|
|
557
|
+
discovery: { dir: './routes', watch: true }, // hot-reload in dev
|
|
558
|
+
})
|
|
559
|
+
```
|
|
389
560
|
|
|
390
561
|
---
|
|
391
562
|
|
|
392
|
-
##
|
|
563
|
+
## Testing Mocks
|
|
393
564
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
565
|
+
A complete mock infrastructure for integration tests — no external services needed.
|
|
566
|
+
|
|
567
|
+
```typescript
|
|
568
|
+
import { MockServiceSuite } from 'raffel'
|
|
569
|
+
|
|
570
|
+
const suite = new MockServiceSuite()
|
|
571
|
+
await suite.start()
|
|
572
|
+
|
|
573
|
+
const { http, ws, tcp, udp, dns, sse, proxy } = suite
|
|
574
|
+
|
|
575
|
+
// HTTP mock with request recording
|
|
576
|
+
http.onGet('/users', { body: [{ id: '1' }] })
|
|
577
|
+
const requests = await http.waitForRequests(1)
|
|
578
|
+
|
|
579
|
+
// WebSocket mock with pattern responses
|
|
580
|
+
ws.setResponse(/ping/, 'pong')
|
|
581
|
+
ws.dropRate = 0.1 // simulate 10% packet loss
|
|
582
|
+
|
|
583
|
+
// DNS mock
|
|
584
|
+
dns.addRecord('api.example.com', 'A', '127.0.0.1')
|
|
585
|
+
|
|
586
|
+
// SSE mock
|
|
587
|
+
sse.emit('data', { event: 'update', data: '{"count":42}' })
|
|
588
|
+
|
|
589
|
+
await suite.stop()
|
|
408
590
|
```
|
|
409
591
|
|
|
592
|
+
| Mock | Features |
|
|
593
|
+
|------|---------|
|
|
594
|
+
| `MockHttpServer` | CORS, global delay, streaming, `times`, statistics |
|
|
595
|
+
| `MockWebSocketServer` | Pattern responses, drop rate, max connections, auto-close |
|
|
596
|
+
| `MockTcpServer` | Echo + custom handlers |
|
|
597
|
+
| `MockUdpServer` | UDP responder |
|
|
598
|
+
| `MockDnsServer` | DNS over UDP (RFC 1035), no deps |
|
|
599
|
+
| `MockSSEServer` | Server-Sent Events |
|
|
600
|
+
| `MockProxyServer` | HTTP forward + MITM with hooks |
|
|
601
|
+
|
|
410
602
|
---
|
|
411
603
|
|
|
412
|
-
##
|
|
604
|
+
## Validation
|
|
413
605
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
606
|
+
Bring your own validator. Raffel adapts to it.
|
|
607
|
+
|
|
608
|
+
```typescript
|
|
609
|
+
import { registerValidator, createZodAdapter } from 'raffel'
|
|
610
|
+
import { z } from 'zod'
|
|
611
|
+
|
|
612
|
+
registerValidator(createZodAdapter(z))
|
|
613
|
+
|
|
614
|
+
server
|
|
615
|
+
.procedure('users.create')
|
|
616
|
+
.input(z.object({ name: z.string().min(2), email: z.string().email() }))
|
|
617
|
+
.handler(async (input) => db.users.create(input))
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
Adapters available: `createZodAdapter`, `createYupAdapter`, `createJoiAdapter`, `createAjvAdapter`, `createFastestValidatorAdapter`.
|
|
424
621
|
|
|
425
622
|
---
|
|
426
623
|
|
|
427
624
|
## MCP Server (AI Integration)
|
|
428
625
|
|
|
429
|
-
Raffel
|
|
626
|
+
Raffel ships an MCP server for AI-assisted development. It gives tools like Claude direct knowledge of your API.
|
|
430
627
|
|
|
431
628
|
```bash
|
|
432
629
|
# Add to Claude Code
|
|
433
630
|
claude mcp add raffel npx raffel-mcp
|
|
434
631
|
|
|
435
632
|
# Or run directly
|
|
436
|
-
npx raffel-mcp
|
|
633
|
+
npx raffel-mcp
|
|
437
634
|
```
|
|
438
635
|
|
|
439
|
-
|
|
636
|
+
Provides: live documentation, code generation prompts (`add_oauth2`, `add_sessions`, etc.), and pattern guidance.
|
|
637
|
+
|
|
638
|
+
---
|
|
639
|
+
|
|
640
|
+
## Migration from Hono
|
|
641
|
+
|
|
642
|
+
Raffel's `HttpApp` is intentionally Hono-compatible. Most migrations are a find-and-replace:
|
|
643
|
+
|
|
644
|
+
```diff
|
|
645
|
+
- import { Hono } from 'hono'
|
|
646
|
+
- import { serve } from '@hono/node-server'
|
|
647
|
+
+ import { HttpApp, serve } from 'raffel'
|
|
648
|
+
|
|
649
|
+
- const app = new Hono()
|
|
650
|
+
+ const app = new HttpApp()
|
|
651
|
+
|
|
652
|
+
app.get('/users', async (c) => c.json(await db.users.findMany()))
|
|
653
|
+
|
|
654
|
+
- serve(app)
|
|
655
|
+
+ serve({
|
|
656
|
+
+ fetch: app.fetch,
|
|
657
|
+
+ port: 3000,
|
|
658
|
+
+ keepAliveTimeout: 65000, // recommended for production
|
|
659
|
+
+ headersTimeout: 66000,
|
|
660
|
+
+ })
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
Everything else is identical: routes, middleware signature, context API, `app.route()`, `app.notFound()`, `app.onError()`.
|
|
664
|
+
|
|
665
|
+
See [full migration guide](./docs/guides/migration.md) for advanced patterns.
|
|
666
|
+
|
|
667
|
+
---
|
|
668
|
+
|
|
669
|
+
## Documentation
|
|
670
|
+
|
|
671
|
+
| Topic | Description |
|
|
672
|
+
|-------|-------------|
|
|
673
|
+
| [Quick Start](https://forattini-dev.github.io/raffel/#/quickstart) | 5-minute guide |
|
|
674
|
+
| [HTTP Guide](https://forattini-dev.github.io/raffel/#/protocols/http) | REST, middleware, routing, serve() |
|
|
675
|
+
| [Authentication](https://forattini-dev.github.io/raffel/#/auth/overview) | JWT, API Key, OAuth2, OIDC, Sessions |
|
|
676
|
+
| [Interceptors](https://forattini-dev.github.io/raffel/#/interceptors) | Rate limit, circuit breaker, cache, etc. |
|
|
677
|
+
| [WebSocket](https://forattini-dev.github.io/raffel/#/protocols/websocket) | Real-time, channels, presence |
|
|
678
|
+
| [Proxy Suite](https://forattini-dev.github.io/raffel/#/proxy) | Forward, CONNECT, SOCKS5, transparent |
|
|
679
|
+
| [Metrics & Tracing](https://forattini-dev.github.io/raffel/#/observability) | Prometheus, OpenTelemetry |
|
|
680
|
+
| [Core Model](https://forattini-dev.github.io/raffel/#/core-model) | Envelope, Context, Router, architecture |
|
|
681
|
+
| [File-based Routing](https://forattini-dev.github.io/raffel/#/file-system-discovery) | Zero-config discovery |
|
|
682
|
+
| [Migration from Hono](./docs/guides/migration.md) | Step-by-step migration guide |
|
|
440
683
|
|
|
441
684
|
---
|
|
442
685
|
|