🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
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.