Representative interview topic

Frontend Interview: How Would You Design Recoverable SPA Navigation with the Navigation API?

FrontendHard
Offer.cc Editorial TeamPublished Updated

Question

How would you redesign an SPA navigation layer to be recoverable and observable without breaking browser history? Explain loading races, error recovery, scroll restoration, and the fallback when Navigation API is unavailable.

Prompt and context

How would you redesign an SPA navigation layer to be recoverable and observable without breaking browser history? Explain loading races, error recovery, scroll restoration, and the fallback when Navigation API is unavailable.

This question fits frontend, full-stack, and web-platform roles. It tests the browser navigation model, asynchronous state machines, and progressive enhancement rather than memorized framework configuration. The Navigation API provides a unified view of same-document navigation through events such as navigate, navigatesuccess, and navigateerror, plus intercept(). It does not replace server-rendered initial loads or change the security boundary of cross-document navigation.

What interviewers assess

  • Treating the URL and history entry as navigation state, not only component memory.
  • Distinguishing same-document routes from documents, downloads, forms, and cross-origin links.
  • Handling stale requests, cancellation, errors, and back/forward navigation.
  • Defining scroll, focus, and page-state restoration rules.
  • Keeping the first load and unsupported browsers functional.
  • Instrumenting success, failure, timeout, cancellation, and recovery.

A 30-second answer

“I would keep server-rendered entry points and normal links working, with the URL and history as the source of truth. When Navigation API is available, I would intercept only same-origin application routes, start cancellable loading in the navigate event, and let a newer navigation cancel an older one. Only the current task can commit the view. On success I restore scroll and focus; on failure I keep the old view and offer retry or reload. Unsupported browsers use the existing History API path, sharing the same loader and metrics.”

Step-by-step solution

Step 1: Bound the navigations you intercept

Check whether the destination is same-origin, an application route, and safe to render in the current document. External links, downloads, cross-origin destinations, special protocols, and form semantics should retain default browser behavior. The first document request still belongs to the server and browser; a navigate event cannot make a failed script load recoverable by itself.

Call intercept() only for an approved application route. The handler loads data, updates the view, and applies scroll policy. For every other destination, allow the browser to navigate normally. This preserves link semantics and avoids turning platform navigation into an accidental client-only state machine.

Step 2: Make navigation cancellable

Assign an increasing sequence number to every navigation and pass its AbortSignal to data loaders. If a user moves from /search?q=a to /search?q=ab, a late response from the first request must not overwrite the second. Before committing, check that the signal is not aborted, the sequence is current, and the destination still matches the intended state.

js
let latestNavigation = 0;

navigation.addEventListener("navigate", (event) => {
  if (!event.canIntercept || !isAppRoute(event.destination.url)) return;
  const id = ++latestNavigation;
  event.intercept({
    async handler() {
      const data = await loadRoute(event.destination.url, event.signal);
      if (event.signal.aborted || id !== latestNavigation) return;
      renderRoute(data);
    }
  });
});

The example shows the race rule, not a complete router. Production code still needs loader errors, timeouts, cache hits, and component teardown. “The last response wins” is incorrect because network completion order is not the user’s latest intent.

Step 3: Commit history, scroll, and page state

After a successful navigation, commit the destination URL as the result. If a small recoverable value belongs in the current history entry, updateCurrentEntry() can store it; large objects and sensitive data do not belong there. Scroll policy should distinguish a new route, back/forward, and an anchor: new routes commonly start at the top, while history traversal restores the saved position.

Avoid changing the title, selected item, or breadcrumb at the start of a load unless the UI explicitly represents a pending state. Commit the main visual state after success, and keep the old content with a retry action after failure. navigatesuccess and navigateerror are useful observation points for latency, cancellation, and failure metrics.

Step 4: Recover from errors

The old page should remain usable when loading fails. Provide retry, back, and full-reload actions, and distinguish authentication, authorization, missing resources, and temporary network failures. If a destination requires a full document response, stop intercepting and let the browser handle it.

