Hone logo
Hone
Problems

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, where paramName is the name of the parameter to be extracted. These segments match a single path component.

Key Requirements:

  1. createRouteMatcher(routes) function:

    • Takes an array of routes. Each route is an object with a path property (string) and a handler property (any value, e.g., a function).
    • Parses the path definitions to create an internal structure optimized for matching.
    • Returns a matcher function.
  2. matcher(url) function:

    • Takes a url string as input.
    • Iterates through the defined routes.
    • If a route matches the url:
      • It should return an object: { route: matchedRouteObject, params: extractedParametersObject }.
      • matchedRouteObject is the original route object from the input array.
      • extractedParametersObject is an object where keys are the dynamic segment names and values are the corresponding parts of the url.
      • The first matching route should be returned.
    • If no route matches, it should return null.

Edge Cases to Consider:

  • Root Path: Handling / correctly.
  • Trailing Slashes: Decide how to handle trailing slashes (e.g., /users vs. /users/). For this challenge, assume exact matches are required, so /users does 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 url is 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/reviews matches the more specific route /products/:productId/reviews first.
  • /products/xyz/details does not match the first route. It matches the second route /products/*. Since there are no named dynamic parameters in the wildcard route, the params object 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/:userId should 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, the params object should be empty. The wildcard itself doesn't create a named parameter unless explicitly defined.
Loading editor...
javascript