skip to content

Your web app's client-side routing is built directly on the History API. How would you decide whether to move it onto the Navigation API, and how would you sequence that change given uneven browser support?

level: principalimportance: nice to knowfreq 16%

answer

  1. price the pain you already have
  2. two engines is the real cost
  3. one interface, one startup branch
  4. roll out on measurement
  5. write the deletion criterion down

basics

~20 s

Decide from what the current routing code costs you — bespoke click interception, focus and scroll bugs, no real transition lifecycle — against maintaining two engines while support is uneven. Sequence it behind one router interface, enable the new path progressively, and delete the fallback on telemetry.

solid answer

~60 s

I would treat it as a capability decision, not a fashion one. The Navigation API buys a single interception point for every navigation, an async transition with `committed` and `finished` promises, cancellation through `event.signal`, default focus reset and scroll restoration, and an addressable entry list with stable keys. The price is that browsers without `window.navigation` still need the History path, so for a while you own two implementations and a doubled test matrix. So I look at what the current code actually costs: how much bespoke click and submit filtering we carry, whether we have open accessibility bugs around focus after navigation, whether loading and error states for route transitions are ad hoc. If those are cheap, waiting is fine. If they are expensive, I put both engines behind one small router interface, ship the Navigation API path behind a flag to a slice of traffic, compare route-transition telemetry, then default it on. The fallback stays until usage data says it is dead, and deleting it is a scheduled piece of work, not a hope.

go deeper

for a junior

Understand that a newer browser API usually needs a fallback path, and that adopting one is a decision about users and maintenance, not just about the API being better.

for a middle

Be ready to describe the concrete wins — one interception point, real transition promises, default focus and scroll handling — and the concrete cost of running two implementations.

for a senior

Show how you would de-risk it: a router interface shipped first as a no-op refactor, the new engine behind a flag, rollout judged on telemetry rather than a manual pass.

for a principal

Own the whole arc, including the part teams skip: the written exit criterion for the fallback, the audience data behind the decision, and the case for not doing it at all.

## Frame the decision around cost already being paid The wrong version of this decision is "the new API is nicer, let's migrate". The right version starts with what the existing routing code costs the team every month. Typical line items in a History-API router: - A capturing click handler with a long filter list — modifier keys, non-primary buttons, `target` attributes, `download` links, cross-origin hrefs, elements inside shadow trees. Every few months someone reports a link that behaves wrongly, and the fix goes into that filter. - Form submissions that route inconsistently, or not at all. - Focus after navigation. This is the accessibility bug almost every hand-rolled router has: the new view renders and focus is still on the link the user activated, or nowhere at all. - Scroll restoration that has to be reimplemented per route and mostly is not. - No lifecycle. There is no platform-level "this transition finished" or "this transition failed", so loading indicators and error handling are wired route by route. What the Navigation API gives back maps onto that list almost item for item: one `navigate` event for link clicks, form submissions, traversals and programmatic navigation; `event.intercept({ handler })` where the handler's promise *is* the transition, ending in `navigatesuccess` or `navigateerror`; `focusReset` and `scrollBehavior` defaulting to the correct behaviour; `event.signal` aborting superseded work; and `entries()` with stable keys so "go back to where the flow started" is addressable rather than a delta. If your list of costs is short — a small app, few links, no accessibility complaints — the honest answer is that this migration is not the highest-value work available, and waiting for support to broaden makes it cheaper later. ## Price the dual-engine period honestly While any meaningful share of your users lack `window.navigation`, you ship both paths. That means: - Two implementations of the same routing behaviour, which must stay behaviourally identical or bugs become browser-specific. - A doubled test matrix for navigation-adjacent behaviour: scroll position, focus, back/forward, cancellation, error states. - A real risk of double-handling if both engines are ever live at once — a click caught by the legacy handler *and* surfacing as a `navigate` event renders the route twice. That last one is the argument for a hard branch: choose an engine once at startup based on `"navigation" in window`, never mix. ## Sequence it as a seam, a flag, and a deletion date 1. **Create the seam first, on the current engine.** Before touching anything, express routing as a small interface the app already uses — start, navigate to a URL, subscribe to route changes. Ship that alone. It is a refactor with no behaviour change and it is the piece that makes everything after it reversible. 2. **Implement the Navigation API engine behind the same interface.** No app code changes. 3. **Roll it out progressively.** A flag choosing the engine on supporting browsers, enabled for a small slice of traffic first. Compare the things you already measure — route transition timing, error rates, whatever session-quality metric you trust — between engines rather than declaring success from a manual pass. 4. **Default it on** for supporting browsers once the comparison is flat or better. 5. **Set an exit criterion for the fallback in advance.** "Below X% of sessions lacking the API for two consecutive months" beats "we'll remove it when we get to it". Without a written criterion the fallback becomes permanent, and permanent duplicated engines are how routing rots. ## What would make me decide against it - A large share of the audience on engines without the API, making the fallback the primary path — you would be maintaining two engines to benefit a minority. - Routing already delegated to a library that will adopt the API itself; then the right move is to upgrade the library, not to fork navigation handling underneath it. - No routing pain to point at. Migrations justified by novelty rather than by a cost line are the ones that get abandoned half-finished, and a half-finished routing migration is worse than either endpoint. ## The signal an interviewer is listening for Not enthusiasm for the API. They want to hear that you priced the current pain, priced the dual-engine period, built a reversible seam before the risky part, rolled out on measurement rather than vibes, and wrote down when the old path dies. The same shape applies to adopting any platform API that is not yet universal; the Navigation API is just a good, current example to reason about out loud.

  • What single metric would most change your mind about doing this migration now?
    The share of sessions on browsers without `window.navigation`, taken from our own analytics rather than global support tables. If the fallback would serve a large slice of real users, the new engine is a second implementation benefiting a minority and the work waits. If that share is small and shrinking, the dual-engine period is short and the case is much stronger.
  • Why build the router interface before implementing the new engine rather than alongside it?
    Because it separates a zero-behaviour-change refactor from a behaviour-changing rewrite. Shipping the seam alone means any regression it causes is diagnosed on its own, and the risky engine work lands as an addition behind an interface that already works — which also makes turning it off a flag flip rather than a revert.
  • What goes wrong if both routing engines are active at the same time?
    Double handling. A link click is caught by the legacy capturing click handler, which cancels it and pushes a URL — and that programmatic navigation surfaces as a `navigate` event the new engine also handles. The route renders twice, or an extra history entry appears, and the symptom is intermittent enough to be painful to diagnose. Choose one engine at startup.
  • How do you keep the fallback from becoming permanent?
    By writing the exit criterion into the plan at the start, as a measurable threshold with an owner and a review date, and by treating the removal as scheduled work rather than cleanup someone might get to. Duplicated navigation engines diverge quietly, so the removal date is part of the migration, not an afterthought.

saying these in an interview costs you the question

  • Migrates because the API is new, with no cost named
  • Ignores the fallback and assumes everyone is on Chromium
  • Rewrites routing in place with no interface or flag
  • Leaves both engines live during the transition
  • Has no plan or criterion for deleting the old path

context