DEV Community

Cover image for I Got Tired of React Router Boilerplate — So I Built Next.js Routing for Vite
Amirmahdi Sultani
Amirmahdi Sultani

Posted on

I Got Tired of React Router Boilerplate — So I Built Next.js Routing for Vite

I love React. I love Vite. Fast builds, full control, no magic, no framework
telling me how to structure my entire application. Just React the way it was
meant to be used.

But every single project, without fail, I found myself writing this:

import { BrowserRouter, Routes, Route } from 'react-router'
import Home from './pages/Home'
import About from './pages/About'
import Blog from './pages/Blog'
import BlogPost from './pages/BlogPost'
import NotFound from './pages/NotFound'

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/blog" element={<Blog />} />
        <Route path="/blog/:id" element={<BlogPost />} />
        <Route path="*" element={<NotFound />} />
      </Routes>
    </BrowserRouter>
  )
}
Enter fullscreen mode Exit fullscreen mode

Every. Single. Project.

And every time a new page gets added, I open this file, add an import at the
top, add a Route at the bottom, save, and get back to what I was actually
trying to do.

It's not hard. It's just... noise. Pure boilerplate that adds zero value.


Then I Looked at Next.js

Next.js developers don't write any of this. They just create a folder.

app/
├── page.jsx          ← that's /
├── about/
│   └── page.jsx      ← that's /about
└── blog/
    └── [id]/
        └── page.jsx  ← that's /blog/:id
Enter fullscreen mode Exit fullscreen mode

The route exists the moment the file exists. No imports. No declarations.
No ceremony.

That's genuinely brilliant. File structure IS the routing structure. It's
visual, it's intuitive, and it completely eliminates an entire category of
boilerplate.

But here's my problem — I don't want to use Next.js.

I don't want a full framework. I don't want server components, I don't want
the Next.js opinions on data fetching, I don't want to be locked into Vercel's
ecosystem. I want React. I want Vite. I want to build my app my way.

I just wanted that one feature.

So I built it.


Introducing upcoming.js

upcoming.js is a Vite plugin that brings Next.js-style file-based routing
to any React + Vite project. No framework. No lock-in. Just the feature.

npm i upcoming.js
Enter fullscreen mode Exit fullscreen mode

The setup is two steps:

Step 1 — Add the plugin to vite.config.js

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import upcoming from 'upcoming.js'

export default defineConfig({
  plugins: [
    react(),
    upcoming()
  ],
  resolve: {
    dedupe: ['react', 'react-dom', 'react-router']
  }
})
Enter fullscreen mode Exit fullscreen mode

Step 2 — Replace your main.jsx

import ReactDOM from 'react-dom/client'
import { UpcomingRouter } from 'upcoming.js/runtime'

ReactDOM.createRoot(document.getElementById('root')).render(
  <UpcomingRouter />
)
Enter fullscreen mode Exit fullscreen mode

That's it. You're done. Now just create folders.


How It Works

Basic Routes

src/
├── page.jsx                   →  /
├── about/
│   └── page.jsx               →  /about
└── contact/
    └── page.jsx               →  /contact
Enter fullscreen mode Exit fullscreen mode

Each page.jsx file is a route. The folder path is the URL. Simple.

Dynamic Routes

Wrap a folder name in square brackets to create a dynamic segment:

src/
└── blog/
    ├── page.jsx               →  /blog
    └── [id]/
        └── page.jsx           →  /blog/:id
Enter fullscreen mode Exit fullscreen mode

And you access the param exactly like you would in React Router:

import { useParams } from 'react-router'

export default function BlogPost() {
  const { id } = useParams()
  return <h1>Post: {id}</h1>
}
Enter fullscreen mode Exit fullscreen mode

Because under the hood, it IS React Router. upcoming.js is just the layer
that wires everything together automatically.

Route Groups

Wrap a folder in parentheses to group routes without affecting the URL:

src/
├── (marketing)/
│   ├── about/
│   │   └── page.jsx           →  /about
│   └── contact/
│       └── page.jsx           →  /contact
└── (app)/
    ├── dashboard/
    │   └── page.jsx           →  /dashboard
    └── settings/
        └── page.jsx           →  /settings
