Framework Examples
This guide shows how to integrate Helix Agents with popular HTTP frameworks. The @helix-agents/ai-sdk package produces framework-agnostic responses that work with any HTTP framework.
Response Format
All handlers return:
typescript
interface FrontendResponse {
status: number;
headers: Record<string, string>;
body: ReadableStream<Uint8Array> | string;
}Hono
Hono works natively with Web standard Response:
typescript
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { createFrontendHandler, 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 handler = createFrontendHandler({
streamManager,
executor,
agent: MyAgent,
stateStore,
});
// Create app
const app = new Hono();
// CORS for frontend
app.use(
'/api/*',
cors({
origin: ['http://localhost:3000'],
allowHeaders: ['Content-Type', 'Last-Event-ID'],
exposeHeaders: ['X-Stream-Id'],
})
);
// Execute agent (POST)
app.post('/api/chat', async (c) => {
try {
const body = await c.req.json();
const response = await handler.handleRequest({
method: 'POST',
body: {
message: body.message ?? body.messages?.[body.messages.length - 1]?.content,
state: body.state,
},
});
return new Response(response.body, {
status: response.status,
headers: response.headers,
});
} 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;
}
});
// Stream existing execution (GET)
app.get('/api/chat/:streamId', async (c) => {
try {
const streamId = c.req.param('streamId');
const lastEventId = c.req.header('Last-Event-ID');
const response = await handler.handleRequest({
method: 'GET',
streamId,
resumeAt: lastEventId ? parseInt(lastEventId) : undefined,
});
return new Response(response.body, {
status: response.status,
headers: response.headers,
});
} 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;
}
});
// Load message history
app.get('/api/messages/:runId', async (c) => {
try {
const runId = c.req.param('runId');
const offset = parseInt(c.req.query('offset') ?? '0');
const limit = parseInt(c.req.query('limit') ?? '100');
const { messages, hasMore } = await handler.getMessages(runId, {
offset,
limit,
});
return c.json({ messages, hasMore });
} 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;
}
});
export default app;Express
Express requires piping the ReadableStream:
typescript
import express from 'express';
import cors from 'cors';
import { createFrontendHandler, FrontendHandlerError } from '@helix-agents/ai-sdk';
import { pipeToExpress, sendErrorToExpress } 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 handler = createFrontendHandler({
streamManager,
executor,
agent: MyAgent,
stateStore,
});
// Create app
const app = express();
// Middleware
app.use(
cors({
origin: ['http://localhost:3000'],
allowedHeaders: ['Content-Type', 'Last-Event-ID'],
exposedHeaders: ['X-Stream-Id'],
})
);
app.use(express.json());
// Execute agent (POST)
app.post('/api/chat', async (req, res) => {
try {
const { message, messages, state } = req.body;
const response = await handler.handleRequest({
method: 'POST',
body: {
message: message ?? messages?.[messages.length - 1]?.content,
state,
},
});
// Use helper to pipe response
await pipeToExpress(response, res);
} catch (error) {
if (error instanceof FrontendHandlerError) {
sendErrorToExpress(error, res);
return;
}
console.error('Unexpected error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Stream existing execution (GET)
app.get('/api/chat/:streamId', async (req, res) => {
try {
const { streamId } = req.params;
const lastEventId = req.get('Last-Event-ID');
const response = await handler.handleRequest({
method: 'GET',
streamId,
resumeAt: lastEventId ? parseInt(lastEventId) : undefined,
});
await pipeToExpress(response, res);
} catch (error) {
if (error instanceof FrontendHandlerError) {
sendErrorToExpress(error, res);
return;
}
console.error('Unexpected error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// Load message history
app.get('/api/messages/:runId', async (req, res) => {
try {
const { runId } = req.params;
const offset = parseInt((req.query.offset as string) ?? '0');
const limit = parseInt((req.query.limit as string) ?? '100');
const { messages, hasMore } = await handler.getMessages(runId, {
offset,
limit,
});
res.json({ messages, hasMore });
} catch (error) {
if (error instanceof FrontendHandlerError) {
sendErrorToExpress(error, res);
return;
}
console.error('Unexpected error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
app.listen(3001, () => {
console.log('Server running on http://localhost:3001');
});Express Middleware Helper
For cleaner code, use the middleware helper:
typescript
import { createExpressMiddleware } from '@helix-agents/ai-sdk/adapters/express';
const chatMiddleware = createExpressMiddleware(handler, (req) => ({
method: req.method as 'GET' | 'POST',
streamId: req.params.streamId,
resumeAt: req.get('Last-Event-ID') ? parseInt(req.get('Last-Event-ID')!) : undefined,
body: req.body?.message ? { message: req.body.message, state: req.body.state } : undefined,
}));
app.post('/api/chat', chatMiddleware);
app.get('/api/chat/:streamId', chatMiddleware);Fastify
Fastify with streaming support:
typescript
import Fastify from 'fastify';
import cors from '@fastify/cors';
import { createFrontendHandler, FrontendHandlerError } from '@helix-agents/ai-sdk';
const fastify = Fastify({ logger: true });
// CORS
await fastify.register(cors, {
origin: ['http://localhost:3000'],
allowedHeaders: ['Content-Type', 'Last-Event-ID'],
exposedHeaders: ['X-Stream-Id'],
});
// Setup handler (same as above)
const handler = createFrontendHandler({
/* ... */
});
// Execute agent
fastify.post('/api/chat', async (request, reply) => {
try {
const { message, state } = request.body as { message: string; state?: Record<string, unknown> };
const response = await handler.handleRequest({
method: 'POST',
body: { message, state },
});
// Set headers
for (const [key, value] of Object.entries(response.headers)) {
reply.header(key, value);
}
// Stream response
if (typeof response.body === 'string') {
return reply.status(response.status).send(response.body);
}
reply.status(response.status);
return reply.send(response.body);
} 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
Native support for Web standards:
typescript
import { createFrontendHandler, FrontendHandlerError } from '@helix-agents/ai-sdk';
import { CloudflareAgentExecutor } from '@helix-agents/runtime-cloudflare';
import { D1StateStore, DOStreamManager } from '@helix-agents/store-cloudflare';
export interface Env {
DB: D1Database;
STREAM_MANAGER: DurableObjectNamespace;
AGENT_WORKFLOW: Workflow;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
// Setup
const stateStore = new D1StateStore(env.DB);
const streamManager = new DOStreamManager(env.STREAM_MANAGER);
const executor = new CloudflareAgentExecutor({
workflowBinding: env.AGENT_WORKFLOW,
stateStore,
streamManager,
});
const handler = createFrontendHandler({
streamManager,
executor,
agent: MyAgent,
stateStore,
});
// 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',
'Access-Control-Expose-Headers': 'X-Stream-Id',
},
});
}
try {
// POST /api/chat
if (url.pathname === '/api/chat' && request.method === 'POST') {
const body = (await request.json()) as { message: string; state?: Record<string, unknown> };
const response = await handler.handleRequest({
method: 'POST',
body: { message: body.message, state: body.state },
});
const headers = new Headers(response.headers);
headers.set('Access-Control-Allow-Origin', '*');
return new Response(response.body, {
status: response.status,
headers,
});
}
// GET /api/chat/:streamId
if (url.pathname.startsWith('/api/chat/') && request.method === 'GET') {
const streamId = url.pathname.split('/').pop()!;
const lastEventId = request.headers.get('Last-Event-ID');
const response = await handler.handleRequest({
method: 'GET',
streamId,
resumeAt: lastEventId ? parseInt(lastEventId) : undefined,
});
const headers = new Headers(response.headers);
headers.set('Access-Control-Allow-Origin', '*');
return new Response(response.body, {
status: response.status,
headers,
});
}
return new Response('Not found', { status: 404 });
} catch (error) {
if (error instanceof FrontendHandlerError) {
return Response.json(
{ error: error.message, code: error.code },
{
status: error.statusCode,
headers: { 'Access-Control-Allow-Origin': '*' },
}
);
}
throw error;
}
},
};CORS Configuration
AI SDK's useChat makes requests from the browser. Configure CORS properly:
Required Headers
typescript
// Allow headers
'Content-Type'; // JSON body
'Last-Event-ID'; // SSE resumption
// Expose headers
'X-Stream-Id'; // Return stream ID to clientDevelopment CORS
typescript
// Hono
app.use(
'/api/*',
cors({
origin: ['http://localhost:3000', 'http://localhost:5173'],
allowHeaders: ['Content-Type', 'Last-Event-ID'],
exposeHeaders: ['X-Stream-Id'],
})
);
// Express
app.use(
cors({
origin: ['http://localhost:3000', 'http://localhost:5173'],
allowedHeaders: ['Content-Type', 'Last-Event-ID'],
exposedHeaders: ['X-Stream-Id'],
})
);Production CORS
typescript
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-Stream-Id'],
credentials: true,
})
);Authentication
Add authentication before the handler:
typescript
// 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', async (c) => {
// Authenticate first
let user;
try {
user = await authenticate(c.req.raw);
} catch {
return c.json({ error: 'Unauthorized' }, 401);
}
// Then handle request
const body = await c.req.json();
const response = await handler.handleRequest({
method: 'POST',
body: {
message: body.message,
state: { ...body.state, userId: user.userId },
},
});
return new Response(response.body, {
status: response.status,
headers: response.headers,
});
});Rate Limiting
typescript
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:
typescript
import { z } from 'zod';
const ChatRequestSchema = z.object({
message: z.string().min(1).max(10000),
state: z.record(z.unknown()).optional(),
});
app.post('/api/chat', async (c) => {
const body = await c.req.json();
// Validate
const result = ChatRequestSchema.safeParse(body);
if (!result.success) {
return c.json(
{
error: 'Invalid request',
details: result.error.issues,
},
400
);
}
// Handle valid request
const response = await handler.handleRequest({
method: 'POST',
body: result.data,
});
return new Response(response.body, {
status: response.status,
headers: response.headers,
});
});Next Steps
- React Integration - Building the frontend
- AI SDK Package - Handler configuration
- Cloudflare Runtime - Full Cloudflare setup