A file-based routing system for React projects that automatically generates routes from your file structure. Similar to Next.js App Router or Remix file conventions.
npm install aaex-file-routerlayout.tsx files to wrap nested routes
bash
npm install aaex-file-router
`
Quick Start
$3
`
src/pages/
└── dashboard/
├── loading.tsx ← Used as fallback for ALL lazy imports below
├── index.tsx
├── stats/
│ ├── loading.tsx ← Overrides parent
│ └── weekly.tsx
└── users/
└── [id].tsx ← used for dynamic routes ex :users/123
`
$3
`typescript
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { aaexFileRouter } from "aaex-file-router/plugin";
export default defineConfig({
plugins: [
react(),
aaexFileRouter({
pagesDir: "./src/pages", //page files location(optional: default ./src/pages)
outputFile: "./src/routes.ts", //generated routes (default: ./src/routes.ts)
}),
],
});
`
$3
$3
#### 1. Using createBrowserRouter (recommended for most users)
`typescript
// src/App.tsx
import "./App.css";
import routes from "./routes";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import { Suspense } from "react";
function App() {
const router = createBrowserRouter(routes);
return (
Loading...
$3
Note I will probably create a custom route provider using this version later since this is the only solution that works with VITE-SSR if you wrap client in and server in
`tsx
//src/App.tsx
import {
BrowserRouter,
Routes,
Route,
type RouteObject,
} from "react-router-dom";
import routes from "./routes";
import { Suspense } from "react";
import "./App.css";
//recursivly creates nested routes
function createRoutes(route: RouteObject) {
return (
{route.children?.map((child) => createRoutes(child))}
);
}
function App() {
return (
Loading...
File Conventions
$3
Renders at the parent route path.
`
pages/index.tsx → "/"
pages/about/index.tsx → "/about"
`
$3
Wraps all sibling and nested routes. Children are rendered in an . Also works for root layout if placed directly in /pages
`tsx
// pages/admin/layout.tsx
import { Outlet } from "react-router-dom";
export default function AdminLayout() {
return (
{/ Nested routes render here /}
);
}
`
$3
Root & folder level fallback.
Creating a file named 404.tsx inside the page root or inside a subfolder creates a 404 page for that segment.
root => /nonexisting
subfolder ex: blog => blog/nonexisting
will render your 404
`tsx
//src/pages/404.tsx
export default function NotFound() {
return <>404 not found!>;
}
`
$3
Filenames wrapper in square brackets [filename] will resolve to a dynamic route
`
src/pages/test/[].tsx → "/test/:"
`
`tsx
// src/pages/test/[slug].tsx
import { useParams } from "react-router-dom";
export default function TestWithSlug() {
// replace slug with what the file is called
const { slug } = useParams();
return {slug};
}
`
$3
Any other .tsx file becomes a route based on its filename.
`
pages/about.tsx → "/about"
pages/blog/post.tsx → "/blog/post"
`
Route Resolution Examples
| File Structure | Route Path |
| --------------------------------------- | -------------------- |
| src/pages/index.tsx | / |
| src/pages/about.tsx | /about |
| src/pages/blog/index.tsx | /blog |
| src/pages/blog/post.tsx | /blog/post |
| src/pages/admin/layout.tsx + children | /admin/* (grouped) |
Layouts
Layouts wrap their child routes and provide shared UI:
`typescript
// src/pages/dashboard/layout.tsx
import { Outlet } from "react-router-dom";
export default function DashboardLayout() {
return (
);
}
`
All routes in src/pages/dashboard/* will render inside this layout.
Import Strategy
| File type | | | Import style | |
| ------------------------------------- | --- | --- | ------------- | --- |
| pages/\*.tsx (top level) | | | static import | |
| Files inside a folder with layout.tsx | | | Lazy loaded | |
| Files inside a folder without layout | | | Lazy loaded | |
| layout.tsx | | | Static import |
| loading.tsx | | | Static import |
---
FileLink component
The FileLink component is a type safe wrapper for the Link component in react router that uses an autogenerated type to check which routes are available.
Notice!
At the moment it can only do the basic routing where the "to" prop is a string.
React Router's normal Link still works in cases where type safety is less important.
Usage
If reades the type file that is automatically generated
users/{string} is what users/:slug gets translated to this means users/ allows any string after even if the route dosnt exist. Will look into better solution
`ts
// src/routeTypes.ts
// * AUTO GENERATED: DO NOT EDIT
/
export type FileRoutes = "/" | "test" | "users/{string}";
`
`tsx
// src/pages/index.tsx
import { FileLink } from "aaex-file-router";
import type { FileRoutes } from "../routeTypes"; //import type
export default function Home() {
return (
<>
Hello Home!
to="test">Test safe
{/ or without type safety /}
Non safe
>
);
}
`
useScroll
Custom React hook that scrolls to the top after navigation.
Designed to work seamlessly with client-side routing while remaining SSR-safe.
$3
- Automatically scrolls on route change
- Supports smooth scrolling
- Can scroll either the window or a specific container
- Safe to use in SSR environments
$3
`tsx
import { useScroll } from "aaex-file-router";
export default function PageWithNavigation() {
useScroll();
return <>{/ rest of content /}>;
}
`
$3
useScroll accepts an optional config object.
`tsx
useScroll({
behavior?: ScrollBehavior;
container?: HTMLElement | null;
});
`
| Option | Type | Default | Description |
| ----------- | --------------------- | ---------- | ---------------------------------------- | ------------------------- |
| behavior | "auto" | "smooth" | "auto" | Scroll animation behavior |
| container | HTMLElement \| null | null | Scrolls this element instead of window |
$3
`tsx
useScroll({
behavior: "smooth",
});
`
$3
`tsx
const ref = useRef(null);
useScroll({
container: ref.current,
});
return ...;
`
---
Generated files
$3
Generated route definition file
`ts
// src/routes.ts
...imports
export default routes = [
{
path: "/",
element: React.createElement(Index)},
{
path: "test",
element: React.createElement(TestLayout),
children:
[
{
path: "",
element: React.createElement(
React.Suspense,
{ fallback: React.createElement(TestLoading) },
React.createElement(React.lazy(() => import("./pages/test/index.tsx")))
)
},
...
]
}
]
`
$3
Exports TypeScript union type of existing routes
`ts
export type FileRoutes = / | test;
`
API Reference
$3
Scans the file system and converts files into a structured format.
`typescript
import { FileScanner } from "aaex-file-router/core";
const scanner = new FileScanner("./src/pages");
const fileData = await scanner.get_file_data();
`
$3
Converts file structure into React Router route configuration.
`typescript
import { RouteGenerator } from "aaex-file-router/core";
const generator = new RouteGenerator();
const routesCode = await generator.generateRoutesFile(fileData);
`
$3
Automatically watches for file changes and regenerates routes.
`typescript
import { aaexFileRouter } from "aaex-file-router/plugin";
export default defineConfig({
plugins: [
aaexFileRouter({
pagesDir: "./src/pages",
outputFile: "./src/routes.ts",
}),
],
});
`
Server routing
If you are using vite SSR you want to configure your vite config with the serverRouter plugin instead of the normal one.
`ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { aaexServerRouter } from "aaex-file-router/plugin";
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), aaexServerRouter()],
});
`
this plugin generates 2 route files
$3
Includes absolute path to the file modulePath
route example:
`ts
{
"path": "",
"element": React.createElement(Index),
"modulePath": "C:/Users/tmraa/OneDrive/Dokument/AaExJS-documentation/test-app/src/pages/index.tsx"
},
`
$3
Just the normal route file.
How It Works
1. File Scanning: Recursively scans your pages directory and builds a file tree
2. Route Generation: Converts the file structure into React Router RouteObject format
3. Smart Importing:
- Top-level files use static imports for faster initial load
- Nested/grouped routes use lazy loading for code splitting
- Layout files are statically imported as route wrappers
4. Auto-Regeneration: Vite plugin watches for changes and automatically regenerates routes.ts
Performance Considerations
- Static Imports: Top-level routes are statically imported, included in the main bundle
- Code Splitting: Routes nested in layout groups are lazy-loaded, improving initial bundle size
- Watch Mode: File watching only runs in development (vite serve), not in production builds
Common Patterns
$3
`sh
pages/
├── layout.tsx # Wraps entire app
├── index.tsx
└── about.tsx
`
$3
`sh
pages/
├── layout.tsx # Root layout
├── admin/
│ ├── layout.tsx # Admin layout (inherits from root)
│ ├── index.tsx
│ └── users.tsx
`
$3
`sh
pages/
├── blog/
│ ├── post.tsx # Routes as /blog/post (no grouping)
│ └── author.tsx # Routes as /blog/author
`
Troubleshooting
$3
- Ensure Vite dev server is running (npm run dev)
- Check that pagesDir in vite config matches your actual pages directory
$3
- This shouldn't happen, but if it does, try restarting the dev server
- Check for files with the same name in different directories
$3
- Remember: index.tsx files inherit their parent's path
- Directories without layout.tsx` flatten their children into absolute routes