If client state or script execution is broken, normal links and server routes must still rebuild the page from the URL. Do not make recovery depend on an in-memory cache. Record the destination, navigation sequence, error class, and whether a fallback occurred so that “the address changed but the old page remained” is diagnosable.

Step 5: Apply progressive enhancement

Navigation API support is newer than the baseline History API, so it cannot be the only route. Detect window.navigation and the methods you actually use, then select the enhanced path. Unsupported browsers should reuse the same route loader through the existing History API or framework router; do not fork business rules merely for compatibility.

Test first load, refresh, back/forward, interrupted links, and script failure in both capability paths. Include slow networks, rapid navigation, cross-origin links, and downloads. Capability detection should be close to the feature use rather than a brittle browser-version list.

Step 6: Verify performance and accessibility

Use real tasks: search, filtering, back, refresh, deep links, and retry. Measure time to interactive, cancellation rate, failure rate, fallback rate, and duplicate requests, grouped by browser capability. Component render time alone cannot explain data, permission, or history failures.

Keep native link semantics, keyboard behavior, and accessible focus movement. After an intercepted navigation, update the document title and move focus to the main content; do not prevent opening a link in a new tab. Caching may reduce latency, but it must not be the only source from which a page can recover.

Information gain and boundaries

The useful distinction is that Navigation API joins platform navigation events, history, and cancellable loading into one state chain. It does not make a data source reliable or turn a cross-origin page into a same-document route. A strong interview answer states the interception boundary, commit point, old-view behavior on failure, and default behavior when the browser lacks the capability.

Model answer

“I would classify navigation into same-origin application routes, full documents, downloads, forms, and cross-origin destinations, and intercept only the first class. Server entry points and ordinary links remain valid, with the URL and history as the source of truth. With Navigation API, the navigate event calls intercept() and creates a sequence number plus cancellation signal. A newer navigation cancels the old load, and the result is committed only if it is still current.

After success I update the view, title, focus, and scroll policy. A new route starts at the top; back and forward restore the history position. Small recoverable state can use the current history entry, while large or sensitive data stays elsewhere. On failure I keep the old view, offer retry, back, or full reload, and record latency, cancellation, and error class through navigation events and server logs.

If Navigation API is unavailable, the same loader runs through History API or the framework router; external links and downloads always use default browser behavior. I would verify deep links, refresh, rapid clicks, slow networks, cross-origin links, script failure, and assistive-technology tasks to ensure address, content, focus, and history never disagree. The result is a progressive navigation layer rather than a client-only replacement that works only in new browsers.”

Common mistakes

  • Intercept every link → downloads, forms, and cross-origin semantics break → intercept only approved same-origin app routes.
  • Let the last response win → stale data can overwrite current intent → use cancellation and sequence checks.
  • Test only rendering → data, permission, and history failures stay hidden → test the full navigation task and recovery.
  • Treat Navigation API as first-load infrastructure → refresh or script failure becomes unrecoverable → keep server responses and normal links.
  • Put all state in history entries → entries become oversized or expose sensitive data → store only small reconstructable state.
  • Duplicate business logic for fallback → enhanced and fallback paths drift → share loaders, state rules, and metrics.

Follow-up questions

What if a stale request already populated the cache?

Cache writes may be allowed, but UI commits still require a current sequence and a non-aborted signal. Key entries by URL, parameters, and version. A late result may warm a future request, but it must not mutate the current page.

Should an authorization failure go back or redirect to login?

Use explicit server status to distinguish unauthenticated, unauthorized, and missing resources. An unauthenticated user can enter login with a return URL; an unauthorized user needs an explanation and a safe next action. Do not disguise authorization as a generic network error.

How do you test browsers without Navigation API?

Run the same deep-link, refresh, back/forward, slow-network, and failure tasks through capability detection, checking URL, content, title, focus, and scroll. Behavioral parity matters; internal event sequences do not need to be identical.

Why not rely entirely on a framework router?

A framework router can be reused, but the answer must define the browser boundary: which navigations stay native, which can be enhanced, and how cancellation, history, and errors map into framework events. Start from platform semantics, then describe the adapter.

Public sources

Related questions