Enter fullscreen mode Exit fullscreen mode

Perfect for feature-based or domain-based folder structures.

Private Folders

Prefix any folder with _ to exclude it from routing completely:

src/
├── _components/
│   └── Navbar.jsx             →  ignored
├── _utils/
│   └── helpers.js             →  ignored
└── about/
    └── page.jsx               →  /about ✓
Enter fullscreen mode Exit fullscreen mode

404 Page

Just create notfound.jsx at the root of your routes folder:

export default function NotFound() {
  return <h1>404 — This page does not exist</h1>
}
Enter fullscreen mode Exit fullscreen mode

Navigation

Since upcoming.js is built on top of React Router, all navigation works
exactly as you already know:

import { Link, useNavigate, useLocation, useParams } from 'react-router'

// Link component
<Link to="/about">Go to About</Link>

// Programmatic navigation
const navigate = useNavigate()
navigate('/dashboard')

// Read current location
const location = useLocation()

// Dynamic params
const { id } = useParams()
Enter fullscreen mode Exit fullscreen mode

No new APIs. No new concepts to learn. If you know React Router, you already
know how to use upcoming.js.


Live Route Updates

One of my favorite parts — when you add a new page.jsx file while the dev
server is running, the route appears instantly. No restart needed.

→ create src/pricing/page.jsx
→ browser reloads automatically
→ /pricing now exists
Enter fullscreen mode Exit fullscreen mode

The Vite plugin watches your folder structure using Vite's built-in file
watcher and invalidates the virtual module whenever a route file is
added or removed.


How It Works Under the Hood

This was the most interesting part to build. Here's the short version:

1. Vite Plugin API

upcoming.js registers itself as a Vite plugin. On startup it scans your
src/ folder recursively looking for page.jsx files and builds a routes
array from the folder structure.

2. Virtual Modules

The routes array gets turned into a JavaScript module that lives in memory —
not on disk. Vite has a concept called virtual modules that lets plugins
intercept imports and return generated code instead of reading from the
filesystem.

So when your Router.jsx does:

import { routes } from 'virtual:upcoming'
Enter fullscreen mode Exit fullscreen mode

Your plugin intercepts that import and returns the generated routes code
on the fly.

3. File Watching

Vite's built-in watcher fires events when files are added or deleted. The
plugin listens for these events, re-scans the folder, regenerates the virtual
module, invalidates the old one in Vite's module graph, and sends a
full-reload signal to the browser via WebSocket.

4. Lazy Loading

Every page component is wrapped in React's lazy() automatically:

const Page0 = lazy(() => import('/src/about/page.jsx'))
Enter fullscreen mode Exit fullscreen mode

So every route is automatically code-split. Users only download the code
for the page they're actually visiting.


This Is My First npm Package

I want to be honest — this is the first package I've ever published. I built
it because I genuinely needed it, not because I wanted to build a package.

I learned a lot along the way:

  • How Vite plugins work and what hooks are available
  • What virtual modules are and how to use them
  • The difference between dependencies and peerDependencies
  • How to handle Windows path separators (always normalize with /)
  • Why duplicate React instances break everything and how dedupe fixes it

If you're thinking about building your first npm package — just build
something you actually need. The motivation to finish it is so much stronger
when you're solving your own real problem.


What's Next

I'm planning to add:

  • layout.jsx — wraps all child routes, like Next.js layouts
  • loading.jsx — per-route loading state
  • error.jsx — per-route error boundary
  • TypeScript support out of the box

Try It

npm i upcoming.js
Enter fullscreen mode Exit fullscreen mode

📦 npm: upcoming.js

🐙 GitHub: github.com/amirmahdi1390/upcoming.js

If you try it and run into any issues, open a GitHub issue. If you find it
useful, a star on GitHub means a lot for a first-time package author ⭐

I'd love to hear what you think.

Top comments (1)

Collapse
 
amirmahdi01 profile image
Amirmahdi Sultani

fell free to conterbute