Framework Examples
Web Response everywhere
v7+ routes call handleChatStream from @helix-agents/ai-sdk directly — it returns a web Response (status + headers + SSE body) that any framework can forward. See Frontend / @helix-agents/ai-sdk for the seven-path orchestrator and buildSnapshot / getUIMessages helpers.
This guide shows how to integrate Helix Agents with popular HTTP frameworks. handleChatStream returns a web Response, which Hono / Fastify / Cloudflare Workers can return directly; Express needs the pipeWebResponseToExpress / createWebResponseExpressMiddleware helpers from @helix-agents/ai-sdk/adapters/express.
Hono
Hono works natively with Web standard Response — handleChatStream returns one directly:
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import {
handleChatStream,
buildSnapshot,
getUIMessages,
FrontendHandlerError,
} from '@helix-agents/ai-sdk';
import { JSAgentExecutor } from '@helix-agents/runtime-js';
import { InMemoryStateStore, InMemoryStreamManager } from '@helix-agents/store-memory';
import { VercelAIAdapter } from '@helix-agents/llm-vercel';
// Setup
const stateStore = new InMemoryStateStore();
const streamManager = new InMemoryStreamManager();
const executor = new JSAgentExecutor(stateStore, streamManager, new VercelAIAdapter());
const deps = {
executor,
stateStore,
streamManager,
agent: MyAgent,
contentReplayEnabled: true,
} as const;
// Create app
const app = new Hono();
// CORS for frontend
app.use(
'/api/*',
cors({
origin: ['http://localhost:3000'],
allowHeaders: [
'Content-Type',
'Last-Event-ID',
'X-Resume-From-Sequence',
'X-Existing-Message-Id',
],
exposeHeaders: ['X-Session-Id'],
})
);
// POST /api/chat/:sessionId — fresh turn / continuing turn / resume.
app.post('/api/chat/:sessionId', async (c) => {
try {
const body = await c.req.json();
return await handleChatStream(deps, {
sessionId: c.req.param('sessionId'),
messages: body.messages ?? [],
request: c.req.raw,
});
} catch (error) {
if (error instanceof FrontendHandlerError) {
return c.json(
{ error: error.message, code: error.code },
error.statusCode as 400 | 404 | 410 | 500 | 501
);
}
throw error;
}
});
// GET /api/chat/:sessionId — reconnect to a live stream (path 5/6).
app.get('/api/chat/:sessionId', async (c) => {
return handleChatStream(deps, {
sessionId: c.req.param('sessionId'),
messages: [],
request: c.req.raw,
});
});
// GET /api/chat/:sessionId/snapshot — SSR hydration data.
app.get('/api/chat/:sessionId/snapshot', async (c) => {
const snapshot = await buildSnapshot(deps, { sessionId: c.req.param('sessionId') });
if (!snapshot) return c.json({ error: 'Not found' }, 404);
return c.json(snapshot);
});
// GET /api/messages/:sessionId — paginated message history.
app.get('/api/messages/:sessionId', async (c) => {
const sessionId = c.req.param('sessionId');
const offset = parseInt(c.req.query('offset') ?? '0');
const limit = parseInt(c.req.query('limit') ?? '100');
const result = await getUIMessages({ stateStore }, { sessionId, offset, limit });
return c.json(result);
});
export default app;Express
Express needs the web-Response → Express adapter helpers from @helix-agents/ai-sdk/adapters/express. The cleanest path is createWebResponseExpressMiddleware, which wraps a handleChatStream-style handler and pipes the Response into res for you:
import express from 'express';
import cors from 'cors';
import {
handleChatStream,
buildSnapshot,
getUIMessages,
FrontendHandlerError,
} from '@helix-agents/ai-sdk';
import {
createWebResponseExpressMiddleware,
pipeWebResponseToExpress,
} from '@helix-agents/ai-sdk/adapters/express';
import { JSAgentExecutor } from '@helix-agents/runtime-js';
import { InMemoryStateStore, InMemoryStreamManager } from '@helix-agents/store-memory';
import { VercelAIAdapter } from '@helix-agents/llm-vercel';
// Setup
const stateStore = new InMemoryStateStore();
const streamManager = new InMemoryStreamManager();
const executor = new JSAgentExecutor(stateStore, streamManager, new VercelAIAdapter());
const deps = {
executor,
stateStore,
streamManager,
agent: MyAgent,
contentReplayEnabled: true,
} as const;
// Adapt the Express `req` into a web `Request` the orchestrator can read.
function toWebRequest(req: express.Request): Request {
const url = new URL(req.originalUrl, `http://${req.headers.host ?? 'localhost'}`);
const headers = new Headers();
for (const [k, v] of Object.entries(req.headers)) {
if (typeof v === 'string') headers.set(k, v);
else if (Array.isArray(v)) headers.set(k, v.join(', '));
}
return new Request(url, {
method: req.method,
headers,
body: req.method === 'GET' || req.method === 'HEAD' ? undefined : JSON.stringify(req.body),
});
}
// Create app
const app = express();
app.use(
cors({
origin: ['http://localhost:3000'],
allowedHeaders: [
'Content-Type',
'Last-Event-ID',
'X-Resume-From-Sequence',
'X-Existing-Message-Id',
],
exposedHeaders: ['X-Session-Id'],
})
);
app.use(express.json());
// POST /api/chat/:sessionId — fresh turn / continuing turn / resume.
app.post(
'/api/chat/:sessionId',
createWebResponseExpressMiddleware(async (req) => {
return handleChatStream(deps, {
sessionId: req.params.sessionId,
messages: req.body?.messages ?? [],
request: toWebRequest(req),
});
})
);
// GET /api/chat/:sessionId — reconnect to a live stream.
app.get(
'/api/chat/:sessionId',
createWebResponseExpressMiddleware(async (req) =>
handleChatStream(deps, {
sessionId: req.params.sessionId,
messages: [],
request: toWebRequest(req),
})
)
);
// GET /api/chat/:sessionId/snapshot — SSR hydration JSON.
app.get('/api/chat/:sessionId/snapshot', async (req, res) => {
const snapshot = await buildSnapshot(deps, { sessionId: req.params.sessionId });
if (!snapshot) {
res.status(404).json({ error: 'Not found' });
return;
}
res.json(snapshot);
});
// GET /api/messages/:sessionId — paginated history.
app.get('/api/messages/:sessionId', async (req, res) => {
const result = await getUIMessages(
{ stateStore },
{
sessionId: req.params.sessionId,
offset: req.query.offset ? Number(req.query.offset) : undefined,
limit: req.query.limit ? Number(req.query.limit) : undefined,
}
);
res.json(result);
});
// Centralized error handler — catches FrontendHandlerError thrown inside the
// middleware (e.g. ValidationError, StreamFailedError).
app.use(
(err: unknown, _req: express.Request, res: express.Response, next: express.NextFunction) => {
if (err instanceof FrontendHandlerError) {
res.status(err.statusCode).json({ error: err.message, code: err.code });
return;
}
next(err);
}
);
app.listen(3001, () => {
console.log('Server running on http://localhost:3001');
});Manual piping with pipeWebResponseToExpress
If you don't want the middleware wrapper, call handleChatStream yourself and pipe its Response into the Express response directly:
import { pipeWebResponseToExpress } from '@helix-agents/ai-sdk/adapters/express';
app.post('/api/chat/:sessionId', async (req, res) => {
const response = await handleChatStream(deps, {
sessionId: req.params.sessionId,
messages: req.body?.messages ?? [],
request: toWebRequest(req),
});
await pipeWebResponseToExpress(response, res);
});Fastify
Fastify with streaming support — call handleChatStream and forward the resulting Response body and headers:
import Fastify from 'fastify';
import cors from '@fastify/cors';
import { Readable } from 'node:stream';
import { handleChatStream, FrontendHandlerError } from '@helix-agents/ai-sdk';
const fastify = Fastify({ logger: true });
await fastify.register(cors, {
origin: ['http://localhost:3000'],
allowedHeaders: [
'Content-Type',
'Last-Event-ID',
'X-Resume-From-Sequence',
'X-Existing-Message-Id',
],
exposedHeaders: ['X-Session-Id'],
});
const deps = {
executor,
stateStore,
streamManager,
agent: MyAgent,
contentReplayEnabled: true,
} as const;
fastify.post('/api/chat/:sessionId', async (request, reply) => {
try {
const sessionId = (request.params as { sessionId: string }).sessionId;
const body = request.body as { messages?: unknown[] };
// Build a web Request to forward resume headers (Last-Event-ID, etc.).
const incoming = new Request(
new URL(request.url, `http://${request.headers.host ?? 'localhost'}`),
{
method: request.method,
headers: request.headers as Record<string, string>,
body: JSON.stringify(body),
}
);
const response = await handleChatStream(deps, {
sessionId,
messages: body.messages ?? [],
request: incoming,
});
response.headers.forEach((value, key) => reply.header(key, value));
reply.status(response.status);
if (!response.body) return reply.send();
return reply.send(Readable.fromWeb(response.body as never));
} catch (error) {
if (error instanceof FrontendHandlerError) {
return reply.status(error.statusCode).send({ error: error.message, code: error.code });
}
throw error;
}
});
await fastify.listen({ port: 3001 });Cloudflare Workers
For Durable-Object-backed deployments, use createCloudflareChatHandler from the @helix-agents/ai-sdk/cloudflare subpath. The factory wires the DO clients internally and exposes handleChat / getSnapshot / getMessages:
import { createCloudflareChatHandler } from '@helix-agents/ai-sdk/cloudflare';
import { FrontendHandlerError } from '@helix-agents/ai-sdk';
export interface Env {
AGENTS: DurableObjectNamespace;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const chat = createCloudflareChatHandler({
namespace: env.AGENTS,
agentName: 'chat-agent',
});
// CORS preflight
if (request.method === 'OPTIONS') {
return new Response(null, {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers':
'Content-Type, Last-Event-ID, X-Resume-From-Sequence, X-Existing-Message-Id',
'Access-Control-Expose-Headers': 'X-Session-Id',
},
});
}
const withCors = (response: Response): Response => {
const headers = new Headers(response.headers);
headers.set('Access-Control-Allow-Origin', '*');
return new Response(response.body, { status: response.status, headers });
};
try {
const match = url.pathname.match(/^\/api\/chat\/([^/]+)(\/snapshot)?$/);
if (match) {
const sessionId = decodeURIComponent(match[1]);
const isSnapshot = match[2] === '/snapshot';
if (isSnapshot && request.method === 'GET') {
const snapshot = await chat.getSnapshot({ sessionId });
if (!snapshot) {
return withCors(Response.json({ error: 'Not found' }, { status: 404 }));
}
return withCors(Response.json(snapshot));
}
if (request.method === 'POST' || request.method === 'GET') {
const body =
request.method === 'POST'
? ((await request.json()) as { messages?: unknown[] })
: { messages: [] };
const response = await chat.handleChat({
sessionId,
messages: (body.messages ?? []) as never,
request,
});
return withCors(response);
}
}
return withCors(new Response('Not found', { status: 404 }));
} catch (error) {
if (error instanceof FrontendHandlerError) {
return withCors(
Response.json({ error: error.message, code: error.code }, { status: error.statusCode })
);
}
throw error;
}
},
};CORS Configuration
AI SDK's useChat makes requests from the browser. Configure CORS properly:
Required Headers
// Allow headers
'Content-Type'; // JSON body
'Last-Event-ID'; // SSE resumption
// Expose headers
'X-Session-Id'; // Return stream ID to clientDevelopment CORS
// Hono
app.use(
'/api/*',
cors({
origin: ['http://localhost:3000', 'http://localhost:5173'],
allowHeaders: ['Content-Type', 'Last-Event-ID'],
exposeHeaders: ['X-Session-Id'],
})
);
// Express
app.use(
cors({
origin: ['http://localhost:3000', 'http://localhost:5173'],
allowedHeaders: ['Content-Type', 'Last-Event-ID'],
exposedHeaders: ['X-Session-Id'],
})
);Production CORS
const ALLOWED_ORIGINS = ['https://myapp.com', 'https://www.myapp.com'];
app.use(
'/api/*',
cors({
origin: (origin) => {
if (!origin || ALLOWED_ORIGINS.includes(origin)) {
return origin;
}
return null;
},
allowHeaders: ['Content-Type', 'Last-Event-ID'],
exposeHeaders: ['X-Session-Id'],
credentials: true,
})
);Authentication
Add authentication before the handler:
// Middleware approach
async function authenticate(req: Request): Promise<{ userId: string }> {
const token = req.headers.get('Authorization')?.replace('Bearer ', '');
if (!token) {
throw new Error('Unauthorized');
}
// Verify token...
return { userId: 'user-123' };
}
app.post('/api/chat/:sessionId', async (c) => {
// Authenticate first
let user;
try {
user = await authenticate(c.req.raw);
} catch {
return c.json({ error: 'Unauthorized' }, 401);
}
// Then dispatch through handleChatStream. Forward authenticated userId
// via the orchestrator's `userId` field for attribution.
const body = await c.req.json();
return handleChatStream(deps, {
sessionId: c.req.param('sessionId'),
messages: body.messages ?? [],
userId: user.userId,
request: c.req.raw,
});
});Rate Limiting
import { rateLimit } from 'hono-rate-limiter';
// Hono
app.use(
'/api/chat',
rateLimit({
windowMs: 60 * 1000, // 1 minute
limit: 20, // 20 requests per minute
keyGenerator: (c) => c.req.header('X-Forwarded-For') ?? 'unknown',
})
);Request Validation
Validate the incoming message:
import { z } from 'zod';
// AI SDK v6 UIMessage shape (subset — parts can be richer).
const UIMessagePartSchema = z.object({
type: z.string(),
text: z.string().optional(),
});
const UIMessageSchema = z.object({
id: z.string(),
role: z.enum(['user', 'assistant', 'system']),
parts: z.array(UIMessagePartSchema).min(1),
});
const ChatRequestSchema = z.object({
messages: z.array(UIMessageSchema).min(1).max(100),
});
app.post('/api/chat/:sessionId', async (c) => {
const body = await c.req.json();
const result = ChatRequestSchema.safeParse(body);
if (!result.success) {
return c.json({ error: 'Invalid request', details: result.error.issues }, 400);
}
return handleChatStream(deps, {
sessionId: c.req.param('sessionId'),
messages: result.data.messages,
request: c.req.raw,
});
});Next Steps
- React Integration - Building the frontend
- AI SDK Package - Handler configuration
- Cloudflare Runtime - Full Cloudflare setup