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>
)
}
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
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
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']
}
})
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 />
)
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
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
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>
}
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
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 ✓
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>
}
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()
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
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'
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'))
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
dependenciesandpeerDependencies - How to handle Windows path separators (always normalize with
/) - Why duplicate React instances break everything and how
dedupefixes 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
📦 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)
fell free to conterbute