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": "john@example.com"}'
# 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