bini-router
v1.0.27
Published
File-based routing, nested layouts, auto-imports, and Hono-powered API routes for Vite + React
Maintainers
Readme
bini-router
File-based routing, nested layouts, per-route metadata, and Hono-powered API routes for Vite.
Like Next.js — but pure SPA, zero server required.
Features
- 🗂️ File-based routing —
page.tsx/page.jsxfiles map directly to URLs - 🪆 Nested layouts — layouts wrap their segment and all children
- 🏷️ Per-route metadata —
export const metadatain any layout or page - 🔀 Dynamic segments —
[id]/page.tsx→/:id - 🌐 API routes — Hono-powered, pure
Request → Responsehandlers - ✨ Auto-imports —
useState,useEffect,Link,useNavigate,getEnvand more available in every page without importing - 🌿 Auto env loading —
.envloaded automatically for API routes via bini-env — no manual dotenv setup - 🎨 Custom loading screen — create
src/app/loading.tsxto replace the built-in spinner - 🛡️ Built-in error boundaries — per-layout crash isolation
- ⏳ Lazy loading — every route is code-split automatically
- 🔄 HMR — file watcher with smart debounce, dedup, and live new-folder detection
- 📦 Zero config — works out of the box
- 💛 JavaScript & TypeScript — full support for both, auto-detected
- 🚀 Deploy anywhere — Netlify, Vercel, Cloudflare, Node, Deno, Bun, AWS
- 🐙 GitHub Pages ready —
basenameset automatically fromimport.meta.env.BASE_URL
Install
npm install bini-router hono bini-env
honoandbini-envare required peer dependencies.
Setup
vite.config.ts / vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { biniroute } from 'bini-router'
import { biniEnv } from 'bini-env'
export default defineConfig({
plugins: [react(), biniEnv(), biniroute()],
})index.html
<!DOCTYPE html>
<html lang="en">
<head>
<!-- bini-router injects all meta tags here automatically -->
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>You do not need to manually add
<title>,<meta>, favicons, or Open Graph tags.
bini-router reads yourmetadataexport and injects everything at build time.
Auto-imports
bini-router automatically injects imports into every page and layout file in src/app/. You never need to write import statements for these:
From react:
useState useEffect useRef useMemo useCallback
useContext createContext useReducer useId useTransition useDeferredValueFrom react-router-dom:
Link NavLink useNavigate useParams useLocation useSearchParams OutletFrom bini-env:
getEnv requireEnvSo your pages look like this — no imports needed:
// src/app/profile/page.tsx
export default function Profile() {
const { id } = useParams();
const navigate = useNavigate();
const [user, setUser] = useState(null);
return (
<div>
<Link to="/">← Home</Link>
<h1>Profile {id}</h1>
</div>
);
}If you already import from one of these packages manually, bini-router detects it and skips injection — no duplicates ever.
Environment Variables
bini-router uses bini-env to handle environment variables automatically:
- Client code — use
import.meta.env.BINI_*(prefix set automatically by bini-env) - API routes — use
getEnv()orrequireEnv()— no dotenv import needed - Dev server —
.envis loaded automatically when the server starts - Production — env vars are read from the host's environment (Netlify dashboard, Vercel settings, etc.)
# .env
BINI_FIREBASE_API_KEY=your_key # client-side — accessible via import.meta.env.BINI_*
[email protected] # server-side — accessible via getEnv() in API routes
SMTP_PASS=your_password
FROM_EMAIL=App <[email protected]>// src/app/api/email.ts — getEnv/requireEnv are auto-imported
const SMTP_USER = requireEnv('SMTP_USER'); // throws if missing
const DEBUG = getEnv('DEBUG_MODE'); // returns undefined if missingJavaScript & TypeScript
bini-router supports both JavaScript and TypeScript projects out of the box — no extra configuration needed.
Auto-detection: bini-router checks for src/main.tsx or src/main.ts to determine project type. Falls back to scanning src/app/ for .ts/.tsx files.
| | TypeScript project | JavaScript project |
|---|---|---|
| Auto-generated app entry | src/App.tsx | src/App.jsx |
| ErrorBoundary | Full generic types | Plain JS class |
| TitleSetter | Typed props | Plain JS function |
| Your pages / layouts | .tsx | .jsx |
| API routes | .ts | .js |
File Structure
src/
main.tsx ← mounts <App /> as usual
App.tsx ← auto-generated by bini-router — do not edit
app/
globals.css ← global styles
layout.tsx ← root layout + global metadata
page.tsx ← /
loading.tsx ← custom loading screen (optional)
not-found.tsx ← custom 404 page (optional)
dashboard/
layout.tsx ← nested layout for /dashboard/*
page.tsx ← /dashboard
[id]/
page.tsx ← /dashboard/:id
blog/
[slug]/
page.tsx ← /blog/:slug
api/
users.ts ← /api/users
posts/
index.ts ← /api/posts
[id].ts ← /api/posts/:id
[...catch].ts ← /api/* catch-allPages
// src/app/dashboard/page.tsx — no imports needed
export default function Dashboard() {
const [count, setCount] = useState(0);
return <h1>Dashboard</h1>;
}Dynamic routes
// src/app/blog/[slug]/page.tsx — useParams auto-imported
export default function Post() {
const { slug } = useParams();
return <h1>Post: {slug}</h1>;
}Layouts
Layouts wrap all pages in their directory and subdirectories.
// src/app/layout.tsx — root layout
export const metadata = {
title : 'My App',
description: 'Built with bini-router',
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return <>{children}</>;
}// src/app/dashboard/layout.tsx — nested layout (Outlet auto-imported)
export const metadata = {
title: 'Dashboard',
}
export default function DashboardLayout() {
return (
<div className="dashboard">
<aside>Sidebar</aside>
<main><Outlet /></main>
</div>
);
}Root layout uses
{children}— it wraps<BrowserRouter>from outside.
Nested layouts use<Outlet />— they are React Router route wrappers.
Custom Loading Screen
Create src/app/loading.tsx with a default export to replace the built-in spinner. bini-router automatically detects and uses it as the Suspense fallback for every lazy-loaded route.
// src/app/loading.tsx
export default function Loading() {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-10 w-10 border-t-2 border-blue-500" />
</div>
);
}If the file is empty or has no default export, the built-in spinner is used automatically.
Custom 404
// src/app/not-found.tsx
export default function NotFound() {
return (
<div>
<h1>404 — Page not found</h1>
<Link to="/">Go home</Link>
</div>
);
}Metadata
Export metadata from any layout.tsx. Root layout metadata is injected into index.html at build time. Nested layout titles update document.title at runtime.
export const metadata = {
title : 'Dashboard',
description : 'Your personal dashboard',
viewport : 'width=device-width, initial-scale=1.0',
themeColor : '#00CFFF',
charset : 'UTF-8',
robots : 'index, follow',
manifest : '/site.webmanifest',
keywords : ['react', 'vite', 'dashboard'],
authors : [{ name: 'Your Name', url: 'https://example.com' }],
metadataBase: new URL('https://myapp.com'),
openGraph: {
title : 'Dashboard',
description: 'Your personal dashboard',
url : 'https://myapp.com/dashboard',
type : 'website',
images : [{ url: '/og.png', width: 1200, height: 630 }],
},
twitter: {
card : 'summary_large_image',
title : 'Dashboard',
description: 'Your personal dashboard',
creator : '@yourhandle',
images : ['/og.png'],
},
icons: {
icon : [{ url: '/favicon.svg', type: 'image/svg+xml' }],
shortcut: [{ url: '/favicon.png' }],
apple : [{ url: '/apple-touch-icon.png', sizes: '180x180' }],
},
}All fields are optional. metadata is stripped from the browser bundle automatically.
API Routes
Write your API files in src/app/api/. Export a Hono app — bini-router serves it in dev and production automatically. No dotenv, no manual env loading.
// src/app/api/email.ts
import { Hono } from 'hono'
import nodemailer from 'nodemailer'
const app = new Hono().basePath('/api')
// requireEnv is auto-imported — throws at startup if missing
const transporter = nodemailer.createTransport({
host: 'smtp-relay.brevo.com',
port: 587,
auth: {
user: requireEnv('SMTP_USER'),
pass: requireEnv('SMTP_PASS'),
},
})
app.post('/email', async (c) => {
const { to, subject, html } = await c.req.json()
await transporter.sendMail({ from: requireEnv('FROM_EMAIL'), to, subject, html })
return c.json({ ok: true })
})
export default appPlain function handlers
// src/app/api/hello.ts
export default function handler(req: Request) {
return Response.json({ message: 'hello', method: req.method })
}Dynamic routes
// src/app/api/posts/[id].ts
import { Hono } from 'hono'
const app = new Hono().basePath('/api')
app.get('/posts/:id', (c) => c.json({ id: c.req.param('id') }))
export default appGitHub Pages / Subpath Deployments
bini-router sets basename={import.meta.env.BASE_URL ?? '/'} on <BrowserRouter> automatically.
// vite.config.ts
export default defineConfig({
base : '/my-repo/',
plugins: [react(), biniEnv(), biniroute()],
})Deployment
Set platform once in vite.config.ts. bini-router generates the production entry file on every vite build.
biniroute({ platform: 'netlify' })🟩 Netlify
biniroute({ platform: 'netlify' })Generates netlify/edge-functions/api.ts — uses Deno URL imports, no npm deps needed.
Add netlify.toml:
[build]
command = "vite build"
publish = "dist"
[[edge_functions]]
path = "/api/*"
function = "api"
[[redirects]]
from = "/*"
to = "/index.html"
status = 200▲ Vercel
biniroute({ platform: 'vercel' })Generates api/index.ts using Vercel Edge Functions with native Hono fetch.
⚠️ Vercel reads
api/before the build step. You must commit the generated file:git add api/index.ts git commit -m "chore: update vercel api entry" git push
🟠 Cloudflare Workers
biniroute({ platform: 'cloudflare' })Generates worker.ts. Add wrangler.toml:
name = "my-app"
main = "worker.ts"
compatibility_date = "2024-01-01"
[assets]
directory = "./dist"vite build && npx wrangler deploy🚂 Node.js (Railway, Render, Fly.io, VPS)
biniroute({ platform: 'node' })Generates server/index.ts. Requires @hono/node-server:
npm install @hono/node-serverStart command:
vite build && npx tsx server/index.ts🦕 Deno
biniroute({ platform: 'deno' })vite build && deno run --allow-net --allow-read server/index.ts🥟 Bun
biniroute({ platform: 'bun' })vite build && bun run server/index.ts☁️ AWS Lambda
biniroute({ platform: 'aws' })Generates handler.ts. Deploy with SST, SAM, or Serverless Framework.
Options
biniroute({
appDir : 'src/app', // Default: src/app
apiDir : 'src/app/api', // Default: src/app/api
cors : true, // CORS on dev/preview API. Default: true
platform: 'netlify', // 'netlify' | 'vercel' | 'cloudflare' | 'node' | 'deno' | 'bun' | 'aws'
})HMR & File Watcher
bini-router watches src/app/ during development and regenerates App.tsx automatically.
- New file → picked up immediately
- New folder → watched instantly
- Deleted file or folder → removed from routes and reloaded
- Changes are debounced — no redundant reloads
You never need to restart the dev server when adding new routes.
License
MIT © Binidu Ranasinghe
