Skip to content
Frontend Routing

On the frontend, a route is a page component, optionally wrapped in layouts inherited from the folders above it.

You never register any of it. The dev server watches pages/ dir, and as pages are created, renamed or deleted, a matching route configuration is written into lib/ for your framework's router to consume.

This page covers the frontend specifics. The shared fundamentals are in Directory-Based Routing, and the parameter syntax is in Parameters. For how API routes are resolved, see Backend Routing.

One folder, one page ​

txt
pages/
├── index/
│   └── index.tsx             ->  /
├── users/
│   ├── index.tsx             ->  /users
│   ├── types.ts              ->  helper, not a route
│   └── [id]/
│       └── index.tsx         ->  /users/:id
├── dashboard/
│   ├── layout.tsx            ->  wraps /dashboard and everything below
│   ├── index.tsx             ->  /dashboard
│   └── settings/
│       ├── layout.tsx        ->  wraps /dashboard/settings and below
│       └── index.tsx         ->  /dashboard/settings
└── docs/
    └── {...path}/
        └── index.tsx         ->  /docs/*

Only two filenames carry meaning inside pages/:

FileRole
index.<ext>Defines the page. Its folder path is the URL.
layout.<ext>Wraps this folder's pages and everything beneath it.

Everything else in a route folder - components, styles, types, tests - is yours. It is never scanned and never mistaken for a page.

The extension depends on the framework the source folder runs.

ReactSolidVueSvelteMDX
Pageindex.tsxindex.tsxindex.vueindex.svelteindex.mdx / index.md
Layoutlayout.tsxlayout.tsxlayout.vuelayout.sveltelayout.mdx

A folder ignores other frameworks' files: a Vue folder skips .tsx, a React folder skips .vue and .svelte. Full support matrix ›

The URL prefix ​

The folder path is only half of the URL. The other half is frontend.base from the source folder's config:

txt
page URL = join(frontend.base, pagePath)
kosmo.config.ts
ts
export default defineConfig({
  frontend: {
    stack: "react",
    base: "/admin",
  },
});

frontend.base is a full path, independent of backend.base. When several source folders share one server, requests are dispatched by prefix, longest first - a folder at base: "/" catches only what no other folder claims.

Pages are components ​

A page default-exports a component. Write it as a named function:

pages/users/index.tsx
tsx
export default function UserPage() {
  return <h1>User</h1>;
}

An anonymous arrow (export default () => ...) may break Vite's HMR.

You rarely write the file from scratch: create it empty and the dev server seeds the correct boilerplate. To control what gets seeded, use custom templates.

Parameters ​

The three syntaxes - required [id], optional {id}, splat {...path} - are the same as on the backend:

SyntaxExampleMatches
[id]pages/users/[id]//users/123
{id}pages/users/{id}//users and /users/123
{...path}pages/docs/{...path}//docs/any/depth

What differs is how a component reads them - and that belongs to the framework's router.

Read params with
Reactreact-router hooks
SolidJS@solidjs/router
Vuevue-router
Svelte, MDXuseParams() from _/use

_/use does not exist everywhere

The _/use module exists only in Vue, Svelte and MDX folders. In React and SolidJS the import does not resolve at all - use the router's own hooks. Details ›

Matching rules ​

  • Static beats dynamic. users/me wins over users/[id] for /users/me.
  • Optional parameters must not be followed by required ones.users/{section}/{subsection} is fine; users/{optional}/[required] is not.
  • An optional segment before a static one can swallow it. With properties/{city}/filters, /properties/filters binds {city} to "filters" and 404s. Add an explicit static sibling (properties/filters/index.tsx) and static priority resolves it.
  • A sibling index makes [id] effectively optional. With both careers/index.tsx and careers/[jobId]/index.tsx, there is already a fallback for /careers, so {jobId} and [jobId] behave the same.

Details ›

Keep frontend routes simple ​

Mixed segments (files/[name].[ext]) and power syntax are backend-friendly, but frontend routers differ:

ReactSolidJSVueSvelteMDX
Mixed segments⚠️ .ext suffix only❌✅✅✅
Power syntax❌❌❌❌❌

The practical rule: stay with plain syntaxes on pages, and use mixed segments on the API side.

Support matrix ›

Layouts ​

Layouts wrap groups of pages with shared UI - navigation, sidebars, auth shells - at any level of the hierarchy. A request to /dashboard/settings renders, outermost first:

txt
pages/layout.tsx                       (if present)
  pages/dashboard/layout.tsx
    pages/dashboard/settings/layout.tsx
      pages/dashboard/settings/index.tsx

Layouts stack outward-in and cannot be escaped by child routes. Moving a folder moves the layouts it inherits with it - the structure is the wiring.

A layout renders its children with whatever the framework uses for it:

ReactSolidVueSvelteMDX
Renders children with<Outlet/>props.children<RouterView/>{@render children()}props.children

More on layouts ›

Lazy loading ​

All page components are lazy-loaded by default. Route code stays out of the initial bundle and is fetched when a user navigates to that path, so users download only the routes they visit. There is nothing to configure.

Data loading on navigation ​

Every framework ties data fetching to the route lifecycle through a page-level export, so data is requested on navigation intent rather than after the component mounts:

ExportRead it withNotes
ReactloaderuseLoaderData (react-router)Runs on initial load, link hover and navigation start
SolidpreloadcreateAsyncWrap the fetch in query() so both share one cache key; needs <Suspense>
VueloaderuseLoaderData() from _/useRuns in a navigation guard, before render
SvelteloaderuseLoaderData() from _/useExported from the module <script> block
MDXloaderuseLoaderData() from _/useRuns before render

Loader results are serialized during SSR and reused on hydration, so a request made on the server is not repeated on the client. Details ›

Plain anchors work, but the typed Link component knows your routes. It takes the route name, then params in path order, plus an optional query:

tsx
<Link to={["users/[id]", 123]} query={{ tab: "posts" }}>Profile</Link>

TypeScript enforces the route name and param types, so renaming a route folder surfaces an error at every stale Link - the same refactor-as-a-checklist property the backend route name gives you.

Details ›

Routes are derived, and native ​

There is no central route tree to register or maintain - no routeTree.gen.ts, no route config object to keep in sync. Definitions are written into lib/<folder>/, and you reach them only through createRoutes() in your entry file, which the seeded boilerplate already wires up.

  • React, Solid, Vue - a plain, framework-native route definition, the same object you would have written by hand.
  • Svelte, MDX - there is no third-party router, so KosmoJS supplies the matcher and emits its own RawRoute shape.

Because the output is native, everything your router documents keeps working: lazy loading, nested layouts, navigation guards, loader and preload, error elements. path-to-regexp runs only at build time; at runtime the routes are registered with the framework's own router.

Details ›

Quick reference ​

You wantYou write
A page at /orderspages/orders/index.tsx (extension per framework)
The root page /pages/index/index.tsx
A required / optional / catch-all segment[id] / {id} / {...path}
Shared UI for a sectionA layout.<ext> in that section's folder
Data before renderA loader (preload in Solid) export on the page
A typed link<Link to={["users/[id]", 123]} />
A helper next to a pageAny file other than index.<ext> / layout.<ext>

Released under the MIT License.