| # Route Organization Pattern |
|
|
| This document describes the pattern used for organizing Express routes into modular, maintainable file structures. This pattern is exemplified by the `app-spec` route module and should be applied to other route modules for consistency and maintainability. |
|
|
| --- |
|
|
| ## Table of Contents |
|
|
| 1. [Overview](#overview) |
| 2. [Directory Structure](#directory-structure) |
| 3. [File Organization Principles](#file-organization-principles) |
| 4. [File Types and Their Roles](#file-types-and-their-roles) |
| 5. [Implementation Guidelines](#implementation-guidelines) |
| 6. [Example: app-spec Module](#example-app-spec-module) |
| 7. [Migration Guide](#migration-guide) |
|
|
| --- |
|
|
| ## Overview |
|
|
| The route organization pattern separates concerns into: |
|
|
| - **Route handlers** - Thin HTTP request/response handlers in `routes/` subdirectory |
| - **Business logic** - Extracted into standalone function files |
| - **Shared utilities** - Common functions and state in `common.ts` |
| - **Route registration** - Centralized in `index.ts` |
|
|
| This pattern improves: |
|
|
| - **Maintainability** - Clear separation of concerns |
| - **Testability** - Functions can be tested independently |
| - **Reusability** - Business logic can be reused across routes |
| - **Readability** - Smaller, focused files are easier to understand |
|
|
| --- |
|
|
| ## Directory Structure |
|
|
| ``` |
| routes/ |
| └── {module-name}/ |
| ├── index.ts # Route registration & export |
| ├── common.ts # Shared utilities & state |
| ├── {business-function}.ts # Extracted business logic functions |
| └── routes/ |
| ├── {endpoint-name}.ts # Individual route handlers |
| └── ... |
| ``` |
|
|
| ### Example Structure |
|
|
| ``` |
| routes/ |
| └── app-spec/ |
| ├── index.ts # createSpecRegenerationRoutes() |
| ├── common.ts # Shared state, logging utilities |
| ├── generate-spec.ts # generateSpec() function |
| ├── generate-features-from-spec.ts # generateFeaturesFromSpec() function |
| ├── parse-and-create-features.ts # parseAndCreateFeatures() function |
| └── routes/ |
| ├── create.ts # POST /create handler |
| ├── generate.ts # POST /generate handler |
| ├── generate-features.ts # POST /generate-features handler |
| ├── status.ts # GET /status handler |
| └── stop.ts # POST /stop handler |
| ``` |
|
|
| --- |
|
|
| ## File Organization Principles |
|
|
| ### 1. **Single Responsibility** |
|
|
| Each file should have one clear purpose: |
|
|
| - Route handlers handle HTTP concerns (request/response, validation) |
| - Business logic files contain domain-specific operations |
| - Common files contain shared utilities and state |
|
|
| ### 2. **Separation of Concerns** |
|
|
| - **HTTP Layer** (`routes/*.ts`) - Request parsing, response formatting, status codes |
| - **Business Logic** (`*.ts` in root) - Core functionality, domain operations |
| - **Shared State** (`common.ts`) - Module-level state, cross-cutting utilities |
|
|
| ### 3. **File Size Management** |
|
|
| - Extract functions when files exceed ~150-200 lines |
| - Extract when a function is reusable across multiple routes |
| - Extract when a function has complex logic that deserves its own file |
|
|
| ### 4. **Naming Conventions** |
|
|
| - Route handlers: `{verb}-{resource}.ts` or `{action}.ts` (e.g., `create.ts`, `status.ts`) |
| - Business logic: `{action}-{noun}.ts` or `{verb}-{noun}.ts` (e.g., `generate-spec.ts`) |
| - Common utilities: Always `common.ts` |
|
|
| --- |
|
|
| ## File Types and Their Roles |
|
|
| ### `index.ts` - Route Registration |
|
|
| **Purpose**: Central export point that creates and configures the Express router. |
|
|
| **Responsibilities**: |
|
|
| - Import route handler factories |
| - Create Express Router instance |
| - Register all routes |
| - Export router creation function |
|
|
| **Pattern**: |
|
|
| ```typescript |
| import { Router } from "express"; |
| import type { EventEmitter } from "../../lib/events.js"; |
| import { createCreateHandler } from "./routes/create.js"; |
| import { createGenerateHandler } from "./routes/generate.js"; |
| |
| export function create{Module}Routes(events: EventEmitter): Router { |
| const router = Router(); |
| |
| router.post("/create", createCreateHandler(events)); |
| router.get("/status", createStatusHandler()); |
| |
| return router; |
| } |
| ``` |
|
|
| **Key Points**: |
|
|
| - Function name: `create{Module}Routes` |
| - Accepts dependencies (e.g., `EventEmitter`) as parameters |
| - Returns configured Router instance |
| - Route handlers are factory functions that accept dependencies |
|
|
| --- |
|
|
| ### `common.ts` - Shared Utilities & State |
|
|
| **Purpose**: Central location for shared state, utilities, and helper functions used across multiple route handlers and business logic files. |
|
|
| **Common Contents**: |
|
|
| - Module-level state (e.g., `isRunning`, `currentAbortController`) |
| - State management functions (e.g., `setRunningState()`) |
| - Logging utilities (e.g., `logAuthStatus()`, `logError()`) |
| - Error handling utilities (e.g., `getErrorMessage()`) |
| - Shared constants |
| - Shared types/interfaces |
|
|
| **Pattern**: |
|
|
| ```typescript |
| import { createLogger } from '../../lib/logger.js'; |
| |
| const logger = createLogger('{ModuleName}'); |
| |
| // Shared state |
| export let isRunning = false; |
| export let currentAbortController: AbortController | null = null; |
| |
| // State management |
| export function setRunningState(running: boolean, controller: AbortController | null = null): void { |
| isRunning = running; |
| currentAbortController = controller; |
| } |
| |
| // Utility functions |
| export function logError(error: unknown, context: string): void { |
| logger.error(`❌ ${context}:`, error); |
| } |
| |
| export function getErrorMessage(error: unknown): string { |
| return error instanceof Error ? error.message : 'Unknown error'; |
| } |
| ``` |
|
|
| **Key Points**: |
|
|
| - Export shared state as `let` variables (mutable state) |
| - Provide setter functions for state management |
| - Keep utilities focused and reusable |
| - Use consistent logging patterns |
|
|
| --- |
|
|
| ### `routes/{endpoint-name}.ts` - Route Handlers |
|
|
| **Purpose**: Thin HTTP request/response handlers that validate input, call business logic, and format responses. |
|
|
| **Responsibilities**: |
|
|
| - Parse and validate request parameters |
| - Check preconditions (e.g., `isRunning` state) |
| - Call business logic functions |
| - Handle errors and format responses |
| - Manage background tasks (if applicable) |
|
|
| **Pattern**: |
|
|
| ```typescript |
| import type { Request, Response } from "express"; |
| import type { EventEmitter } from "../../../lib/events.js"; |
| import { createLogger } from "../../../lib/logger.js"; |
| import { |
| isRunning, |
| setRunningState, |
| logError, |
| getErrorMessage, |
| } from "../common.js"; |
| import { businessLogicFunction } from "../business-logic.js"; |
| |
| const logger = createLogger("{ModuleName}"); |
| |
| export function create{Action}Handler(events: EventEmitter) { |
| return async (req: Request, res: Response): Promise<void> => { |
| logger.info("========== /{endpoint} endpoint called =========="); |
| |
| try { |
| // 1. Parse and validate input |
| const { param1, param2 } = req.body as { param1: string; param2?: number }; |
| |
| if (!param1) { |
| res.status(400).json({ success: false, error: "param1 required" }); |
| return; |
| } |
| |
| // 2. Check preconditions |
| if (isRunning) { |
| res.json({ success: false, error: "Operation already running" }); |
| return; |
| } |
| |
| // 3. Set up state |
| const abortController = new AbortController(); |
| setRunningState(true, abortController); |
| |
| // 4. Call business logic (background if async) |
| businessLogicFunction(param1, param2, events, abortController) |
| .catch((error) => { |
| logError(error, "Operation failed"); |
| events.emit("module:event", { type: "error", error: getErrorMessage(error) }); |
| }) |
| .finally(() => { |
| setRunningState(false, null); |
| }); |
| |
| // 5. Return immediate response |
| res.json({ success: true }); |
| } catch (error) { |
| logger.error("❌ Route handler exception:", error); |
| res.status(500).json({ success: false, error: getErrorMessage(error) }); |
| } |
| }; |
| } |
| ``` |
|
|
| **Key Points**: |
|
|
| - Factory function pattern: `create{Action}Handler(dependencies)` |
| - Returns async Express handler function |
| - Validate input early |
| - Use shared utilities from `common.ts` |
| - Handle errors consistently |
| - For background tasks, return success immediately and handle completion asynchronously |
|
|
| --- |
|
|
| ### `{business-function}.ts` - Business Logic Files |
|
|
| **Purpose**: Standalone files containing complex business logic functions that can be reused across routes or extracted to reduce file size. |
|
|
| **When to Extract**: |
|
|
| - Function exceeds ~100-150 lines |
| - Function is called from multiple route handlers |
| - Function has complex logic that deserves its own file |
| - Function can be tested independently |
|
|
| **Pattern**: |
|
|
| ```typescript |
| /** |
| * {Brief description of what this function does} |
| */ |
| |
| import { query, type Options } from '@anthropic-ai/claude-agent-sdk'; |
| import type { EventEmitter } from '../../lib/events.js'; |
| import { createLogger } from '../../lib/logger.js'; |
| import { logAuthStatus } from './common.js'; |
| import { anotherBusinessFunction } from './another-business-function.js'; |
| |
| const logger = createLogger('{ModuleName}'); |
| |
| export async function businessLogicFunction( |
| param1: string, |
| param2: number, |
| events: EventEmitter, |
| abortController: AbortController |
| ): Promise<void> { |
| logger.debug('========== businessLogicFunction() started =========='); |
| |
| try { |
| // Business logic here |
| // ... |
| |
| // Can call other business logic functions |
| await anotherBusinessFunction(param1, events, abortController); |
| |
| logger.debug('========== businessLogicFunction() completed =========='); |
| } catch (error) { |
| logger.error('❌ businessLogicFunction() failed:', error); |
| throw error; |
| } |
| } |
| ``` |
|
|
| **Key Points**: |
|
|
| - Export named functions (not default exports) |
| - Include JSDoc comment at top |
| - Import shared utilities from `common.ts` |
| - Use consistent logging patterns |
| - Can import and call other business logic functions |
| - Handle errors and re-throw or emit events as appropriate |
|
|
| --- |
|
|
| ## Implementation Guidelines |
|
|
| ### Step 1: Create Directory Structure |
|
|
| ```bash |
| mkdir -p routes/{module-name}/routes |
| ``` |
|
|
| ### Step 2: Create `common.ts` |
|
|
| Start with shared state and utilities: |
|
|
| - Module-level state variables |
| - State management functions |
| - Logging utilities |
| - Error handling utilities |
|
|
| ### Step 3: Extract Business Logic |
|
|
| Identify large functions or reusable logic: |
|
|
| - Functions > 150 lines → extract to separate file |
| - Functions used by multiple routes → extract to separate file |
| - Complex operations → extract to separate file |
|
|
| ### Step 4: Create Route Handlers |
|
|
| For each endpoint: |
|
|
| - Create `routes/{endpoint-name}.ts` |
| - Implement factory function pattern |
| - Keep handlers thin (validation + call business logic) |
| - Use utilities from `common.ts` |
|
|
| ### Step 5: Create `index.ts` |
|
|
| - Import all route handler factories |
| - Create router and register routes |
| - Export router creation function |
|
|
| ### Step 6: Register Module |
|
|
| In main routes file: |
|
|
| ```typescript |
| import { create{Module}Routes } from "./{module-name}/index.js"; |
| |
| app.use("/api/{module-name}", create{Module}Routes(events)); |
| ``` |
|
|
| --- |
|
|
| ## Example: app-spec Module |
|
|
| The `app-spec` module demonstrates this pattern: |
|
|
| ### File Breakdown |
|
|
| **`index.ts`** (24 lines) |
|
|
| - Creates router |
| - Registers 5 endpoints |
| - Exports `createSpecRegenerationRoutes()` |
|
|
| **`common.ts`** (74 lines) |
|
|
| - Shared state: `isRunning`, `currentAbortController` |
| - State management: `setRunningState()` |
| - Utilities: `logAuthStatus()`, `logError()`, `getErrorMessage()` |
|
|
| **`generate-spec.ts`** (204 lines) |
|
|
| - Extracted business logic for spec generation |
| - Handles SDK calls, streaming, file I/O |
| - Called by both `create.ts` and `generate.ts` routes |
|
|
| **`generate-features-from-spec.ts`** (155 lines) |
|
|
| - Extracted business logic for feature generation |
| - Handles SDK calls and streaming |
| - Calls `parseAndCreateFeatures()` for final step |
|
|
| **`parse-and-create-features.ts`** (84 lines) |
|
|
| - Extracted parsing and file creation logic |
| - Called by `generate-features-from-spec.ts` |
|
|
| **`routes/create.ts`** (96 lines) |
|
|
| - Thin handler for POST /create |
| - Validates input, checks state, calls `generateSpec()` |
|
|
| **`routes/generate.ts`** (99 lines) |
|
|
| - Thin handler for POST /generate |
| - Similar to `create.ts` but different input parameter |
|
|
| **`routes/generate-features.ts`** (71 lines) |
|
|
| - Thin handler for POST /generate-features |
| - Calls `generateFeaturesFromSpec()` |
|
|
| **`routes/status.ts`** (17 lines) |
|
|
| - Simple handler for GET /status |
| - Returns current state |
|
|
| **`routes/stop.ts`** (25 lines) |
|
|
| - Simple handler for POST /stop |
| - Aborts current operation |
|
|
| ### Key Observations |
|
|
| 1. **Route handlers are thin** - Most are 70-100 lines, focused on HTTP concerns |
| 2. **Business logic is extracted** - Complex operations in separate files |
| 3. **Shared utilities centralized** - Common functions in `common.ts` |
| 4. **Reusability** - `generateSpec()` used by both `create.ts` and `generate.ts` |
| 5. **Clear separation** - HTTP layer vs business logic vs shared utilities |
|
|
| --- |
|
|
| ## Migration Guide |
|
|
| ### Migrating an Existing Route Module |
|
|
| 1. **Analyze current structure** |
| - Identify all endpoints |
| - Identify shared state/utilities |
| - Identify large functions (>150 lines) |
|
|
| 2. **Create directory structure** |
|
|
| ```bash |
| mkdir -p routes/{module-name}/routes |
| ``` |
|
|
| 3. **Extract common utilities** |
| - Move shared state to `common.ts` |
| - Move utility functions to `common.ts` |
| - Update imports in existing files |
|
|
| 4. **Extract business logic** |
| - Identify functions to extract |
| - Create `{function-name}.ts` files |
| - Move logic, update imports |
|
|
| 5. **Create route handlers** |
| - Create `routes/{endpoint-name}.ts` for each endpoint |
| - Move HTTP handling logic |
| - Keep handlers thin |
|
|
| 6. **Create index.ts** |
| - Import route handlers |
| - Register routes |
| - Export router creation function |
|
|
| 7. **Update main routes file** |
| - Import from new `index.ts` |
| - Update route registration |
|
|
| 8. **Test** |
| - Verify all endpoints work |
| - Check error handling |
| - Verify shared state management |
|
|
| ### Example Migration |
|
|
| **Before** (monolithic `routes.ts`): |
|
|
| ```typescript |
| // routes.ts - 500+ lines |
| router.post('/create', async (req, res) => { |
| // 200 lines of logic |
| }); |
| |
| router.post('/generate', async (req, res) => { |
| // 200 lines of similar logic |
| }); |
| ``` |
|
|
| **After** (organized structure): |
|
|
| ```typescript |
| // routes/app-spec/index.ts |
| export function createSpecRegenerationRoutes(events) { |
| const router = Router(); |
| router.post("/create", createCreateHandler(events)); |
| router.post("/generate", createGenerateHandler(events)); |
| return router; |
| } |
| |
| // routes/app-spec/routes/create.ts - 96 lines |
| export function createCreateHandler(events) { |
| return async (req, res) => { |
| // Thin handler, calls generateSpec() |
| }; |
| } |
| |
| // routes/app-spec/generate-spec.ts - 204 lines |
| export async function generateSpec(...) { |
| // Business logic extracted here |
| } |
| ``` |
|
|
| --- |
|
|
| ## Best Practices |
|
|
| ### ✅ Do |
|
|
| - Keep route handlers thin (< 150 lines) |
| - Extract complex business logic to separate files |
| - Centralize shared utilities in `common.ts` |
| - Use factory function pattern for route handlers |
| - Export named functions (not default exports) |
| - Use consistent logging patterns |
| - Handle errors consistently |
| - Document complex functions with JSDoc |
|
|
| ### ❌ Don't |
|
|
| - Put business logic directly in route handlers |
| - Duplicate utility functions across files |
| - Create files with only one small function (< 20 lines) |
| - Mix HTTP concerns with business logic |
| - Use default exports for route handlers |
| - Create deeply nested directory structures |
| - Put route handlers in root of module directory |
|
|
| --- |
|
|
| ## Summary |
|
|
| The route organization pattern provides: |
|
|
| 1. **Clear structure** - Easy to find and understand code |
| 2. **Separation of concerns** - HTTP, business logic, and utilities separated |
| 3. **Reusability** - Business logic can be shared across routes |
| 4. **Maintainability** - Smaller, focused files are easier to maintain |
| 5. **Testability** - Functions can be tested independently |
|
|
| Apply this pattern to all route modules for consistency and improved code quality. |
|
|