Open
Description
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:
- Compiles route patterns from user-friendly syntax (
:param
) to executable regex patterns - Matches incoming requests against compiled route patterns efficiently
- Extracts path parameters with type safety for dynamic routes
- Prioritizes route matching (static routes before dynamic routes for performance)
- 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:
- Feature request: create
BaseRouter
with base methods #3971 - BaseRouter with HTTP method decorators - Feature request: Implement Route Management System for Event Handler #4139 - Route Management System for accessing stored routes
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
- This feature request meets Powertools for AWS Lambda (TypeScript) Tenets
- Should this be considered in other Powertools for AWS Lambda languages? i.e. Python, Java, and .NET
Future readers
Please react with 👍 and your use case to help us understand customer demand.
Metadata
Metadata
Assignees
Labels
Type
Projects
Status
On hold