Skip to main content

Why Hash-Based Routing

  • By Alan James

I spent time deciding how routing should work in the frontend.

On the surface, this looks like a small, almost cosmetic decision. Whether URLs look like /page1 or #/page1 feels trivial. But, just like database and authentication choices, routing decisions have second-order effects that only show up over time.

I’ve built and maintained multiple SPAs using History API routing, hash-based routing, and framework-managed routing. Every time, the same pattern emerges.

Q: What is the size of the team?
A: As small as possible.

Q: What causes frontend systems to degrade over time?
A: Implicit behavior, hidden coupling, and rules that must be remembered rather than enforced.

Q: What keeps frontend systems healthy long-term?
A: Clear ownership boundaries and platforms that prevent incorrect behavior by default.


The Problem with History API Routing

History API routing (/page1) looks clean, but it introduces invisible dependencies.

The browser treats /page1 as a real resource. That means:

  • The server must be configured to always return index.html
  • Reverse proxies must be correctly rewritten
  • Docker, NGINX, and hosting configs must stay aligned
  • Every internal link must intercept navigation
  • Developers must remember not to use plain <a> tags
  • One missed rule causes a full page reload

None of these failures are obvious at first. They appear gradually as exceptions, workarounds, and “just this once” decisions.

This is not a discipline problem. It is a system design problem.

History API routing relies on everyone doing the right thing, every time.


What Hash-Based Routing Actually Does

Hash-based routing (#/page1) draws a hard line of responsibility.

Everything before # belongs to the server. Everything after # belongs to the frontend.

The browser enforces this boundary.

Because of that:

  • Route changes never trigger a network request (other than API requests from mount events)
  • No server rewrites are required
  • NGINX configuration becomes trivial
  • Docker images stay simple
  • Plain <a> tags work correctly
  • Back/forward behavior works automatically
  • It is nearly impossible to accidentally cause a full page reload

This is not a workaround. It is using the browser as designed.


Why This Matters for a Small Team

Over time, frontend codebases decay in predictable ways:

  • Someone forgets to use the Link component when using Next
  • Someone adds a direct <a href="/page">
  • Someone changes server config without realizing routing depends on it
  • Someone introduces a new hosting target with slightly different behavior

Hash routing removes entire classes of these problems.

It does not require:

  • Documentation
  • Code review vigilance
  • Custom abstractions
  • Team-wide discipline

It simply cannot break in those ways.


The Tradeoff

The tradeoff is mostly aesthetic.

#/page1 is less visually clean than /page1.

The only functional change is that URLSearchParams does not find query parameters set after the #.

So URLSearchParams will not work for /#/search?page=1 but will work for ?page=1#/search.

Again, this mostly comes back to aesthetics.

Additionally, the majority of users do not manually alter their URLs to navigate to pages.

The average user will never even notice the URL structure.

These types of URLs can also hurt SEO, but for this application:

  • Everything is behind login
  • SEO is irrelevant
  • No public marketing pages exist
  • URLs are for users, not crawlers
  • Stability matters more than appearance

The Broader Pattern

This choice mirrors other decisions in the system:

  • Postgres instead of MongoDB
  • Cookie sessions instead of JWT
  • NestJS instead of ad-hoc Express
  • Static SPA instead of SSR complexity

Each choice reduces flexibility in exchange for correctness.

Each choice moves enforcement into the platform instead of the team.


Conclusion

Hash-based routing is not outdated.

It is boring. It is explicit. It is constrained. It is difficult to misuse.

Those are the exact properties needed for a long-lived system with a small team.

So the choice is obvious.

Hash routing enforces order, and that is what this project needs.