Implement a JavaScript Route Matcher
This challenge asks you to build a robust route matching function in JavaScript. In web development, you often need to match incoming request URLs to specific handler functions. A flexible route matcher is crucial for routing logic in frameworks like Express.js or for building your own client-side or server-side routing systems.
Problem Description
You need to implement a JavaScript function, let's call it createRouteMatcher, that takes an array of route definitions and returns a matcher function. This matcher function will accept a URL string and return an object containing the matched route definition and any extracted parameters, or null if no route matches.
The route definitions will be strings representing URL patterns. These patterns can include:
- Literal segments: e.g.,
/users,/products - Wildcard segments: Represented by
*, matching any sequence of characters (but not across/boundaries). A wildcard segment should capture everything after it until the next literal segment or the end of the path. - Dynamic segments: Represented by
:paramName, whereparamNameis the name of the parameter to be extracted. These segments match a single path component.
Key Requirements:
-
createRouteMatcher(routes)function:- Takes an array of
routes. Eachrouteis an object with apathproperty (string) and ahandlerproperty (any value, e.g., a function). - Parses the
pathdefinitions to create an internal structure optimized for matching. - Returns a
matcherfunction.
- Takes an array of
-
matcher(url)function:- Takes a
urlstring as input. - Iterates through the defined routes.
- If a route matches the
url:- It should return an object:
{ route: matchedRouteObject, params: extractedParametersObject }. matchedRouteObjectis the original route object from the input array.extractedParametersObjectis an object where keys are the dynamic segment names and values are the corresponding parts of theurl.- The first matching route should be returned.
- It should return an object:
- If no route matches, it should return
null.
- Takes a
Edge Cases to Consider:
- Root Path: Handling
/correctly. - Trailing Slashes: Decide how to handle trailing slashes (e.g.,
/usersvs./users/). For this challenge, assume exact matches are required, so/usersdoes not match/users/. - Multiple Wildcards: A path can contain multiple wildcards.
- Wildcard at the end: A wildcard can be the last segment.
- Dynamic segments and wildcards together: e.g.,
/users/:userId/* - Empty URL: What happens if the input
urlis an empty string?
Examples
Example 1:
const routes = [
{ path: '/', handler: 'homeHandler' },
{ path: '/users', handler: 'usersHandler' },
{ path: '/users/:userId', handler: 'userDetailHandler' }
];
const matcher = createRouteMatcher(routes);
console.log(matcher('/users/123'));
Output:
{
route: { path: '/users/:userId', handler: 'userDetailHandler' },
params: { userId: '123' }
}
Explanation: The URL /users/123 matches the route /users/:userId. The dynamic segment :userId captures 123.
Example 2:
const routes = [
{ path: '/products/:productId/reviews', handler: 'productReviewsHandler' },
{ path: '/products/*', handler: 'productWildcardHandler' }
];
const matcher = createRouteMatcher(routes);
console.log(matcher('/products/abc/reviews'));
console.log(matcher('/products/xyz/details'));
Output:
{
route: { path: '/products/:productId/reviews', handler: 'productReviewsHandler' },
params: { productId: 'abc' }
}
{
route: { path: '/products/*', handler: 'productWildcardHandler' },
params: {} // No dynamic params, but wildcard is implicitly captured if needed (though not explicitly named here)
}
Explanation:
/products/abc/reviewsmatches the more specific route/products/:productId/reviewsfirst./products/xyz/detailsdoes not match the first route. It matches the second route/products/*. Since there are no named dynamic parameters in the wildcard route, theparamsobject is empty.
Example 3: (Wildcard at the end)
const routes = [
{ path: '/files/*', handler: 'fileHandler' },
{ path: '/static/:asset', handler: 'staticAssetHandler' }
];
const matcher = createRouteMatcher(routes);
console.log(matcher('/files/images/logo.png'));
console.log(matcher('/static/style.css'));
console.log(matcher('/files/')); // Edge case: wildcard but nothing after
Output:
{
route: { path: '/files/*', handler: 'fileHandler' },
params: {} // Wildcard captures 'images/logo.png', but no named params
}
{
route: { path: '/static/:asset', handler: 'staticAssetHandler' },
params: { asset: 'style.css' }
}
{
route: { path: '/files/*', handler: 'fileHandler' },
params: {} // Wildcard captures empty string
}
Explanation:
- The first match captures everything after
/files/into the wildcard. - The second match works as expected with a dynamic parameter.
- The third example shows the wildcard capturing an empty string when the URL ends at the wildcard segment.
Constraints
- The number of routes will be between 1 and 100.
- URL strings will be valid paths, starting with
/. They can contain alphanumeric characters, hyphens, underscores, and forward slashes. - Dynamic segment names (
:paramName) will consist of alphanumeric characters. - Performance is important. The matching process should be reasonably efficient, especially for a moderate number of routes. Avoid brute-force string comparisons for every segment if possible.
Notes
- Consider how you will parse the route paths. Regular expressions can be very powerful here, but be mindful of their complexity and performance implications.
- Think about the order of matching. More specific routes should generally be evaluated before less specific ones (e.g.,
/users/:userIdshould match before/users/*). - The wildcard
*should capture everything from its position until the end of the path, or until the next literal segment if the wildcard is not at the end. However, for simplicity in this challenge, a wildcard at the end will capture the rest of the string. - When a wildcard segment is used (e.g.,
/files/*), if there are no named dynamic parameters in the rest of the route, theparamsobject should be empty. The wildcard itself doesn't create a named parameter unless explicitly defined.