The routing engine/Filesystem routing

Filesystem routing

The folder convention and the route tree it produces.

The convention

BionicJS routes map directly from the filesystem. The rule is simple:

Route mapping
1app/page.tsx                    → /
2app/about/page.tsx              → /about
3app/dashboard/settings/page.tsx → /dashboard/settings

Layouts wrap every route below them, so nesting folders is nesting layouts:

A small app on disk
1app/
2|-- page.tsx              -> /
3|-- layout.tsx            -> wraps all pages
4|-- about/
5|   `-- page.tsx          -> /about
6`-- blog/
7    |-- layout.tsx        -> wraps /blog/*
8    |-- page.tsx          -> /blog
9    `-- [slug]/
10        `-- page.tsx      -> /blog/:slug

Every route is a file named page.tsx. Its path relative to app/ is the URL. No manual registration, no import list, no router config.

ⓘ
The starter project contains only app/layout.tsx and app/page.tsx. When you create app/about/page.tsx, the route appears automatically. No restart required during development.

Build-time discovery

Filesystem scanning is strictly a build-time and dev-server concern, owned by bionicjsRoutesPlugin. The browser runtime never scans the filesystem or calls import.meta.glob.

Plugin discovery
1const relativeFiles = fg.sync("**/{page,layout}.tsx", { cwd: appDir });
2const manifest = parseRoutes(relativeFiles);
3return generateRouteModule(manifest);

During development, Vite's file watcher monitors app/. When a route file is created or deleted, bionicjsRoutesPlugin invalidates the virtual module graph entry, triggering instant HMR update.

virtual:bionicjs-routes & React Router

BionicJS's generator transforms the manifest tree into a virtual module string, virtual:bionicjs-routes, exported directly to React Router. Each route uses lazy dynamic imports for optimal code splitting.

Generated virtual:bionicjs-routes
1import React from "react";
2import { BionicJSLayoutAdapter } from "@bionicjs/core/router";
3
4export const routes = [
5  {
6    lazy: async () => {
7      const mod = await import("/app/layout.tsx");
8      return {
9        Component: () => React.createElement(BionicJSLayoutAdapter, { Layout: mod.default })
10      };
11    },
12    children: [
13      {
14        index: true,
15        lazy: async () => {
16          const mod = await import("/app/page.tsx");
17          return { Component: mod.default };
18        }
19      },
20      {
21        path: "about",
22        lazy: async () => {
23          const mod = await import("/app/about/page.tsx");
24          return { Component: mod.default };
25        }
26      }
27    ]
28  }
29];

Layouts use standard React { children } syntax. The BionicJSLayoutAdapter injects React Router's <Outlet /> transparently behind the scenes.

The developer mounts <BionicJSRouter /> at their application root. That is the entire router API surface exposed to them. React Router is an internal framework detail.