Skip to Content
AgentPress is finally here! šŸŽ‰

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

AppPortPurposeRoute File
Console3000Admin dashboardapps/console/src/routes.tsx
Portal3002User-facing chat interfaceapps/portal/src/routes.tsx
Drop-in Chat3003Embeddable iframe widgetapps/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 routes

Console 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 /tasks

Portal 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 initiation
  • GET /auth/github/callback - GitHub OAuth callback
  • GET /auth/google - Google OAuth initiation
  • GET /auth/google/callback - Google OAuth callback
  • POST /auth/login/credentials - Email/password login
  • POST /auth/logout - User logout

Agents (/agents)

  • GET /agents - List all agents
  • GET /agents/:id - Get agent details
  • POST /agents - Create agent
  • PUT /agents/:id - Update agent
  • DELETE /agents/:id - Delete agent

Chat (/chat)

  • POST /chat - Send message and get streaming response
  • GET /chat/tools - Get available tools

Threads (/threads)

  • GET /threads - Get threads with pagination
  • POST /threads - Create new thread
  • DELETE /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

  1. Route Factory Pattern: Always export createRouter() functions
  2. Middleware Composition: Apply middleware at router level, not individual handlers
  3. Zod Validation: Validate all request data with schemas
  4. Dependency Injection: Use container pattern for registries
  5. Before-Load Hooks: Use for authentication guards in frontend routes
Last updated on