- Removed the old spec regeneration routes and replaced them with a new structure under the app-spec directory for better modularity. - Introduced unit tests for common functionalities in app-spec, covering state management and error handling. - Added documentation on route organization patterns to improve maintainability and clarity for future development.
16 KiB
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
- Overview
- Directory Structure
- File Organization Principles
- File Types and Their Roles
- Implementation Guidelines
- Example: app-spec Module
- 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 (
*.tsin 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}.tsor{action}.ts(e.g.,create.ts,status.ts) - Business logic:
{action}-{noun}.tsor{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:
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:
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
letvariables (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.,
isRunningstate) - Call business logic functions
- Handle errors and format responses
- Manage background tasks (if applicable)
Pattern:
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:
/**
* {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
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:
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.tsandgenerate.tsroutes
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.tsbut 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
- Route handlers are thin - Most are 70-100 lines, focused on HTTP concerns
- Business logic is extracted - Complex operations in separate files
- Shared utilities centralized - Common functions in
common.ts - Reusability -
generateSpec()used by bothcreate.tsandgenerate.ts - Clear separation - HTTP layer vs business logic vs shared utilities
Migration Guide
Migrating an Existing Route Module
-
Analyze current structure
- Identify all endpoints
- Identify shared state/utilities
- Identify large functions (>150 lines)
-
Create directory structure
mkdir -p routes/{module-name}/routes -
Extract common utilities
- Move shared state to
common.ts - Move utility functions to
common.ts - Update imports in existing files
- Move shared state to
-
Extract business logic
- Identify functions to extract
- Create
{function-name}.tsfiles - Move logic, update imports
-
Create route handlers
- Create
routes/{endpoint-name}.tsfor each endpoint - Move HTTP handling logic
- Keep handlers thin
- Create
-
Create index.ts
- Import route handlers
- Register routes
- Export router creation function
-
Update main routes file
- Import from new
index.ts - Update route registration
- Import from new
-
Test
- Verify all endpoints work
- Check error handling
- Verify shared state management
Example Migration
Before (monolithic routes.ts):
// 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):
// 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:
- Clear structure - Easy to find and understand code
- Separation of concerns - HTTP, business logic, and utilities separated
- Reusability - Business logic can be shared across routes
- Maintainability - Smaller, focused files are easier to maintain
- Testability - Functions can be tested independently
Apply this pattern to all route modules for consistency and improved code quality.