Skip to content

File-Based Routing

Pareto uses file-based routing. Every page.tsx inside app/ becomes a route. The file system is the router — no separate route configuration file needed.

Each directory in your app/ folder can contain special convention files that control how that route behaves:

FilePurpose
page.tsxRoute component — renders the page content
layout.tsxWrapping layout (nests from root to page)
loader.tsSeparate loader file for server-side data fetching
head.tsxPer-route <title> and meta tags
not-found.tsx404 page (root level only)
route.tsResource route (JSON API, no HTML)
app/
page.tsx → /
stream/
page.tsx → /stream
blog/
[slug]/
page.tsx → /blog/:slug
api/
users/
route.ts → /api/users (JSON)

Use [param] directory names for dynamic segments:

app/blog/[slug]/page.tsx → /blog/:slug

Access params via the loader context:

export function loader(ctx: LoaderContext) {
const { slug } = ctx.params
return { post: getPost(slug) }
}

Dynamic routes match any value for that segment. The param name inside the brackets becomes the key in ctx.params.

Use [...param] for catch-all segments:

app/docs/[...path]/page.tsx → /docs/*

The path param will be a string containing the rest of the URL. For example, /docs/getting-started/install sets ctx.params.path to getting-started/install.

Layouts at each level wrap their children. A root layout.tsx wraps every page:

app/
layout.tsx ← wraps everything
page.tsx ← / (wrapped by root layout)
dashboard/
layout.tsx ← wraps dashboard pages
page.tsx ← /dashboard (wrapped by both layouts)
settings/
page.tsx ← /dashboard/settings (wrapped by both layouts)

Nested layouts are useful for sections of your app that share a common UI shell. For example, a dashboard section might have a sidebar navigation that only appears on dashboard pages.

You can define a route’s loader in a separate loader.ts file instead of exporting it from page.tsx:

app/dashboard/loader.ts
import type { LoaderContext } from '@paretojs/core'
export function loader(ctx: LoaderContext) {
return { stats: getDashboardStats() }
}

This keeps your data fetching logic separate from your components, which is useful for complex loaders with many imports. If both loader.ts and a loader export in page.tsx exist, loader.ts takes precedence.

If you want to share a layout between routes without adding a URL segment, use parenthesized directory names:

app/
(marketing)/
layout.tsx ← shared layout for marketing pages
page.tsx ← / (no /marketing prefix)
about/
page.tsx ← /about
(dashboard)/
layout.tsx ← shared layout for dashboard pages
overview/
page.tsx ← /overview

Directories wrapped in parentheses () are route groups. They affect layout nesting but do not appear in the URL.

Each route can define its own <title> and meta tags via a head.tsx file. See Head Management for details on how head descriptors merge from root to page.

Use ParetoErrorBoundary in your layouts or pages to catch render errors. You can place error boundaries at any level for fine-grained error isolation.