skip to content

In a Vue Router 5 multi-tenant dashboard, how do you register, check and remove permission-based feature routes so a tenant switch or logout leaves no stale routes behind?

level: seniorimportance: should knowfreq 32%

answer

  1. addRoute returns a remover
  2. names make routes removable
  3. children and aliases go too
  4. removing does not navigate
  5. links to removed names throw

basics

~20 s

Add each feature's records with router.addRoute('dashboard', record) and keep the removers it returns, or give records names for router.removeRoute(name). On a tenant switch, navigate to a neutral page first, then remove the old set and add the new one.

solid answer

~50 s

In Vue Router 5 the runtime route API is `addRoute(record)` or `addRoute(parentName, record)`, which returns a function that removes what it added; `removeRoute(name)`; `hasRoute(name)`; `getRoutes()`, which lists the registered records; and `clearRoutes()`. For a tenant's feature modules I add each module's records under the dashboard layout and keep the removers in one registry, or rely on unique names; adding a record whose name already exists replaces the old one. Removing a record also removes its children and aliases. Removal does not navigate, so on a tenant switch or logout I first move the user to a neutral page, then remove the old modules and register the new ones. A `router.push()` or RouterLink by the name of a removed route throws a no-match error, so menus should be built from what is registered. The routes only shape the UI; the server still checks every request.

code

ts · 18 lines
ts
import type { RouteRecordRaw } from 'vue-router'
import { router } from './router'

type Feature = { id: string; route: RouteRecordRaw }
const installed = new Map<string, () => void>()

export async function switchTenant(load: () => Promise<Feature[]>) {
  await router.replace({ name: 'tenant-picker' })

  for (const remove of installed.values()) remove()
  installed.clear()

  for (const feature of await load()) {
    installed.set(feature.id, router.addRoute('dashboard', feature.route))
  }

  await router.replace({ name: 'dashboard-home' })
}

go deeper

for a junior

Recall that addRoute adds routes at runtime, removeRoute removes a named route, and hasRoute checks whether one exists.

for a middle

Explain the returned remover, name uniqueness and replacement, that removal takes children and aliases, and that none of these methods navigates.

for a senior

Show a safe tenant switch: leave the page, uninstall, install, navigate, with menus built from getRoutes and no stale named links.

for a principal

Weigh a runtime route registry against one static table filtered by guards: download size and isolation versus simpler debugging and deep links.

## The runtime route API Vue Router 5 exposes these methods on the router instance for changing the route table while the app runs: | Method | What it does | |---|---| | `addRoute(record)` | adds a top-level record and returns a function that removes it | | `addRoute(parentName, record)` | adds the record as a child of the named route | | `removeRoute(name)` | removes the named record with its children and aliases | | `hasRoute(name)` | tells whether a record with that name is registered | | `getRoutes()` | returns the registered, normalized route records | | `clearRoutes()` | removes every record | Two rules sit under the table. **Names are unique**: adding a record whose name is already registered removes the old record first, and in development a nested record may not reuse an ancestor's name. **Changes are silent**: none of these methods navigates. ## Registering a tenant's features Each feature module exports its route records, and a registry decides which ones the current user gets: ```ts const installed = new Map<string, () => void>() export async function installFeatures(permissions: string[]) { for (const feature of await loadFeatureModules(permissions)) { installed.set(feature.id, router.addRoute('dashboard', feature.route)) } } export function uninstallFeatures() { for (const remove of installed.values()) remove() installed.clear() } ``` Keeping the **remover** that `addRoute()` returns works even for unnamed records. With named records, `router.removeRoute(name)` does the same, and a `Symbol` as the name rules out collisions between teams' modules. ## Switching tenants and logging out Because removal does not navigate, order matters: 1. navigate away first, for example `await router.replace({ name: 'tenant-picker' })`, so the page on screen does not belong to a record about to disappear; 2. uninstall the previous tenant's features; 3. install the new tenant's features; 4. navigate to the new tenant's start page. Skipping step 1 leaves the old tenant's page on screen, still showing its data, until something navigates; a reload of that URL then no longer matches the removed route. ## Links, menus and stale names Navigating by the name of a removed record fails loudly: `router.push({ name: 'billing' })`, and a RouterLink with that `to`, make the matcher throw a no-match error when it resolves the location. Two habits prevent it: - build the navigation menu from the registered records, for example `router.getRoutes().filter(r => r.meta.menu)`, or guard each entry with `router.hasRoute(name)`; - clear cached locations, such as a *last visited page*, when a tenant switch removes their routes. In the current source `router.resolve()` also depends on the route table, so a RouterLink with a path `to`, rendered before its route was added, re-resolves once the record arrives. ## Reading the table back `router.getRoutes()` returns **normalized** records, which differ from what you passed to `addRoute()`: - every record appears as its own entry, children included, except a record with no component, no name and no redirect, which only contributes a path prefix; - each record's `path` is the **full** path, so a `reports` child of a `/t/:tenant` dashboard reads `/t/:tenant/reports`; - aliases appear as extra entries that point back to their original record. That makes `getRoutes()` a good source for a generated navigation menu or a debugging panel listing what a tenant can reach, and a good assertion target in tests. It is a snapshot: the array does not update itself, so read it again after installing or removing modules. ## What the route table is not Registering only permitted routes keeps other tenants' pages out of the UI and out of the downloaded code, because each module's components stay lazy. It is not access control: anyone can call the API behind a page. The server must authorise every request on its own. ## Checks worth automating - a test that installs, uninstalls and reinstalls a tenant's modules, then asserts on `router.getRoutes()` to prove nothing leaks between tenants; - a lint or review rule that feature modules name their records with a module prefix or a `Symbol`; - a development check with `router.hasRoute()` before navigating by name from shared code. ## Common mistakes - Removing routes while one of them is on screen. - Adding a second tenant's modules on top of the first without removing anything. - Unnamed records added without keeping their removers, which then cannot be removed individually. - Treating the client route table as security.

  • What happens to a removed route's children and aliases?
    They go with it. Removing a record, through `removeRoute(name)` or the function `addRoute()` returned, also removes every child record and every alias record created from it, so removing the billing layout removes invoices, payments and plans as well.
  • Two teams' modules both register a route named `settings`. What does Vue Router 5 do?
    The second `addRoute()` removes the first record, because names are unique across the router, and registers its own. Nothing warns about it at runtime, so the first team's settings page silently disappears. Prefix names with the module id or use `Symbol` names to rule this out.

The route table is like a building's directory board: adding a feature is putting up a new sign, removing one takes the sign and every sub-sign under it down, but neither moves the people already in a room. You walk visitors to the lobby before you rearrange the board, and a visitor asking for a room whose sign is gone gets turned away loudly.

saying these in an interview costs you the question

  • removeRoute navigates the user away from a removed page
  • Removing a parent route leaves its children registered
  • Registering only permitted routes is enough to secure tenant data
  • Adding a route with an existing name throws an error
  • Unnamed routes can be removed with removeRoute by their path