Raffel is a TypeScript runtime for building APIs, proxies, and traffic-aware infrastructure in one stack.
- Build HTTP, WebSocket, gRPC, JSON-RPC, GraphQL, TCP, and UDP services with one project.
- Run reverse proxy, explicit proxy, CONNECT MITM, SOCKS5/SOCKS5h, and transparent proxy modes.
- Export service-mesh style telemetry with
source -> destination -> protocol, p50/p90/p95, throughput, bytes, and error rates. - Add resilience and policy with rate limit, retry, timeout, circuit breaker, validation, auth/session, filters, and proxy middleware.
Heavy edge features stay off by default. Telemetry, graph snapshots, Prometheus exporters, and proxy middleware only run when configured.
Need the right guide fast? Use the docs or the built-in MCP tools: raffel_feature_catalog, raffel_proxy_capabilities, raffel_get_guide, and raffel_search.
- Protocols: HTTP, WebSocket, gRPC, UDP
- Proxies: Overview, Modes, Routing, Service Mesh
- Observability: Flow Metrics, Observability, Tracing
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.
pnpm add raffelimport { 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 })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 })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}`),
})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.
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.
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":"alice@example.com"}'
# WebSocket
wscat -c ws://localhost:3000/ws
> {"method":"users.create","params":{"name":"Alice","email":"alice@example.com"}}
# JSON-RPC 2.0
curl -X POST http://localhost:3000/rpc \
-d '{"jsonrpc":"2.0","method":"users.create","params":{...},"id":1}'// 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 })
}
})server
.event('emails.send')
.delivery('at-least-once')
.handler(async (payload, ctx, ack) => {
await sendEmail(payload)
ack()
})| 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 |
| Docs UI | Generate USD/OpenAPI reference and render file-backed Markdown Markdown guides |
| 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 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 |
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).
Full proxy toolkit built into Raffel — no extra dependencies.
import { createHttpForwardProxy } from 'raffel'
const proxy = createHttpForwardProxy({
auth: { credentials: { username: 'admin', password: 'secret' } },
filter: {
allowHosts: ['*.trusted.com', 'api.internal'],
denyHosts: ['*.evil.com'],
},
onRequest: (req) => { /* log or modify */ return req },
})
proxy.attachTo(httpServer)import { createConnectTunnel } from 'raffel'
// Plain CONNECT tunnel
const tunnel = createConnectTunnel({ mode: 'forward' })
// 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
})import { createProxySuite } from 'raffel'
const suite = createProxySuite({
explicit: {
port: 8080,
middleware: [
async (ctx, next) => {
if (ctx.kind === 'http-request' || ctx.kind === 'mitm-request') {
ctx.request.headers['x-raffel-policy'] = 'edge-a'
}
if (ctx.kind === 'connect' && ctx.target.host.endsWith('.blocked.internal')) {
ctx.blocked = { statusCode: 403, reason: 'blocked by policy' }
return
}
if (ctx.kind === 'upgrade-request' && ctx.target.host === 'ws.internal.local') {
ctx.target.port = 4102
}
await next()
},
],
tunnel: { mode: 'mitm' },
},
socks5: {
port: 1080,
middleware: [
async (ctx, next) => {
if (ctx.kind === 'socks5-connect' && ctx.target.host === 'db.internal') {
ctx.target.host = 'db-replica.internal'
}
await next()
},
],
},
})One middleware model now spans reverse, explicit, MITM, SOCKS5/SOCKS5h, and transparent flows. Use it to inspect, block, rewrite destinations, shape headers, and attach policy logic only where configured.
import { createExplicitProxy } from 'raffel'
const proxy = createExplicitProxy({
port: 8080,
auth: { credentials: { username: 'admin', password: 'secret' } },
tunnel: { mode: 'forward' },
telemetry: {
sourceHeader: 'x-service-name',
metricsEndpoint: '/metrics',
graphEndpoint: '/proxy/graph',
defaultLabels: { proxy: 'mesh-edge-a' },
percentiles: [0.5, 0.9, 0.95],
},
})
await proxy.start()
// Prometheus: GET http://127.0.0.1:8080/metrics
// Graph JSON: GET http://127.0.0.1:8080/proxy/graph
// Programmatic: proxy.graphSnapshot()
// Edge latency: snapshot.edges[0].latency.percentiles.p95import { createSocks5Proxy } from 'raffel'
const socks5 = createSocks5Proxy({
port: 1080,
auth: { credentials: { username: 'alice', password: 'secret' } },
telemetry: {
defaultLabels: { proxy: 'mesh-socks' },
},
})
await socks5.start()
// Supports CONNECT, BIND, and UDP ASSOCIATE.
// SOCKS5h is supported by sending hostnames (ATYP 0x03).
// Telemetry protocols: socks5, socks5h, socks5-bind, socks5h-bind, socks5-udp, socks5h-udp.import { createProxySuite } from 'raffel'
const suite = createProxySuite({
explicit: {
port: 8080,
tunnel: { mode: 'forward' }, // HTTPS via CONNECT
},
socks5: {
port: 1080,
auth: { credentials: { username: 'svc-billing', password: 'secret' } },
},
telemetry: {
sourceHeader: 'x-service-name',
metricsEndpoint: '/metrics',
graphEndpoint: '/proxy/graph',
percentiles: [0.5, 0.9, 0.95],
defaultLabels: { proxy: 'mesh-gateway' },
},
})
await suite.start()
// Shared graph across HTTP, HTTPS/CONNECT, and SOCKS5 CONNECT/BIND/UDP:
// GET http://127.0.0.1:8080/proxy/graph
// Shared Prometheus metrics:
// GET http://127.0.0.1:8080/metricsimport { createTransparentProxy } from 'raffel'
const proxy = createTransparentProxy({
mode: 'tproxy',
port: 8080,
})
await proxy.start()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 }).
import { createServer } from 'raffel'
const server = createServer({ port: 3000 })
.enableUSD({
basePath: '/docs',
info: { title: 'My API', version: '1.0.0' },
docsDir: true,
ui: {
assets: { mode: 'external' },
sidebar: { search: true, docsPages: true, subMaxLevel: 3 },
markdown: { autoHeader: true, formatUpdated: 'YYYY-MM-DD', html: 'escape' },
},
})
await server.start()This gives you one documentation surface:
/docsrenders generated USD/OpenAPI API reference plus Markdown guides./docs/usd.jsonand/docs/usd.yamlexpose the full USD contract./docs/openapi.jsonexposes pure OpenAPI 3.1 for Swagger/OpenAPI tooling.docsDir: trueautomatically loads the project./docsdirectory with file-backed MarkdownREADME.md,_sidebar.md,_navbar.md,_coverpage.md, and_404.mdconventions.
basePath is the real HTTP mount path, so use it to avoid API route
collisions. docsDir.routeBase is only the in-app Markdown route prefix:
server.enableUSD({
basePath: '/internal-docs',
info: { title: 'My API', version: '1.0.0' },
docsDir: {
dir: './docs',
routeBase: '/guides',
},
})Here the docs UI lives at /internal-docs, while docs/quickstart.md renders
inside the UI as #/guides/quickstart. It does not create a real API route at
/guides/quickstart.
Use USD/OpenAPI docs for API truth, schemas, routes, auth, and protocols. Use Markdown docs for free-form guides, tutorials, runbooks, architecture notes, and product documentation. The UI supports light, dark, and auto theme modes, and the theme toggle persists the user's preference locally. See Docs UI for the full setup.
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',
}))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 }))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.startupControl 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: ['*'],
},
})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,
},
})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
})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 |
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.
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.
Declarative, opt-in authz that replaces scattered if (user.role !== ...) checks. Define policies once, gate procedures with .authz(), enforce across every protocol (HTTP, WS, gRPC, TCP, …) automatically.
const server = createServer({
port: 3000,
policy: {
principal: { from: 'session' },
policies: [
{
id: 'allow-active-leads',
effect: 'allow',
principals: ['scope:lead.read'],
actions: ['lead.read'],
resources: ['lead:*'],
match: { 'resource.status': 'active' },
},
],
},
})
server
.procedure('lead.read')
.authz({ resource: ({ id }, ctx) => ({ type: 'lead', id, tenantId: ctx.principal?.tenantId }) })
.handler(async ({ id }) => loadLead(id))Features:
- Allow/deny rules with principals, actions, resources, match conditions
- Match DSL:
eq,in,regex,gt/gte/lt/lte,exists,and/or/not - Pluggable engine via
policy.engine(default in-memory, or BYO) - Zero bundle/runtime cost when not configured
- Discoverable via the MCP server for AI-assisted authoring
See Policies overview, Match DSL, and Patterns.
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.
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.
Browse the full site at forattini-dev.github.io/raffel or read the Markdown sources directly under docs/.
| Topic | Description |
|---|---|
| Quick Start | 5-minute guide |
| Architecture | Envelope, Context, Router, runtime model |
| HTTP Guide | REST, middleware, routing, serve() |
| WebSocket | Real-time, channels, presence |
| gRPC / TCP / UDP / JSON-RPC / SMTP | All other transports |
| Authentication | JWT, API Key, OAuth2, OIDC, Sessions |
| Authorization Policies | Declarative authz, match DSL, patterns |
| Interceptors | Rate limit, circuit breaker, cache, retry, fallback, … |
| Proxy Suite | Forward, CONNECT, SOCKS5, transparent |
| Observability | Prometheus metrics, OpenTelemetry tracing, logging |
| File-based Routing | Zero-config discovery |
| Docs UI | USD/OpenAPI reference plus Markdown guides |
| Migration Guide | Concept mapping from Express, Fastify, ws, Socket.IO |
| Guide | What you build |
|---|---|
| Multi-protocol service | One server exposing HTTP + WS + gRPC at once |
| REST API | Production-ready REST with validation and auth |
| Authentication flow | OAuth2 + sessions wired end-to-end |
| Authorization policies | Replacing imperative checks with declarative rules |
| Reverse proxy | Forwarding, CONNECT MITM, cert pinning |
| Webhook edge | Signed inbound webhooks |
| SMTP server / relay | Custom mail handling |
| MCP server | Exposing your API to AI tools |
| Docs MCP | Live, AI-readable docs |
ISC
Documentation · GitHub · npm