Custom REST Endpoints
VoltAgent Server allows you to add custom REST endpoints alongside the built-in agent and workflow endpoints. This enables extending your API with business logic, integrations, and custom functionality.
Overview
With @voltagent/server-hono, you can add custom routes using the configureApp callback which gives you direct access to the Hono app instance.
Basic Setup
Add custom endpoints through the server configuration:
import { VoltAgent } from "@voltagent/core";
import { honoServer } from "@voltagent/server-hono";
new VoltAgent({
agents: { myAgent },
server: honoServer({
configureApp: (app) => {
// Add custom routes here
app.get("/api/health", (c) => c.json({ status: "healthy" }));
app.post("/api/data", async (c) => {
const body = await c.req.json();
// Process data
return c.json({ success: true, data: body });
});
},
}),
});
CORS Configuration
Configure Cross-Origin Resource Sharing (CORS) for your API using the cors field in server configuration. By default, VoltAgent allows all origins (*).
Default CORS (Permissive)
new VoltAgent({
agents: { myAgent },
server: honoServer({
// Default: allows all origins
}),
});
Custom CORS Settings
new VoltAgent({
agents: { myAgent },
server: honoServer({
cors: {
origin: "https://your-domain.com",
allowHeaders: ["X-Custom-Header", "Content-Type", "Authorization"],
allowMethods: ["POST", "GET", "OPTIONS"],
exposeHeaders: ["Content-Length"],
maxAge: 600,
credentials: true,
},
}),
});
Multiple Origins
new VoltAgent({
agents: { myAgent },
server: honoServer({
cors: {
origin: ["https://app1.com", "https://app2.com", "https://app3.com"],
credentials: true,
},
}),
});
Dynamic Origin
new VoltAgent({
agents: { myAgent },
server: honoServer({
cors: {
origin: (origin) => {
// Allow specific domains
const allowedDomains = ["app1.com", "app2.com"];
if (allowedDomains.some((domain) => origin.includes(domain))) {
return origin;
}
return undefined; // Reject
},
credentials: true,
},
}),
});
Route-Specific CORS
For advanced use cases where different routes need different CORS policies, disable the default CORS and configure route-specific CORS in configureApp:
import { cors } from "hono/cors";
new VoltAgent({
agents: { myAgent },
server: honoServer({
cors: false, // Disable default CORS
configureApp: (app) => {
// Agent routes - strict CORS
app.use(
"/agents/*",
cors({
origin: "https://agents-app.com",
credentials: true,
}),
);
// Public API - permissive CORS
app.use(
"/api/public/*",
cors({
origin: "*",
}),
);
// Admin routes - very strict CORS
app.use(
"/api/admin/*",
cors({
origin: ["https://admin.com", "https://admin-staging.com"],
credentials: true,
allowMethods: ["GET", "POST"],
}),
);
// Your custom routes
app.get("/api/public/status", (c) => c.json({ status: "ok" }));
app.get("/api/admin/stats", (c) => c.json({ stats: {...} }));
},
}),
});
Important: When cors: false, you must manually configure CORS for all routes that need it, including VoltAgent's built-in routes.
Route Patterns
Hono supports various route patterns:
Static Routes
configureApp: (app) => {
app.get("/api/status", (c) => c.json({ status: "ok" }));
app.post("/api/users", async (c) => {
/* ... */
});
app.put("/api/settings", async (c) => {
/* ... */
});
app.delete("/api/cache", async (c) => {
/* ... */
});
};
Path Parameters
configureApp: (app) => {
// Single parameter
app.get("/api/users/:id", (c) => {
const userId = c.req.param("id");
return c.json({ userId });
});
// Multiple parameters
app.get("/api/posts/:postId/comments/:commentId", (c) => {
const postId = c.req.param("postId");
const commentId = c.req.param("commentId");
return c.json({ postId, commentId });
});
// Optional parameters with regex
app.get("/api/files/:filename{.+\\.pdf}", (c) => {
const filename = c.req.param("filename");
return c.json({ pdf: filename });
});
};
Wildcards
configureApp: (app) => {
// Match any path after /api/
app.get("/api/*", (c) => {
const path = c.req.path;
return c.json({ path });
});
};
Request Handling
Query Parameters
app.get("/api/search", (c) => {
// Single value
const query = c.req.query("q");
// Multiple values for same key
const tags = c.req.queries("tag");
// All query parameters
const allParams = c.req.query();
return c.json({
query,
tags,
allParams,
});
});
Request Body
// JSON body
app.post("/api/json", async (c) => {
const body = await c.req.json();
return c.json({ received: body });
});
// Form data
app.post("/api/form", async (c) => {
const formData = await c.req.formData();
const name = formData.get("name");
return c.json({ name });
});
// Text body
app.post("/api/text", async (c) => {
const text = await c.req.text();
return c.text(`Received: ${text}`);
});
// Raw body
app.post("/api/raw", async (c) => {
const buffer = await c.req.arrayBuffer();
return c.json({ size: buffer.byteLength });
});
Headers
app.get("/api/headers", (c) => {
// Get specific header
const auth = c.req.header("Authorization");
const contentType = c.req.header("Content-Type");
// Get all headers
const headers = c.req.header();
return c.json({ auth, contentType, headers });
});
Response Types
JSON Response
app.get("/api/data", (c) => {
return c.json(
{ success: true, data: { id: 1, name: "Item" } },
200 // Optional status code
);
});
Text Response
app.get("/api/text", (c) => {
return c.text("Hello World", 200);
});
HTML Response
app.get("/api/html", (c) => {
return c.html("<h1>Hello World</h1>", 200);
});
File Response
app.get("/api/file", async (c) => {
const file = await readFile("./data.pdf");
return c.body(file, 200, {
"Content-Type": "application/pdf",
"Content-Disposition": 'attachment; filename="data.pdf"',
});
});
Redirect
app.get("/api/redirect", (c) => {
return c.redirect("/new-location", 301);
});
Custom Headers
app.get("/api/custom", (c) => {
c.header("X-Custom-Header", "value");
c.header("Cache-Control", "max-age=3600");
return c.json({ data: "with custom headers" });
});
Middleware
Add middleware to your custom routes:
Route-Specific Middleware
import { logger } from "hono/logger";
import { compress } from "hono/compress";
configureApp: (app) => {
// Apply to specific routes
app.use("/api/*", logger());
app.use("/api/*", compress());
// Custom middleware
app.use("/api/admin/*", async (c, next) => {
// Check admin access
const user = c.get("authenticatedUser");
if (!user?.roles?.includes("admin")) {
return c.json({ error: "Admin access required" }, 403);
}
await next();
});
};
Request Validation
import { z } from "zod";
import { zValidator } from "@hono/zod-validator";
const userSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
age: z.number().min(0).max(120),
});
configureApp: (app) => {
app.post("/api/users", zValidator("json", userSchema), async (c) => {
const data = c.req.valid("json");
// data is typed and validated
return c.json({ success: true, user: data });
});
};
Error Handling
Try-Catch Pattern
app.get("/api/risky", async (c) => {
try {
const result = await riskyOperation();
return c.json({ success: true, data: result });
} catch (error) {
logger.error("Operation failed:", error);
return c.json({ success: false, error: error.message }, 500);
}
});
Global Error Handler
configureApp: (app) => {
// Add error handler
app.onError((err, c) => {
console.error("Global error:", err);
return c.json(
{
success: false,
error: err.message,
stack: process.env.NODE_ENV === "development" ? err.stack : undefined,
},
500
);
});
// Add not found handler
app.notFound((c) => {
return c.json({ success: false, error: "Route not found" }, 404);
});
};
Route Groups
Organize related routes:
configureApp: (app) => {
// Create a sub-application
const api = app.basePath("/api/v2");
// User routes
api.get("/users", listUsers);
api.get("/users/:id", getUser);
api.post("/users", createUser);
api.put("/users/:id", updateUser);
api.delete("/users/:id", deleteUser);
// Product routes
api.get("/products", listProducts);
api.get("/products/:id", getProduct);
api.post("/products", createProduct);
};
Integration Examples
Database Integration
import { db } from "./database";
configureApp: (app) => {
app.get("/api/items", async (c) => {
const items = await db.query("SELECT * FROM items");
return c.json({ success: true, data: items });
});
app.post("/api/items", async (c) => {
const data = await c.req.json();
const result = await db.insert("items", data);
return c.json({ success: true, id: result.id }, 201);
});
};
External API Integration
configureApp: (app) => {
app.get("/api/weather/:city", async (c) => {
const city = c.req.param("city");
const response = await fetch(`https://api.weather.com/v1/weather?city=${city}`, {
headers: { "API-Key": process.env.WEATHER_API_KEY },
});
const weather = await response.json();
return c.json({ success: true, data: weather });
});
};
WebSocket Upgrade
import { createWebSocketHandler } from "./websocket";
configureApp: (app) => {
app.get("/api/ws", (c) => {
// Upgrade to WebSocket
const wsHandler = createWebSocketHandler();
return wsHandler(c.req.raw, c.env);
});
};
Authentication for Custom Endpoints
Important: Custom routes added via configureApp are registered AFTER the authentication middleware. This means when you configure an auth provider, your custom routes automatically inherit the same authentication behavior as VoltAgent's built-in routes.
How Authentication Works with Custom Routes
VoltAgent applies authentication middleware to all routes before configureApp is called. This ensures your custom endpoints have the same security posture as built-in endpoints.
// Authentication flow:
// 1. CORS middleware applied
// 2. Auth middleware applied (if configured)
// 3. VoltAgent built-in routes registered
// 4. configureApp called → your custom routes registered
Opt-In Mode (Default)
By default (defaultPrivate: false), only execution endpoints require authentication. Custom routes are public unless they match a protected pattern:
import { jwtAuth } from "@voltagent/server-core";
new VoltAgent({
agents: { myAgent },
server: honoServer({
auth: jwtAuth({
secret: process.env.JWT_SECRET,
// defaultPrivate: false (default)
}),
configureApp: (app) => {
// Public endpoint (doesn't match protected patterns)
app.get("/api/public-data", (c) => {
return c.json({ data: "anyone can access this" });
});
// Also public (you can access authenticatedUser if it exists)
app.get("/api/optional-auth", (c) => {
const user = c.get("authenticatedUser");
if (user) {
return c.json({ message: `Hello, ${user.email}` });
}
return c.json({ message: "Hello, anonymous" });
});
},
}),
});
Opt-Out Mode (Recommended)
Set defaultPrivate: true to protect all routes by default, including custom endpoints. Then selectively make routes public using publicRoutes:
import { jwtAuth } from "@voltagent/server-core";
new VoltAgent({
agents: { myAgent },
server: honoServer({
auth: jwtAuth({
secret: process.env.JWT_SECRET,
defaultPrivate: true, // Protect all routes by default
publicRoutes: ["GET /api/health", "GET /api/status", "POST /api/webhooks/*"],
}),
configureApp: (app) => {
// Public endpoint (in publicRoutes)
app.get("/api/health", (c) => c.json({ status: "ok" }));
// Protected endpoint (requires authentication)
app.get("/api/user/profile", (c) => {
const user = c.get("authenticatedUser");
return c.json({ user }); // user is guaranteed to exist
});
// All custom routes are protected unless in publicRoutes
app.post("/api/data", async (c) => {
const user = c.get("authenticatedUser");
const body = await c.req.json();
// Process authenticated request
return c.json({ success: true, userId: user.id });
});
},
}),
});
Benefits of Opt-Out Mode:
- ✅ Automatic protection for all custom endpoints
- ✅ No need to manually check authentication in each route
- ✅ Better security by default (fail-safe)
- ✅ Easier to maintain when using third-party auth providers (Clerk, Auth0)
- ✅ Consistent auth behavior across all routes
Best Practices
1. Consistent Response Format
// Create a standard response helper
const apiResponse = (success: boolean, data?: any, error?: string) => ({
success,
...(data && { data }),
...(error && { error }),
timestamp: new Date().toISOString(),
});
app.get("/api/example", (c) => {
return c.json(apiResponse(true, { message: "Hello" }));
});
2. Input Validation
Always validate user input:
app.post("/api/data", async (c) => {
const body = await c.req.json();
// Validate required fields
if (!body.name || !body.email) {
return c.json(apiResponse(false, null, "Missing required fields"), 400);
}
// Process valid data
return c.json(apiResponse(true, body));
});
3. Async Error Handling
Use try-catch for async operations:
app.get("/api/async", async (c) => {
try {
const result = await someAsyncOperation();
return c.json(apiResponse(true, result));
} catch (error) {
logger.error("Async operation failed:", error);
return c.json(apiResponse(false, null, "Internal server error"), 500);
}
});
4. Rate Limiting
Protect endpoints from abuse:
import { rateLimiter } from "hono-rate-limiter";
configureApp: (app) => {
app.use(
"/api/*",
rateLimiter({
windowMs: 15 * 60 * 1000, // 15 minutes
limit: 100, // Max requests per window
standardHeaders: "draft-6",
keyGenerator: (c) => c.req.header("x-forwarded-for") || "anonymous",
})
);
};
Testing Custom Endpoints
# Test GET endpoint
curl http://localhost:3141/api/health
# Test POST with JSON
curl -X POST http://localhost:3141/api/users \
-H "Content-Type: application/json" \
-d '{"name": "John", "email": "[email protected]"}'
# Test with authentication
curl http://localhost:3141/api/protected \
-H "Authorization: Bearer $TOKEN"
# Test with query parameters
curl "http://localhost:3141/api/search?q=test&limit=10"
Next Steps
- Learn about Authentication to secure custom endpoints
- Check API Reference for complete endpoint list
- Explore Server Architecture for advanced configuration