Files
automaker/docs/server/route-organization.md
SuperComboGamer 8d578558ff style: fix formatting with Prettier
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-21 20:31:57 -05:00

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

  1. Overview
  2. Directory Structure
  3. File Organization Principles
  4. File Types and Their Roles
  5. Implementation Guidelines
  6. Example: app-spec Module
  7. 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:

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 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:

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.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

    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):

// 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:

  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.