Creating HTTP Routes
Define GET, POST, and other HTTP endpoints with createRouter()
Routes define how your application responds to HTTP requests. Built on Hono, the router provides a familiar Express-like API with full TypeScript support.
Routes Location
All routes live in src/api/. Import agents you need and call them directly.
Basic Routes
Create routes using createRouter():
import { createRouter } from '@agentuity/runtime';
const router = createRouter();
router.get('/', async (c) => {
return c.json({ status: 'healthy' });
});
router.post('/process', async (c) => {
const body = await c.req.json();
return c.json({ received: body });
});
export default router;Automatic Response Conversion
Return values are automatically converted: string → text response, object → JSON, ReadableStream → streamed response. You can also use explicit methods (c.json(), c.text()) for more control.
HTTP Methods
The router supports all standard HTTP methods:
router.get('/items', handler); // Read
router.post('/items', handler); // Create
router.put('/items/:id', handler); // Replace
router.patch('/items/:id', handler); // Update
router.delete('/items/:id', handler); // DeleteRoute Parameters
Capture URL segments with :paramName:
router.get('/users/:id', async (c) => {
const userId = c.req.param('id');
return c.json({ userId });
});
router.get('/posts/:year/:month/:slug', async (c) => {
const { year, month, slug } = c.req.param();
return c.json({ year, month, slug });
});Wildcard Parameters
For paths with variable depth, use regex patterns:
router.get('/files/:bucket/:key{.*}', async (c) => {
const bucket = c.req.param('bucket');
const key = c.req.param('key'); // Captures "path/to/file.txt"
return c.json({ bucket, key });
});
// GET /files/uploads/images/photo.jpg → { bucket: "uploads", key: "images/photo.jpg" }Query Parameters
Access query strings with c.req.query():
router.get('/search', async (c) => {
const query = c.req.query('q');
const page = c.req.query('page') || '1';
const limit = c.req.query('limit') || '10';
return c.json({ query, page, limit });
});
// GET /search?q=hello&page=2 → { query: "hello", page: "2", limit: "10" }Calling Agents
Import and call agents directly:
import { createRouter } from '@agentuity/runtime';
import assistant from '@agent/assistant';
const router = createRouter();
router.post('/chat', async (c) => {
const { message } = await c.req.json();
const response = await assistant.run({ message });
return c.json(response);
});
export default router;For background processing, use c.waitUntil():
import webhookProcessor from '@agent/webhook-processor';
router.post('/webhook', async (c) => {
const payload = await c.req.json();
// Process in background, respond immediately
c.waitUntil(async () => {
await webhookProcessor.run(payload);
});
return c.json({ status: 'accepted' });
});Request Validation
Use agent.validator() for type-safe request validation with full TypeScript support:
import { createRouter } from '@agentuity/runtime';
import userCreator from '@agent/user-creator';
const router = createRouter();
// Validates using agent's input schema
router.post('/users', userCreator.validator(), async (c) => {
const data = c.req.valid('json'); // Fully typed from agent schema
const user = await userCreator.run(data);
return c.json(user);
});
export default router;For custom validation (different from the agent's schema), pass a schema override:
import { type } from 'arktype';
import userCreator from '@agent/user-creator';
router.post('/custom',
userCreator.validator({ input: type({ email: 'string.email' }) }),
async (c) => {
const data = c.req.valid('json'); // Typed as { email: string }
return c.json(data);
}
);Validator Overloads
agent.validator() supports three signatures:
agent.validator()— Uses agent's input/output schemasagent.validator({ output: schema })— Output-only validation (GET-compatible)agent.validator({ input: schema, output?: schema })— Custom schemas
Request Context
The context object (c) provides access to request data and Agentuity services:
Request data:
await c.req.json(); // Parse JSON body
await c.req.text(); // Get raw text body
c.req.param('id'); // Route parameter
c.req.query('page'); // Query string
c.req.header('Authorization'); // Request headerResponses:
c.json({ data }); // JSON response
c.text('OK'); // Plain text
c.html('<h1>Hello</h1>'); // HTML response
c.redirect('/other'); // RedirectAgentuity services:
// Import agents at the top of your file
import myAgent from '@agent/my-agent';
await myAgent.run(input); // Call an agent
c.var.kv.get('bucket', 'key'); // Key-value storage
c.var.vector.search('ns', opts); // Vector search
c.var.logger.info('message'); // LoggingBest Practices
Validate input
Always validate request bodies, especially for public endpoints:
router.post('/api', agent.validator(), async (c) => {
const data = c.req.valid('json');
// data is guaranteed valid and fully typed
});Use structured logging
Use c.var.logger instead of console.log for searchable, traceable logs:
c.var.logger.info('Request processed', { userId, duration: Date.now() - start });
c.var.logger.error('Processing failed', { error: err.message });Order routes correctly
Register specific routes before generic ones:
// Correct: specific before generic
router.get('/users/me', getCurrentUser);
router.get('/users/:id', getUserById);
// Wrong: :id matches "me" first
router.get('/users/:id', getUserById);
router.get('/users/me', getCurrentUser); // Never reachedUse middleware for cross-cutting concerns
Apply middleware to all routes with router.use():
router.use(loggingMiddleware);
router.use(authMiddleware);
router.get('/protected', handler); // Both middlewares applyFor authentication patterns, rate limiting, and more, see Middleware.
Handle errors gracefully
Return appropriate status codes when things go wrong:
import processor from '@agent/processor';
router.post('/process', async (c) => {
try {
const body = await c.req.json();
const result = await processor.run(body);
return c.json(result);
} catch (error) {
c.var.logger.error('Processing failed', { error });
return c.json({ error: 'Processing failed' }, 500);
}
});Streaming Responses
Use router.stream() to return a ReadableStream directly to the client without buffering:
import chatAgent from '@agent/chat';
router.stream('/chat', async (c) => {
const body = await c.req.json();
return chatAgent.run(body); // Returns a ReadableStream
});Creating Custom Streams
Return any ReadableStream for custom streaming:
router.stream('/events', (c) => {
return new ReadableStream({
start(controller) {
controller.enqueue('data: event 1\n\n');
controller.enqueue('data: event 2\n\n');
controller.close();
}
});
});With Middleware
Stream routes support middleware:
import streamAgent from '@agent/stream';
router.stream('/protected', authMiddleware, async (c) => {
return streamAgent.run({ userId: c.var.userId });
});Stream vs SSE vs WebSocket
| Type | Direction | Format | Use Case |
|---|---|---|---|
router.stream() | Server → Client | Raw bytes | LLM responses, file downloads |
router.sse() | Server → Client | SSE events | Progress updates, notifications |
router.websocket() | Bidirectional | Messages | Chat, collaboration |
Use router.stream() for raw streaming (like AI SDK textStream). Use router.sse() when you need named events or auto-reconnection. See Streaming Responses for the full guide on streaming agents.
Next Steps
- Middleware: Authentication, rate limiting, logging
- Scheduled Jobs (Cron): Run tasks on a schedule
- Email Handling: Process incoming emails
- WebSockets: Real-time bidirectional communication
- Server-Sent Events: Stream updates to clients
Need Help?
Join our Community for assistance or just to hang with other humans building agents.
Send us an email at hi@agentuity.com if you'd like to get in touch.
Please Follow us on
If you haven't already, please Signup for your free account now and start building your first agent!