Skip to content

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 client

Development 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

Released under the MIT License.