raffel
v1.0.19
Published
Unified multi-protocol server runtime. One core, multiple transports.
Maintainers
Readme
Raffel
One server. HTTP, WebSocket, gRPC, TCP, UDP — all at once.
Quick Start · Full Documentation · Examples · Migration Guide
Start with HTTP
import { HttpApp, serve } from 'raffel'
const app = new HttpApp()
app.get('/users', async (c) => {
return c.json(await db.users.findMany())
})
app.get('/users/:id', async (c) => {
const user = await db.users.findById(c.req.param('id'))
if (!user) return c.json({ error: 'Not found' }, 404)
return c.json(user)
})
app.post('/users', async (c) => {
const body = await c.req.json()
return c.json(await db.users.create(body), 201)
})
serve({ fetch: app.fetch, port: 3000 })HttpApp gives Raffel a native Fetch-style HTTP front door. Start with familiar
route and middleware concepts, then expand the same contracts across protocols.
Quick Start
pnpm add raffelHello World
import { HttpApp, serve } from 'raffel'
const app = new HttpApp()
app.get('/hello/:name', (c) => c.text(`Hello, ${c.req.param('name')}!`))
serve({ fetch: app.fetch, port: 3000 })CRUD in 30 Seconds
import { HttpApp, serve } from 'raffel'
const app = new HttpApp()
const users = new Map<string, unknown>()
app.get('/users', (c) => c.json([...users.values()]))
app.get('/users/:id', (c) => {
const user = users.get(c.req.param('id'))
return user ? c.json(user) : c.json({ error: 'Not found' }, 404)
})
app.post('/users', async (c) => {
const user = { id: crypto.randomUUID(), ...(await c.req.json()) }
users.set(user.id, user)
return c.json(user, 201)
})
app.put('/users/:id', async (c) => {
const id = c.req.param('id')
if (!users.has(id)) return c.json({ error: 'Not found' }, 404)
const user = { id, ...(await c.req.json()) }
users.set(id, user)
return c.json(user)
})
app.delete('/users/:id', (c) => {
const id = c.req.param('id')
return users.delete(id)
? c.json({ success: true })
: c.json({ error: 'Not found' }, 404)
})
serve({ fetch: app.fetch, port: 3000 })Production-Ready serve()
serve({
fetch: app.fetch,
port: 3000,
keepAliveTimeout: 65000, // slightly above load balancer idle timeout
headersTimeout: 66000,
onListen: ({ port, hostname }) => console.log(`Listening on ${hostname}:${port}`),
})Developer Experience In 2026
Raffel now ships with an inspection-first workflow for multi-protocol services:
# optional, for a global CLI
npm i -g raffel
npx raffel new api my-service
cd my-service
pnpm install
npx raffel inspect src/server.ts
npx raffel explain "users.list" src/server.ts
npx raffel doctor src/server.ts
npx raffel playground src/server.ts --port 4301
npx raffel contract-tests src/server.ts
pnpm devThe same runtime graph powers:
server.preview()raffel inspectraffel explainraffel doctorraffel playgroundraffel contract-tests- OpenAPI/USD output
That keeps your docs, runtime bindings, local tooling, and contract checks aligned.
Wait, There's More
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.
The Procedure API
import { createServer } from 'raffel'
import { z } from 'zod'
const server = createServer({
port: 3000,
websocket: { path: '/ws' },
jsonrpc: { path: '/rpc' },
})
server
.procedure('users.create')
.input(z.object({ name: z.string().min(2), email: z.string().email() }))
.output(z.object({ id: z.string(), name: z.string(), email: z.string() }))
.handler(async (input, ctx) => {
return db.users.create(input)
})
await server.start()Same handler. Every protocol. Zero extra code.
# HTTP
curl -X POST http://localhost:3000/users \
-d '{"name":"Alice","email":"[email protected]"}'
# WebSocket
wscat -c ws://localhost:3000/ws
> {"method":"users.create","params":{"name":"Alice","email":"[email protected]"}}
# JSON-RPC 2.0
curl -X POST http://localhost:3000/rpc \
-d '{"jsonrpc":"2.0","method":"users.create","params":{...},"id":1}'Streaming
// Server → client stream
server
.stream('logs.tail')
.handler(async function* ({ file }) {
for await (const line of readLines(file)) {
yield { line, timestamp: Date.now() }
}
})
// Bidirectional stream
server
.stream('chat.session')
.bidi()
.handler(async (stream, ctx) => {
for await (const msg of stream) {
await stream.write({ echo: msg, from: ctx.auth?.userId })
}
})Events with Delivery Guarantees
server
.event('emails.send')
.delivery('at-least-once')
.handler(async (payload, ctx, ack) => {
await sendEmail(payload)
ack()
})The Full Picture
| Module | What it does |
|--------|-------------|
| HTTP | Native HTTP front door + serve() with production timeouts |
| WebSocket | Real-time adapter + Pusher-like channels (public/private/presence) |
| gRPC | Full gRPC adapter with TLS and streaming |
| JSON-RPC 2.0 | Batch + notification + error codes per spec |
| GraphQL | Schema-first adapter with subscriptions |
| TCP / UDP | Raw socket handlers with connection filters |
| Single-Port | Sniff protocol on one port — HTTP, WS, gRPC, gRPC-Web all on :3000 |
| Interceptors | Rate limit, circuit breaker, retry, timeout, cache, bulkhead, and more |
| Session Store | Memory + Redis drivers with lazy load + auto-save |
| Proxy Suite | HTTP forward, CONNECT tunnel (MITM), SOCKS5, transparent |
| Metrics | Prometheus-style counters, gauges, histograms with exporters |
| Tracing | OpenTelemetry spans with Jaeger / Zipkin exporters |
| OpenAPI | Generate spec from schemas + serve ReDoc / Swagger UI |
| Channels | Pusher-like pub/sub with presence and authorization |
| MCP Server | Model Context Protocol for AI-assisted development |
| Testing | Full mock suite: HTTP, WS, TCP, UDP, DNS, SSE, Proxy |
| Validation | Plug in Zod, Yup, Joi, Ajv, or fastest-validator |
Interceptors
Interceptors are reusable middleware that compose cleanly across any protocol.
import {
createRateLimitInterceptor,
createCircuitBreakerInterceptor,
createRetryInterceptor,
createTimeoutInterceptor,
createCacheInterceptor,
createLoggingInterceptor,
createTracingInterceptor,
} from 'raffel'
server
.procedure('users.list')
.use(createTimeoutInterceptor({ timeout: 5000 }))
.use(createRateLimitInterceptor({ limit: 100, window: '1m' }))
.use(createCacheInterceptor({ ttl: 60, store: cacheStore }))
.use(createLoggingInterceptor())
.handler(async () => db.users.findMany())Apply globally, per-group, or per-procedure:
// Global
server.use(createTracingInterceptor({ tracer }))
server.use(createLoggingInterceptor())
// Group / module
const adminModule = createRouterModule('admin', [requireAdmin])
adminModule.procedure('users.delete').handler(...)
// Per-procedure
server.procedure('payments.charge')
.use(createCircuitBreakerInterceptor({ threshold: 5, timeout: 30000 }))
.use(createRetryInterceptor({ attempts: 3, backoff: 'exponential' }))
.handler(...)| Interceptor | Purpose |
|-------------|---------|
| createRateLimitInterceptor | Token bucket / sliding window (memory, Redis, filesystem) |
| createCircuitBreakerInterceptor | Auto-open after failures, half-open probe |
| createBulkheadInterceptor | Concurrency isolation per procedure |
| createRetryInterceptor | Exponential backoff with jitter |
| createTimeoutInterceptor | Per-phase, cascading, deadline propagation |
| createCacheInterceptor | Read-through / write-through (memory, file, Redis) |
| createDedupInterceptor | In-flight request deduplication |
| createSizeLimitInterceptor | Request / response size guard |
| createFallbackInterceptor | Return default on failure |
| createRequestIdInterceptor | Inject/propagate correlation IDs |
| createLoggingInterceptor | Structured request/response logging |
| createMetricsInterceptor | Auto-instrument with Prometheus metrics |
| createTracingInterceptor | OpenTelemetry span creation |
| createSessionInterceptor | Session load/save via memory or Redis |
| createValidationInterceptor | Schema validation on input/output |
| createAuthMiddleware | Bearer token, API key strategies |
Channels (Real-Time Pub/Sub)
Pusher-compatible channel model over WebSocket.
import { createChannelManager } from 'raffel'
const channels = createChannelManager(
{
authorize: async (socketId, channel, ctx) => {
// private-* and presence-* channels require auth
return { authorized: !!ctx.auth }
},
presence: {
onJoin: (channel, member) => broadcastPresence(channel),
onLeave: (channel, member) => broadcastPresence(channel),
},
},
(socketId, message) => ws.sendToClient(socketId, message)
)
// Subscribe
await channels.subscribe(socketId, 'presence-room:42', ctx)
// Broadcast to all subscribers
channels.broadcast('presence-room:42', 'new-message', { text: 'Hello!' })
// Get online members
const members = channels.getMembers('presence-room:42')Channel types: public-* (anyone), private-* (authorized), presence-* (auth + member tracking).
Proxy Suite
Full proxy toolkit built into Raffel — no extra dependencies.
HTTP Forward Proxy
import { createHttpForwardProxy } from 'raffel'
const proxy = createHttpForwardProxy(httpServer, {
auth: { type: 'basic', credentials: { admin: 'secret' } },
filter: {
allowHosts: ['*.trusted.com', 'api.internal'],
denyHosts: ['*.evil.com'],
},
onRequest: (req) => { /* log or modify */ return req },
})CONNECT Tunnel (with MITM)
import { createConnectTunnel } from 'raffel'
// Transparent tunnel
const tunnel = createConnectTunnel({ mode: 'pipe' })
// MITM: inspect and modify HTTPS traffic
const mitm = createConnectTunnel({
mode: 'mitm',
onRequest: (req) => {
req.headers['x-intercepted'] = 'true'
return req
},
onResponse: (res) => {
res.headers['x-inspected'] = 'true'
return res
},
onUpstreamCert: (cert) => trustedCerts.has(cert.fingerprint), // cert pinning
})SOCKS5 Proxy
import { createSocks5Proxy } from 'raffel'
const socks5 = createSocks5Proxy({
port: 1080,
auth: { type: 'userpass', users: { alice: 'secret' } },
})
await socks5.start()Transparent Proxy (Linux TPROXY)
import { createTransparentProxy } from 'raffel'
const proxy = createTransparentProxy({
mode: 'tproxy',
port: 8080,
upstream: { host: 'backend.internal', port: 8080 },
})Session Store
import { createSessionInterceptor, createRedisSessionDriver } from 'raffel'
const sessions = createSessionInterceptor({
driver: createRedisSessionDriver({ client: redis }),
cookie: { name: 'sid', httpOnly: true, secure: true, sameSite: 'lax' },
ttl: 86400,
})
server.use(sessions)
server.procedure('auth.me').handler(async (_, ctx) => {
// ctx.session is loaded lazily, saved automatically
const { userId } = ctx.session.get()
return db.users.findById(userId)
})Drivers: createMemorySessionDriver(), createRedisSessionDriver({ client }).
OpenAPI + Docs UI
import { mountOpenApiDocs } from 'raffel'
server.enableUSD({
info: { title: 'My API', version: '1.0.0' },
})
const spec = server.getOpenAPIDocument()
if (!spec) throw new Error('OpenAPI document is not available')
// Mount /openapi.json + /docs (ReDoc or Swagger UI)
mountOpenApiDocs(app, {
spec,
ui: 'redoc', // or 'swagger'
path: '/docs',
})Metrics & Tracing
Prometheus Metrics
import { createMetricRegistry, createMetricsInterceptor, exportPrometheus } from 'raffel'
const metrics = createMetricRegistry()
server.use(createMetricsInterceptor({ registry: metrics }))
// Expose /metrics endpoint
app.get('/metrics', (c) => c.text(exportPrometheus(metrics), 200, {
'Content-Type': 'text/plain; version=0.0.4',
}))OpenTelemetry Tracing
import { createTracer, createTracingInterceptor, createJaegerExporter } from 'raffel'
const tracer = createTracer({
serviceName: 'my-api',
exporter: createJaegerExporter({ endpoint: 'http://jaeger:14268/api/traces' }),
sampler: createProbabilitySampler(0.1), // 10% sampling
})
server.use(createTracingInterceptor({ tracer }))Health Checks
import { createHealthCheckProcedures, CommonProbes } from 'raffel'
const health = createHealthCheckProcedures({
probes: [
CommonProbes.memory({ maxHeapMb: 512 }),
CommonProbes.uptime(),
{
name: 'database',
check: async () => {
await db.ping()
return { status: 'healthy' }
},
},
],
})
server.mount('/', health)
// Registers: health.live, health.ready, health.startupConnection Filters
Control who can connect to your TCP, UDP, and WebSocket adapters.
import { createTcpAdapter } from 'raffel'
const tcp = createTcpAdapter(router, {
connectionFilter: {
allowHosts: ['10.0.0.*', 'trusted.internal'],
denyHosts: ['*.untrusted.net'],
onDenied: (host, port) => logger.warn(`Blocked connection from ${host}:${port}`),
},
})WebSocket adds origin filtering:
const ws = createWebSocketAdapter(router, {
connectionFilter: {
allowOrigins: ['https://app.example.com'],
denyOrigins: ['*'],
},
})Single-Port Multi-Protocol
Run HTTP, WebSocket, gRPC, and gRPC-Web all on the same port. Raffel sniffs the protocol from the first bytes.
const server = createServer({
port: 3000,
singlePort: {
http: true,
websocket: true,
grpc: true,
grpcWeb: true,
},
})File-Based Routing
Drop files into a directory. Raffel discovers and registers them automatically.
routes/
users/
index.ts → GET /users
[id].ts → GET /users/:id
[id]/posts.ts → GET /users/:id/posts
tcp/
echo.ts → TCP handler "echo"
udp/
ping.ts → UDP handler "ping"const server = createServer({
port: 3000,
discovery: { dir: './routes', watch: true }, // hot-reload in dev
})Testing Mocks
A complete mock infrastructure for integration tests — no external services needed.
import { MockServiceSuite } from 'raffel'
const suite = new MockServiceSuite()
await suite.start()
const { http, ws, tcp, udp, dns, sse, proxy } = suite
// HTTP mock with request recording
http.onGet('/users', { body: [{ id: '1' }] })
const requests = await http.waitForRequests(1)
// WebSocket mock with pattern responses
ws.setResponse(/ping/, 'pong')
ws.dropRate = 0.1 // simulate 10% packet loss
// DNS mock
dns.addRecord('api.example.com', 'A', '127.0.0.1')
// SSE mock
sse.emit('data', { event: 'update', data: '{"count":42}' })
await suite.stop()| Mock | Features |
|------|---------|
| MockHttpServer | CORS, global delay, streaming, times, statistics |
| MockWebSocketServer | Pattern responses, drop rate, max connections, auto-close |
| MockTcpServer | Echo + custom handlers |
| MockUdpServer | UDP responder |
| MockDnsServer | DNS over UDP (RFC 1035), no deps |
| MockSSEServer | Server-Sent Events |
| MockProxyServer | HTTP forward + MITM with hooks |
Spec-Driven Mock Server
You can also stand up mock endpoints directly from OpenAPI or USD documents:
import { createMockServer } from 'raffel'
const openapi = server.getOpenAPIDocument()
if (!openapi) throw new Error('OpenAPI document is not available')
await createMockServer({
spec: openapi,
port: 4100,
})This gives you:
- HTTP routes extracted from documented endpoints
- example-first responses with schema-generated fallback data
- request validation from the same contract
- optional JSON-RPC and WebSocket mocks when the source document is USD
It is useful for frontend handoff, local integration tests, and spec-first development.
Validation
Bring your own validator. Raffel adapts to it.
import { registerValidator, createZodAdapter } from 'raffel'
import { z } from 'zod'
registerValidator(createZodAdapter(z))
server
.procedure('users.create')
.input(z.object({ name: z.string().min(2), email: z.string().email() }))
.handler(async (input) => db.users.create(input))Adapters available: createZodAdapter, createYupAdapter, createJoiAdapter, createAjvAdapter, createFastestValidatorAdapter.
MCP Server (AI Integration)
Raffel ships an MCP server for AI-assisted development. It gives tools like Claude direct knowledge of your API.
# Add to Claude Code
claude mcp add raffel npx raffel-mcp
# Or run directly
npx raffel-mcpProvides: live documentation, code generation prompts (add_oauth2, add_sessions, etc.), and pattern guidance.
Migrating an Existing HTTP App
Raffel can front an existing HTTP application model, but its goal is bigger than HTTP parity. Migrate by mapping routes, middleware, validation, and lifecycle concepts into Raffel's runtime model, then reuse the same contracts across other transports.
See the migration guide for concept mapping from
Express, Fastify, Fetch-first routers, ws, and Socket.IO.
Documentation
| Topic | Description | |-------|-------------| | Quick Start | 5-minute guide | | HTTP Guide | REST, middleware, routing, serve() | | Authentication | JWT, API Key, OAuth2, OIDC, Sessions | | Interceptors | Rate limit, circuit breaker, cache, etc. | | WebSocket | Real-time, channels, presence | | Proxy Suite | Forward, CONNECT, SOCKS5, transparent | | Metrics & Tracing | Prometheus, OpenTelemetry | | Core Model | Envelope, Context, Router, architecture | | File-based Routing | Zero-config discovery | | Migration Guide | Concept mapping from existing HTTP and realtime stacks |
License
ISC
Documentation · GitHub · npm
