Introduction
A client-side router built from first principles.
Michi (道)
atharvdange618/MichiMichi is an educational router built from first principles to explore the architecture behind modern routing systems. Each feature is implemented as a self-contained slice that answers one fundamental question about how client-side routing works, from the History API to nested layouts, data loading, and beyond.10I’ve used React Router and TanStack Router in production for years. They work great. But I never really understood how they work. So I started building one from scratch to find out.
Michi is a client-side router built from first principles – no routing libraries, no framework abstractions, just the raw History API and React primitives. It’s not trying to replace anything. It’s here to answer the question: what actually happens between the click and the render?
The name 道 (michi) is Japanese for “path” or “the way.” Seemed fitting.
The idea
Every feature is built as a slice – a self-contained, demonstrable milestone that builds on the previous one. Each slice answers a specific question about how routing works:
| Slice | Status | What it covers |
|---|---|---|
| 1 | Done | History API - pushState, popstate, and the core router loop |
| 2 | Done | Route Matching - turning URL patterns into regex that actually matches |
| 3 | Done | Nested Routes - the route tree, <Outlet />, and layouts that persist |
| 4 | Done | Data Loaders - render-as-you-fetch with parallel execution |
| 5 | Done | Error Boundaries - isolating failures per route |
| 6 | Done | Prefetch on Hover - running loaders early |
| 7 | Planned | Search Params - typed, serializable URL state |
| 8 | Planned | File-Based Routing - codegen from filesystem |
| 9 | Planned | Typed Routes - compile-time path and param safety |
The stack
- Monorepo: Turborepo + pnpm workspaces
- Language: TypeScript (strict)
- Demo app: Vite + React 19
- Router package: Pure TypeScript, zero runtime dependencies
- Testing: Vitest + @testing-library/react
What’s done
Slices 1 through 6 are built and working. The router supports:
- Dynamic params –
/user/$idmatches/user/atharvand extracts{ id: "atharv" } - Wildcard routes –
/files/*matches/files/public/uploads/report.pdf - Nested layouts –
/settingswraps its children in a sidebar layout via<Outlet /> - Pathless layouts –
_authwraps routes without adding a URL segment - Nested outlets – 3+ levels of rendering depth (root, layout, page)
- Programmatic navigation –
useRouter()hook for navigating from event handlers - Persistent layouts – nav and sidebars stay mounted across navigations
- Data loaders – fetch data before rendering with
useLoaderData() - Parallel loader execution – all matched route loaders run simultaneously
- Race condition protection – stale loader results are discarded on rapid navigation
- Per-route error boundaries – loader and render errors are isolated to the failing route
- Custom error components – each route can define its own
errorComponent - Prefetch on hover – run loaders early when the user hovers a link