Routing Architecture
This document describes the routing implementation for both frontend and backend applications in the AgentPress monorepo.
Overview
AgentPress uses a dual routing architecture:
- Frontend: TanStack React Router with Vite
- Backend: Express.js with modular route organization
Frontend Routing (TanStack React Router)
Applications
| App | Port | Purpose | Route File |
|---|---|---|---|
| Console | 3000 | Admin dashboard | apps/console/src/routes.tsx |
| Portal | 3002 | User-facing chat interface | apps/portal/src/routes.tsx |
| Drop-in Chat | 3003 | Embeddable iframe widget | apps/dropin-chat/src/routes.tsx |
Router Initialization
All frontend apps follow the same initialization pattern in main.tsx:
import { createRouter, RouterProvider } from "@tanstack/react-router";
import { routeTree } from "./routes";
const router = createRouter({ routeTree });
declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
}
}
ReactDOM.createRoot(document.getElementById("root")!).render(
<RouterProvider router={router} />
);Root Route Pattern
Each application wraps routes in providers:
const rootRoute = createRootRoute({
component: () => (
<AgentPressProvider>
<Outlet />
<ReactQueryDevtools />
<Toaster />
</AgentPressProvider>
),
});
export const routeTree = rootRoute.addChildren(
createAppRoutes(rootRoute) as any,
);Configuration
Routes require configuration to be set before creating the route tree:
const config = {
apiHost: import.meta.env.VITE_API_HOST || "http://localhost:3001",
toolsUI,
voiceTranscription: true,
appType: EAppType.CONSOLE, // or PORTAL
};
setAgentPressConfig(config); // Must be called BEFORE creating routesConsole Route Structure
The console app uses dynamic route generation via createAppRoutes():
/ -> redirects to /dashboard/agents
Public Routes (PublicLayout - no sidebar):
/login
/invite?token=...
/reset-password?token=...
Protected Routes (ConsoleLayout - with sidebar):
/dashboard/
/agents
/agents/create
/agents/$id
/analytics
/conversations
/conversations/$threadId
/evaluations
/evaluations/$id
/evaluations/$id/edit
/feedback
/knowledge
/knowledge/$documentId
/mcps
/personas
/playground
/settings
/telemetry
/tools
/tools/create
/tools/$id
/users
/tasksPortal Route Structure
The portal app supports extensive customization via query parameters:
const chatSearchSchema = z.object({
agentId: z.string().optional().default(""),
showThreadList: z.boolean().optional().default(true),
showUsername: z.boolean().optional().default(true),
showAgentSelector: z.boolean().optional().default(true),
enableUpload: z.boolean().optional().default(true),
sidebarOpen: z.boolean().optional().default(true),
showNewThreadButton: z.boolean().optional().default(true),
showFeedback: z.boolean().optional().default(true),
showInputField: z.boolean().optional().default(true),
showUserHeaders: z.boolean().optional().default(false),
showTaskButton: z.boolean().optional().default(true),
initialUserMessage: z.string().optional().default(""),
authToken: z.string().optional().default(""),
providerName: z.string().optional().default(""),
threadId: z.string().optional().default(""),
});
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/",
validateSearch: chatSearchSchema,
component: ChatPage,
});Route Guards
Routes use before-load hooks for authentication:
// Public routes redirect authenticated users
beforeLoad: async ({ location }) => {
await redirectIfAuthenticated();
}
// Protected routes require authentication
beforeLoad: async ({ location }) => {
await requireAuth();
}Dynamic Routes
Dynamic route parameters use the $ prefix:
// Route: /agents/$id
const agentRoute = createRoute({
getParentRoute: () => dashboardRoute,
path: "agents/$id",
component: AgentDetailPage,
});
// Access parameter in component
function AgentDetailPage() {
const { id } = Route.useParams();
}Backend Routing (Express.js)
Architecture
The backend uses Express.js v5.1.0 with a modular route organization pattern.
Server Entry Point
File: apps/api/src/index.ts
export async function startServer({
agentsPath,
toolsPath,
externalApp,
seedAdminUser,
}) {
const app = createAgentPressApp({ externalApp });
// Initialize registries
await agentRegistry.initialize(agentsPath);
await mcpRegistry.initialize();
// Set up dependency injection
container.setDependencies({
toolRegistry,
agentRegistry,
pluginRegistry,
mcpRegistry,
taskRegistry,
});
// Register routes
const authRouter = createAuthRoute();
app.use("/auth", authRouter);
await registerExpressRoutes(app);
app.listen(port);
}Route Registration
File: packages/api/src/routes/index.ts
Routes are loaded dynamically from individual files:
export const registerExpressRoutes = async (app: Express) => {
const expressRoutes = [
{ file: "auth", prefix: "auth" },
{ file: "health", prefix: "health" },
{ file: "users", prefix: "users" },
{ file: "tools", prefix: "tools" },
{ file: "messages", prefix: "messages" },
{ file: "threads", prefix: "threads" },
{ file: "agents", prefix: "agents" },
{ file: "chat", prefix: "chat" },
{ file: "evaluations", prefix: "evaluations" },
{ file: "rag", prefix: "rag" },
{ file: "files", prefix: "files" },
{ file: "tasks", prefix: "tasks" },
// ... more routes
];
await Promise.all(
expressRoutes.map(async ({ file, prefix }) => {
const module = await import(`./${file}`);
if (module.createRouter) {
const router = module.createRouter(dependencies);
app.use(`/${prefix}`, router);
}
})
);
};Route Factory Pattern
All routes export a createRouter() function:
// packages/api/src/routes/agents.ts
export const createRouter = (dependencies: AppDependencies): Router => {
const { agentRegistry } = dependencies;
const router = Router();
// Apply middleware at router level
router.use(sessionMiddleware());
router.use(requireAdminMiddleware);
// Define routes
router.get("/", async (req: Request, res: Response) => {
const agents = await agentRegistry.getAllAgents();
res.json(agents);
});
router.get("/:id", async (req: Request, res: Response) => {
const { id } = req.params;
const agent = await agentRegistry.getAgent(id);
res.json(agent.getAgentPressConfig());
});
return router;
};Request Validation
Use Zod schemas with the validate() middleware:
import { validate } from "../middleware/validation";
const CreateAgentSchema = z.object({
name: z.string().min(1),
description: z.string().optional(),
});
router.post(
"/",
validate({ body: CreateAgentSchema }),
async (req: Request, res: Response) => {
// req.body is validated and typed
}
);API Endpoints Reference
Authentication (/auth)
GET /auth/github- GitHub OAuth initiationGET /auth/github/callback- GitHub OAuth callbackGET /auth/google- Google OAuth initiationGET /auth/google/callback- Google OAuth callbackPOST /auth/login/credentials- Email/password loginPOST /auth/logout- User logout
Agents (/agents)
GET /agents- List all agentsGET /agents/:id- Get agent detailsPOST /agents- Create agentPUT /agents/:id- Update agentDELETE /agents/:id- Delete agent
Chat (/chat)
POST /chat- Send message and get streaming responseGET /chat/tools- Get available tools
Threads (/threads)
GET /threads- Get threads with paginationPOST /threads- Create new threadDELETE /threads/:threadId- Delete thread
Middleware
Authentication Middleware
// Require session with minimum role
router.use(sessionMiddleware(EUserRole.GUEST));
// Require admin role
router.use(requireAdminMiddleware);
// Optional - extract if present
router.use(optionalSessionMiddleware);Rate Limiting
// Global rate limit (15 min window, 100 requests)
app.use("/api/", rateLimit({
windowMs: 900000,
max: 100,
}));Best Practices
- Route Factory Pattern: Always export
createRouter()functions - Middleware Composition: Apply middleware at router level, not individual handlers
- Zod Validation: Validate all request data with schemas
- Dependency Injection: Use container pattern for registries
- Before-Load Hooks: Use for authentication guards in frontend routes