skip to content

In Angular's router, when do you need a custom UrlMatcher instead of a path, and what must the matcher function return?

level: middleimportance: nice to knowfreq 26%

answer

  1. patterns path syntax cannot express
  2. segments, group, route
  3. consumed and posParams
  4. null means try the next route
  5. UrlSerializer rewrites every URL instead

basics

~20 s

Use Route.matcher when path syntax cannot express the pattern, such as /@username or a regex rule. The function receives the URL segments and returns null for no match or {consumed, posParams}, whose parameters appear in paramMap like :id values.

solid answer

~40 s

A `path` string only knows static segments, `:param` segments and `**`. When a route must match something like `/@alice` or a segment of a certain format, you set `matcher` instead of `path` (the two cannot be combined). The `UrlMatcher` is called with `(segments, group, route)` and returns `null` when it does not match, which lets the router try the next route, or a `UrlMatchResult`: `consumed`, the leading segments it claims, and optional `posParams`, a map of `UrlSegment` values whose `path` strings appear in `paramMap` and bound inputs. Segments left unconsumed must be matched by `children`. Matchers are synchronous and run during route recognition on every navigation that reaches them, so keep them cheap. If the need is global, such as case-insensitive URLs, a custom `UrlSerializer` that normalises every parsed URL is the alternative.

code

ts · 16 lines
ts
import {Routes, UrlMatchResult, UrlSegment} from '@angular/router';
import {Profile} from './profile';

export function profileMatcher(segments: UrlSegment[]): UrlMatchResult | null {
  if (segments.length === 1 && /^@\w+$/.test(segments[0].path)) {
    return {
      consumed: segments,
      posParams: {username: new UrlSegment(segments[0].path.slice(1), {})},
    };
  }
  return null;
}

export const routes: Routes = [
  {matcher: profileMatcher, component: Profile},
];

go deeper

for a junior

Know that route paths support literals, :params and **, and that anything more needs a custom matcher.

for a middle

Explain the UrlMatcher signature and result: null means no match, consumed claims segments, and posParams becomes route parameters.

for a senior

Choose the right tool: a matcher for one route, a canMatch guard for runtime conditions, a UrlSerializer only for app-wide URL rules.

for a principal

Keep URL rules few and visible; every custom matcher or serializer is routing logic that readers cannot see in the route table.

## What path matching can and cannot express The router's default matcher compares a route's `path` segment by segment: a literal must equal the URL segment **exactly** (matching is case-sensitive), a `:name` part captures any single segment, and `**` captures the rest. It cannot say "a segment that starts with `@`", "a segment that is a four-digit year", or "either `/help` or `/aide`". For those rules, a route supplies a **custom `UrlMatcher`** in its `matcher` property. A route has either `path` (with optional `pathMatch`) or `matcher`, never both. ## The UrlMatcher contract ```ts type UrlMatcher = ( segments: UrlSegment[], group: UrlSegmentGroup, route: Route, ) => UrlMatchResult | null; type UrlMatchResult = { consumed: UrlSegment[]; posParams?: {[name: string]: UrlSegment}; }; ``` - **`segments`** are the URL segments still to be matched at this level of the route tree; each `UrlSegment` has a `path` string and its own matrix `parameters`. - **`group`** is the surrounding `UrlSegmentGroup`, useful to check whether named outlets or children follow. - **`route`** is the route config the matcher belongs to, so one function can serve several routes by reading their `data`. The return value: 1. **`null`** means no match. The router moves on to the next route in the array, exactly as with a failed `path`. 2. A result with **`consumed`** claims the leading segments. Any segments left over must be matched by this route's `children`; otherwise recognition fails for this route and the router keeps looking. 3. **`posParams`** names the captured segments. The router exposes each entry's `path` string as a route parameter, so it shows up in `paramMap`, in the snapshot, and in component inputs bound with `withComponentInputBinding()`. ## A worked example: profile URLs The Angular guide's example matches `/@alice` and exposes `username` as a parameter: - the matcher checks `segments.length === 1` and a regular expression like `/^@[\w]+$/`, - it returns `{consumed: segments, posParams: {username: new UrlSegment(segments[0].path.slice(1), {})}}`, - the route is `{matcher: profileMatcher, component: Profile}`. ## Rules for writing matchers - **Synchronous and cheap.** The return type is not a Promise or Observable; you cannot call an API. A matcher runs for every navigation that reaches its position in the route array, so avoid heavy regular expressions and return `null` early. - **Order still matters.** First match wins, so place a broad matcher after the specific routes it could shadow. - **Use guards for runtime conditions.** If the decision depends on who the user is or on a feature flag, a `canMatch` guard on an ordinary route is clearer than logic inside a matcher. - **Name matchers.** An exported, named function is testable in isolation: feed it `UrlSegment` arrays and assert the result. ## Where matchers show up in real code - **Vanity or handle URLs** such as `/@alice`, where the prefix character carries meaning. - **Format rules**, for example a route that only accepts a numeric id or a date-like segment, so that other routes can claim the same position. - **Legacy URL shapes** kept alive after a redesign, such as `/product-42.html`, matched by suffix and mapped to a parameter. - **Locale prefixes**, where one matcher accepts any supported language code as the first segment and passes it on as a parameter. ## UrlMatcher versus a custom UrlSerializer A matcher customises **one route**. A **`UrlSerializer`** customises how **every** URL string becomes a `UrlTree` and back. The default, `DefaultUrlSerializer`, parses the `(outlet:path)` and `;key=value` syntax; you replace it with `{provide: UrlSerializer, useClass: ...}`. | Need | Better tool | Why | |---|---|---| | `/@username` or a format rule on one route | `UrlMatcher` | local, keeps other routes untouched | | case-insensitive matching everywhere | `UrlSerializer` subclass lowercasing paths in `parse()` | one rule for the whole app | | keep literal parentheses or semicolons in URLs | `UrlSerializer` | the default parser gives them router meaning | | decide by user role or flag | `canMatch` guard | runtime context, may be async | A serializer is powerful but blunt: lowercasing in `parse()` also lowercases parameter values such as `/users/McDonald`, so a production version must lowercase only the static part it cares about.

  • Your matcher returns {consumed: [segments[0]]} for /reports/2026/q3, and the route has no children. What happens?
    The route claims only the first segment, and the remaining `2026/q3` has nothing to match it, so recognition fails for this route and the router continues with the next routes in the array. If nothing else matches, navigation ends in a no-match error or the `**` route. Either consume all segments or add `children` for the rest.
  • Why is lowercasing the whole URL in a custom UrlSerializer's parse() risky?
    It also rewrites parameter and query values, so `/users/McDonald` reaches the component as `mcdonald` and case-sensitive IDs or tokens break. Normalise only what needs it, for example the static leading segments, or solve the case problem on the one route with a matcher.

saying these in an interview costs you the question

  • A route can set both path and matcher
  • A matcher can return a Promise to look up the URL on the server
  • posParams values arrive in paramMap as UrlSegment objects
  • Returning null from a matcher cancels the navigation
  • Angular path matching is case-insensitive by default