skip to content

In Vue Router 5, how do a docs route '/docs/:path+' and a '/:pathMatch(.*)*' 404 route match multi-segment URLs, and what params do they produce?

level: middleimportance: should knowfreq 55%

answer

  1. plus, star, question mark
  2. repeatable params become arrays
  3. custom regex in parentheses
  4. trailing star keeps slashes unencoded
  5. wildcards rank last

basics

~10 s

A + param repeats over one or more segments and yields an array, so /docs/guide/setup gives ['guide', 'setup']; /:pathMatch(.) matches any path, ranks below specific routes, and yields the segments as an array.

solid answer

~40 s

Modifiers change how many segments a param takes: `+` is one or more, `*` zero or more, `?` zero or one. A repeatable param yields an **array**, so `/docs/:path+` gives `route.params.path` equal to `['guide', 'setup']` for `/docs/guide/setup`, does not match `/docs`, and needs an array when you push by name. `/:pathMatch(.*)*` combines a custom regex `.*`, which can cross slashes, with the `*` modifier: it matches everything, ranks below every more specific route whatever its array position, and yields the segments as an array. The trailing `*` matters when you push to the 404 route by name: with an array the slashes stay real, while a plain string param would be encoded as `%2F`. This syntax replaced Vue Router 3's `*` catch-all in Vue Router 4 and is unchanged in 5.

code

ts · 12 lines
ts
import { createRouter, createWebHistory } from 'vue-router'

export const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/products/:id', name: 'product', component: () => import('@/views/ProductPage.vue') },
    // /docs/guide/setup -> params.path = ['guide', 'setup']
    { path: '/docs/:path+', name: 'docs', component: () => import('@/views/DocsPage.vue') },
    // ranks last; params.pathMatch = ['any', 'unknown', 'path']
    { path: '/:pathMatch(.*)*', name: 'NotFound', component: () => import('@/views/NotFound.vue') },
  ],
})

go deeper

for a junior

Know that /:pathMatch(.) is the 404 route and that + and * let a param span several segments.

for a middle

Explain the array values, why + rejects /docs, the trailing star's effect on named pushes, and why the catch-all ranks last.

for a senior

Render a 404 in place for missing records without changing the URL, and avoid slow wildcard patterns in the middle of paths.

for a principal

Decide which sections use open-ended paths and how their URLs map to content, so docs moves do not break links.

## The three modifiers Vue Router 5's path syntax lets a param take more or fewer segments than one: | Modifier | Segments | Value when present | Value when absent | |---|---|---|---| | none, `:id` | exactly 1 | string | no match | | `?`, `:lang?` | 0 or 1 | string | key missing | | `+`, `:path+` | 1 or more | array of strings | no match | | `*`, `:path*` | 0 or more | array of strings | key missing | Two rules come from the tokenizer: - A **repeatable** param (`+` or `*`) must be **alone in its segment**. `/docs-:path+` throws when the route is added. - `?` params cannot repeat; `*` is effectively optional and repeatable at once. When an optional param is absent the router deletes the key, so `route.params.lang` is `undefined`, not an empty string. ## The docs section: `/docs/:path+` ```ts { path: '/docs/:path+', name: 'docs', component: DocsPage } ``` - `/docs/guide` gives `{ path: ['guide'] }`. - `/docs/guide/routing/params` gives `{ path: ['guide', 'routing', 'params'] }`. - `/docs` does not match, because `+` needs at least one segment. Use `*` if `/docs` should render the same page. - Pushing by name needs an **array**: `router.push({ name: 'docs', params: { path: ['guide', 'setup'] } })` produces `/docs/guide/setup`. An empty array throws for `+` and produces `/docs` for `*`. Passing an array to a non-repeatable param also throws. - A custom regex applies to **each** repeated segment: `/:chapters(\d+)+` matches `/1/2/3` but not `/1/intro`. The component usually joins the array back: `route.params.path.join('/')` becomes the key for loading the page's markdown. ## Optional params in the catalogue A product page with an optional tab segment shows the `?` modifier: - `{ path: '/products/:id/:tab?', name: 'product' }` matches both `/products/42` and `/products/42/specs`. - On `/products/42`, `route.params.tab` is `undefined`, because the router deletes absent optional params. - `router.push({ name: 'product', params: { id: '42' } })` builds `/products/42`; adding `tab: 'specs'` builds `/products/42/specs`. - The router docs warn about a subtlety: when a segment holds **more** than just the optional param, as in `/products/:id-:variant?`, the rules for the missing value and the trailing slash get stricter, so keep optional params alone in their segment. Optional and repeatable params also cost ranking points, which is why a more specific record wins when both match. ## The 404 route: `/:pathMatch(.*)*` A custom regex goes in parentheses right after the param name. `.*` matches any characters, **including slashes**, which a plain param never does. The pieces of the catch-all: 1. `pathMatch` is just a name; any name works. 2. `(.*)` lets the param swallow the whole remaining path. 3. The trailing `*` makes it repeatable, so the value is an **array of segments**. ```ts { path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound } ``` **Ranking puts it below specific routes.** The router scores each record; a `.*` wildcard gets a large penalty, so the catch-all loses to every more specific route regardless of where it sits in the `routes` array. Putting it at the end is still good style for readers. **Why the trailing star.** Suppose a product id exists in the URL but the API says the product is gone. The page can show the 404 view **without changing the URL**: ```ts router.replace({ name: 'NotFound', params: { pathMatch: route.path.substring(1).split('/') }, query: route.query, hash: route.hash, }) ``` With the array form the slashes are rebuilt as real separators. Declared as `/:pathMatch(.*)` without the star, the param is a single string, and resolving it by name encodes `/` as `%2F`, giving `/products%2F42`. If you never push to the catch-all by name, the star is optional. ## Performance: keep wildcards at the end The router docs warn that `.*` combined with a repeatable modifier and followed by more path, such as `/:pathMatch(.*)*/edit`, builds a very slow regular expression. Use match-everything params only at the **end** of a path; in the middle, drop the repeat modifier. ## What changed from Vue Router 3 Vue Router 3 used `path-to-regexp` and a special `*` path for catch-alls. Vue Router 4 replaced both with its own parser, which is what makes ranking possible, and removed `*` and unnamed params; `/foo/(.*)` must become a named param such as `/foo/:_(.*)`. Vue Router 5 kept the syntax unchanged.

  • The docs page must also render at /docs with an index page. What do you change, and what does the component see?
    Change `+` to `*`: `/docs/:path*` matches `/docs` as well. On `/docs` the `path` key is absent, so the component should treat a missing value as the index. Pushing by name with `path: []` then produces `/docs` instead of throwing.
  • A catch-all 404 route catches /products/abc even though /products/:id exists. Why might that happen?
    It should not with the default pattern, since `/products/:id` outranks the wildcard. It happens when `:id` has a custom regex such as digits only, so `abc` no longer matches it and the catch-all is the only candidate. That is usually the desired 404.

saying these in an interview costs you the question

  • The catch-all must be the last array entry or it swallows every route.
  • Vue Router 5 still supports path: '*' for a catch-all.
  • A :path+ param gives a single string with slashes in it.
  • The trailing * in /:pathMatch(.*)* is decorative and changes nothing.
  • An absent optional param shows up as an empty string.