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
Linkcomponent 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.