Application model
Rooted apps are organised as vertical slices. Each feature is one folder that owns its routes, its components, its styles, and any data it needs. There is no top-level routes/, components/, or styles/ tree.
This page explains the layout and the small set of conventions that hold it together.
A slice
A slice is a folder that represents one user-facing feature. The folder contains:
- A
_routes.mtsfile that declares the routes the slice owns. - The components those routes render.
- The CSS for those components.
- Anything else local to the feature (icons, helpers, types).
The example app's recipes slice looks like this:
src/recipes/
_routes.mts
recipe.mts
recipe.css
ingredients-list.mts
ingredients-list.css
measurements.mts
recipe-tabs.mts
recipe-tabs.css
serving-stepper.mts
serving-stepper.css
screen-lock-button.mts
screen-lock-button.css_routes.mts is the only file with a special name. The rest is whatever makes sense for the feature.
Why slices
Two reasons:
- When you delete a feature, you delete one folder.
- When you change a feature, the things you need are next to each other.
There is no enforced structure inside a slice. If a slice grows enough that it wants its own data/ or _shared/ subfolder, do that. Rooted does not have an opinion.
The _routes.mts convention
Every slice that has routes exposes them from _routes.mts. The Vite plugin scans for these files at build time and writes a generated module that imports every route at once.
A typical _routes.mts:
import { route, token } from '@rooted/router/routes'
export const RecipeRoute = route`/recipe/${token('id', Number)}/`({
async resolve({ create, tokens }) {
const { Recipe } = await import('./recipe.mts')
return create(Recipe, {
id: tokens.id
})
},
})A few things worth knowing:
- The
await import('./recipe.mts')makes the component lazy. The component module is not loaded until the route is visited for the first time. - The route is just a value. You can re-export it from the slice and reference it in other slices to build links (
href.for(RecipeRoute, { id })). - A route can declare
seofor per-page metadata. See SEO.
The route manifest
Add the plugin to vite.config.mts:
import { generateRouteManifest } from '@rooted/router/manifest'
export default {
plugins: [
generateRouteManifest({
glob: './src/**/_routes.mts',
routeManifestPath: './src/_routes.g.mts',
}),
],
}The plugin writes src/_routes.g.mts with one big appRoutes export. Treat the generated file as build output. Don't edit it, and add it to your .gitignore if you don't want it in source control.
Because it's build output, it doesn't exist yet on a fresh checkout, which upsets TypeScript (and sometimes the TS server). Reference the ambient types once and the import resolves either way:
// src/vite-env.d.ts
/// <reference types="@rooted/router/types" />You then pass appRoutes to the router(...):
import { router } from '@rooted/router/application'
import { appRoutes } from './_routes.g.mts'
const Router = router({
home: HomePage,
notFound: NotFoundPage,
...appRoutes,
})The app shell
The shell is whatever wraps the router. In the example app it's a single component called Application that holds:
- The page header (with the navigation menu).
- The
<main>element that the router renders into. - The footer.
const Application = component({
name: 'recipe-application',
styles,
onMount({ append, element, create }) {
append(
element('div', {
id: 'app',
children: [
element('header', {
children: create(NavigationMenu)
}),
element('main', {
id: 'main-content',
children: create(Router)
}),
element('footer', {
children: create(Doormat)
}),
],
}),
)
},
})Rooted does not generate shells, layouts, or wrappers for you. The shell is just another component. If you want nested layouts, write them as components and use gates to mount them on the right URLs.
Slice-shared code
Things that don't belong to any one slice usually go in a _shared/ folder at the source root. The leading underscore keeps it next to the slices alphabetically and signals that it isn't a feature.
src/
_layout/ # global header/footer pieces
_shared/ # data loaders, accessibility helpers, content utilities
_routes.g.mts # generated by the plugin
application.mts # the shell + bootstrap
recipes/
categories/
search/
navigation/This is a convention, not a requirement. The runtime does not look at folder names.
What rooted does not give you
- A scaffolder. Make folders and files yourself.
- File-based routing. Routes are declared in code, in
_routes.mts. - A layout system. Use components and gates.
- A data layer. Use
fetchor whatever data tool you prefer. Pass results to components through the mount context'soptions.
The framework's job stops at the component model, the router, the store, and the SEO tooling. Everything above that is yours.