Skip to content

Feature request: Implement Route Matching & Resolution System for Event Handler #4140

Open
@dreamorosi

Description

@dreamorosi

Important

This issue is dependent on #3971 & #4139 and is on hold until they're merged.

Use case

As part of the Event Handler implementation (#3251), we need a system to match incoming HTTP requests against registered routes and extract path parameters.

The Route Matching System provides the pattern matching engine that:

  1. Compiles route patterns from user-friendly syntax (:param) to executable regex patterns
  2. Matches incoming requests against compiled route patterns efficiently
  3. Extracts path parameters with type safety for dynamic routes
  4. Prioritizes route matching (static routes before dynamic routes for performance)
  5. Handles edge cases like trailing slashes, special characters, and conflicting patterns

This system acts as the matching layer between route storage (Route Management) and request resolution, ensuring incoming requests are correctly matched to their handlers.

Solution/User Experience

Note

The code snippets below are provided as reference only - they are not exhaustive and final implementation might vary.

Route Matching Engine

// Efficient route matching with prioritization
class RouteMatcher {
  private staticRoutes: Map<string, Route[]> = new Map();
  private dynamicRoutes: Route[] = [];
  
  constructor(routes: Route[]) {
    this.organizeRoutes(routes);
  }
  
  // Organize routes for efficient matching
  private organizeRoutes(routes: Route[]): void {
    for (const route of routes) {
      const compiled = RouteCompiler.compile(route.path);
      
      if (compiled.isDynamic) {
        // Dynamic routes need regex matching
        this.dynamicRoutes.push({
          ...route,
          compiledRoute: compiled
        });
      } else {
        // Static routes get O(1) lookup
        const key = `${route.method}:${route.path}`;
        if (!this.staticRoutes.has(key)) {
          this.staticRoutes.set(key, []);
        }
        this.staticRoutes.get(key)!.push(route);
      }
    }
    
    // Sort dynamic routes by specificity (more specific patterns first)
    this.dynamicRoutes.sort(this.compareRouteSpecificity);
  }
  
  // Find matching route for incoming request
  findMatch(method: string, path: string): RouteMatch | null {
    // 1. Try static routes first (fastest)
    const staticKey = `${method.toUpperCase()}:${path}`;
    const staticMatches = this.staticRoutes.get(staticKey);
    
    if (staticMatches && staticMatches.length > 0) {
      return {
        route: staticMatches[0],
        params: {},
        matchType: 'static'
      };
    }
    
    // 2. Try dynamic routes
    for (const route of this.dynamicRoutes) {
      if (route.method !== method.toUpperCase()) continue;
      
      const match = route.compiledRoute.regex.exec(path);
      if (match) {
        return {
          route,
          params: match.groups || {},
          matchType: 'dynamic'
        };
      }
    }
    
    return null;
  }
  
  // Sort routes by specificity (more specific = higher priority)
  private compareRouteSpecificity(a: Route, b: Route): number {
    // Routes with fewer parameters are more specific
    const aParams = a.compiledRoute.paramNames.length;
    const bParams = b.compiledRoute.paramNames.length;
    
    if (aParams !== bParams) {
      return aParams - bParams;
    }
    
    // Routes with more path segments are more specific
    const aSegments = a.path.split('/').length;
    const bSegments = b.path.split('/').length;
    
    return bSegments - aSegments;
  }
}

Path Parameter Extraction with Type Safety

// Type-safe parameter extraction
type ExtractParams<T extends string> = 
  T extends `${infer Start}:${infer Param}/${infer Rest}`
    ? { [K in Param]: string } & ExtractParams<`${Start}${Rest}`>
    : T extends `${infer Start}:${infer Param}`
    ? { [K in Param]: string }
    : {};

interface RouteMatch {
  route: Route;
  params: Record<string, string>;
  matchType: 'static' | 'dynamic';
}

// Parameter validation and conversion
class ParameterProcessor {
  static processParams(
    params: Record<string, string>, 
    route: Route
  ): ProcessedParams {
    const processed: Record<string, any> = {};
    
    for (const [key, value] of Object.entries(params)) {
      // Basic URL decoding
      processed[key] = decodeURIComponent(value);
    }
    
    return {
      raw: params,
      processed,
      route
    };
  }
  
  // Validate parameter values
  static validateParams(params: Record<string, string>): ValidationResult {
    const issues: string[] = [];
    
    for (const [key, value] of Object.entries(params)) {
      if (!value || value.trim() === '') {
        issues.push(`Parameter '${key}' cannot be empty`);
      }
    }
    
    return {
      isValid: issues.length === 0,
      issues
    };
  }
}

Integration with Route Management

// Integration point with Route Management System
class RouteResolver {
  private routeMatcher: RouteMatcher;
  
  constructor(routeRegistry: RouteRegistry) {
    // Get all routes from management system
    const allRoutes = routeRegistry.getAllRoutes();
    this.routeMatcher = new RouteMatcher(allRoutes);
  }
  
  // Main resolution method
  resolve(method: string, path: string): ResolvedRoute | null {
    const match = this.routeMatcher.findMatch(method, path);
    
    if (!match) {
      return null;
    }
    
    // Process and validate parameters
    const processedParams = ParameterProcessor.processParams(match.params, match.route);
    const validation = ParameterProcessor.validateParams(match.params);
    
    if (!validation.isValid) {
      throw new ParameterValidationError(validation.issues);
    }
    
    return {
      route: match.route,
      params: processedParams.processed,
      rawParams: processedParams.raw,
      matchType: match.matchType
    };
  }
  
  // Refresh matcher when routes change
  refresh(routeRegistry: RouteRegistry): void {
    const allRoutes = routeRegistry.getAllRoutes();
    this.routeMatcher = new RouteMatcher(allRoutes);
  }
}

Error Handling

// Route matching specific errors
class RouteMatchingError extends Error {
  constructor(message: string, public readonly path: string, public readonly method: string) {
    super(message);
    this.name = 'RouteMatchingError';
  }
}

class ParameterValidationError extends RouteMatchingError {
  constructor(public readonly issues: string[]) {
    super(`Parameter validation failed: ${issues.join(', ')}`, '', '');
    this.name = 'ParameterValidationError';
  }
}

class RouteCompilationError extends Error {
  constructor(message: string, public readonly path: string) {
    super(message);
    this.name = 'RouteCompilationError';
  }
}

Implementation Details

Scope - In Scope:

  • Route compilation: Convert :param syntax to regex patterns
  • Pattern matching: Efficient matching algorithm with static/dynamic prioritization
  • Parameter extraction: Type-safe extraction of path parameters
  • Route prioritization: Static routes before dynamic, specificity-based ordering
  • Path validation: Basic validation of route patterns and parameters
  • Error handling: Specific errors for matching
  • Integration: Clean interface with Route Management System

Scope - Out of Scope (Future Issues):

  • Route caching and performance optimizations - future enhancement
  • Wildcard routes and catch-all patterns - future feature
  • Route middleware execution - separate middleware issue

Key Design Principles:

  • Performance First: Static routes get O(1) lookup, dynamic routes are sorted by specificity
  • Type Safety: Full TypeScript support with parameter type extraction
  • Developer Friendly: Clear error messages and validation
  • Extensible: Clean interfaces for future enhancements
  • Standards Compliant: Follows web routing conventions

Dependencies and Integration

Prerequisites:

Integration Points:

  • Route Management: Uses RouteRegistry to get routes for compilation
  • BaseRouter: Provides the RouteResolver for request resolution
  • Response Handling (future): Will use resolved routes for response generation

Data Flow:

HTTP Request (method, path)
    ↓
RouteResolver.resolve()
    ↓
RouteMatcher.findMatch()
    ↓
Route pattern matching
    ↓
Parameter extraction & validation
    ↓
ResolvedRoute with handler & params

Alternative solutions

N/A

Acknowledgment

Future readers

Please react with 👍 and your use case to help us understand customer demand.

Metadata

Metadata

Assignees

No one assigned

    Labels

    event-handlerThis item relates to the Event Handler Utilityfeature-requestThis item refers to a feature request for an existing or new utilityon-holdThis item is on-hold and will be revisited in the future

    Type

    No type

    Projects

    Status

    On hold

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions