# Define routes

In this chapter we will learn about the basics of the file-system based router. We will learn how to define routes, how to use parameters and page layouts.

## Configuration

The router can be configured by creating a `react-server.config.mjs` or `react-server.config.json` file in the root of your project. The configuration file should export a configuration object and it should include the `root` path for your routes. You can also specify a `public` path for your static files. The file-system based router will automatically scaffold the files in the root directory to prepare and build the routing based on your files in the root directory.

```js
export default {
  root: "src/pages",
  public: "public",
};
```

## Creating Routes

Your directories and files specify the routes your application will use. There are built-in conventions in the router which translates the names of directories and files to route paths and route parameters. To create a new route you need to create a file in the root directory.

Route files have to export default a React component which will be used to render the route. By default, pages are server components. If a page starts with the `"use client"` directive, it becomes a [client-only route](/router/typed-router#client-only-routes) — navigating to it happens entirely on the client with no server round-trip, and component state is preserved between navigations.

```jsx
export default function Home() {
  return <h1>Home</h1>;
}
```

### Index routes

If you name your file `index` or `page` it will be treated as an index route. Index routes are the default routes for directories. For example if you create a file named `index.jsx` in the root directory it will be treated as the default route for your application. If you create a file named `index.jsx` in a directory named `about` it will be treated as the default route for the `about` directory.

### Named routes

If you name your file anything other than `index` or `page` it will be treated as a named route. For example if you create a file named `about.jsx` in the root directory it will be treated as a named route with the path `/about`. If you create a file named `about.jsx` in a directory named `users` it will be treated as a named route with the path `/users/about`.

### Nested routes

You can create nested routes by creating directories in the root directory. For example if you create a directory named `users` in the root directory and create a file named `index.jsx` in the `users` directory it will be treated as a nested route with the path `/users`. If you create a file named `about.jsx` in the `users` directory it will be treated as a nested route with the path `/users/about`.

### Route parameters

You can create route parameters by adding a parameter name in brackets to the name of the file. For example if you create a file named `[id].jsx` in the root directory it will be treated as a named route with the path `/[id]`. If you create a file named `[id].jsx` in a directory named `users` it will be treated as a named route with the path `/users/[id]`. You can also create nested route parameters by creating nested directories. For example if you create a directory named `users` in the root directory and create a file named `[id].jsx` in the `users` directory it will be treated as a nested route with the path `/users/[id]`. You will be able to access the route parameter in your component as React props.

```jsx
export default function User({ id }) {
  return <h1>User #{id}</h1>;
}
```

You can also use multiple route parameters in a single route segment. For example if you create a file named `[id]-[name].jsx` it will be treated as a route with the path pattern of `/[id]-[name]` and you will receive both `id` and `name` as props in your component.

```jsx
export default function User({ id, name }) {
  return <h1>User #{id} - {name}</h1>;
}
```

> **Matcher alias:** a parameter bracket can carry an `=alias` suffix — e.g. `[id=numeric].page.jsx` → `/[id=numeric]`. The alias binds the segment to a predicate supplied via `export const matchers`, letting you gate the route on the segment's shape before it matches. See [Route matchers](/router/define-routes#route-matchers) below. Plain `[id]` extracts the param without filtering.

### Route parameters for multiple segments

You can create route parameters for multiple segments by adding a parameter name in brackets to the name of the file. For example if you create a file named `[...id].jsx` in the root directory it will be treated as a named route with the path `/[...id]`. If you create a file named `[...id].jsx` in a directory named `users` it will be treated as a named route with the path `/users/[...id]`. You can also create nested route parameters by creating nested directories. For example if you create a directory named `users` in the root directory and create a file named `[...id].jsx` in the `users` directory it will be treated as a nested route with the path `/users/[...id]`.

The parameter at runtime will be an array of strings. You will be able to access the route parameter in your component as a React prop.

```jsx
// /[...slug].page.jsx
export default function Page({ slug }) {
  return <h1>/{slug.join("/")}</h1>;
}
```

### Route parameters for optional segments

You can create route parameters for optional segments by adding a parameter name in brackets to the name of the file. For example if you create a file named `[[...id]].jsx` in the root directory it will be treated as a named route with the path `/[[...id]]`. If you create a file named `[[...id]].jsx` in a directory named `users` it will be treated as a named route with the path `/users/[[...id]]`. You can also create nested route parameters by creating nested directories. For example if you create a directory named `users` in the root directory and create a file named `[[...id]].jsx` in the `users` directory it will be treated as a nested route with the path `/users/[[...id]]`.

> **Omit:** you can omit any part of the directory or file name by wrapping the part in parentheses. For example if you create a file named `(404).[[...slug]].page.mdx` in the root directory it will be treated as a route with the path `/[[...slug]]`. You can use this to extend the directory/file name with additional information without affecting the route path.

## Layouts

You can create layouts by creating a file including `layout.jsx` in the file name. The layout file will be used to wrap all the routes in the same directory where the layout file is. You can also create nested layouts by creating a file named `layout.jsx` in a sub-directory. You can also use omitted parts in the layout file name. For example if you create a file named `(root).layout.jsx` in the root directory it will be used as the layout for all the routes in the root directory.

Your layout component will receive a `children` prop which you need to use to render your route components.

```jsx
export default function Layout({ children }) {
  return (
    <>
      <h1>Layout</h1>
      {children}
    </>
  );
}
```

## Transparent route segments

Transparent segments are segments that are not rendered in the URL but are used to identify your file for yourself. You can create transparent segments by creating a file named `(transparent).page.jsx` where `(transparent)` is the name of your transparent segment and could be anything you want. For example if you create a file named `(main).page.jsx` in the root directory it will be treated as a route with the path `/`. If you create a file named `(main).page.jsx` in a directory named `users` it will be treated as a route with the path `/users`. You can also use transparent segments in your directory structure to group your files. For example if you create a file named `page.jsx` in a directory named `(dashboard)/users` it will be treated as a route with the path `/users`.

```txt
src
- (root).layout.jsx
- (root).page.jsx
- (dashboard)
  - users
    - (users).page.jsx
    - [userId].page.jsx
```

## Escape route segments

You can escape route segments by wrapping the segment in curly braces. For example if you create a file named `{sitemap.xml}.server.mjs` in the root directory it will be treated as a named route with the path `/sitemap.xml`.

> **Example:** see the Photos example in the [examples](https://github.com/lazarv/react-server/tree/main/examples/photos) directory for a basic example of file-system based routing.

## Route validation

In the file-system based router, you can validate route params and search params at runtime by exporting a `validate` object from your page file. The runtime picks up this export automatically — no extra configuration needed.

The `validate` export is an object with optional `params` and `search` keys, each set to a schema from any compatible library (Zod, ArkType, Valibot, or anything satisfying the [`ValidateSchema`](/router/typed-router#validation) interface).

```tsx filename="pages/user/[id].page.tsx"
import { z } from "zod";
import { user } from "@lazarv/react-server/routes";

export const validate = {
  params: z.object({
    id: z.string().regex(/^\d+$/, "ID must be numeric"),
  }),
};

export default user.createPage(({ id }) => {
  // id is typed as string and guaranteed to match /^\d+$/
  return <h1>User {id}</h1>;
});
```

When validation is present, the generated TypeScript types infer param and search types from the schema — so `useParams()`, `.Link`, and `createPage` all receive the validated types instead of raw strings.

**With ArkType:**

```tsx filename="pages/user/[id].page.tsx"
import { type } from "arktype";
import { user } from "@lazarv/react-server/routes";

export const validate = {
  params: type({ id: /^\d+$/ }),
};

export default user.createPage(({ id }) => {
  return <h1>User {id}</h1>;
});
```

**Validating search params:**

```tsx filename="pages/products.page.tsx"
import { z } from "zod";
import { products } from "@lazarv/react-server/routes";

export const validate = {
  search: z.object({
    sort: z.enum(["name", "price", "rating"]).catch("name"),
    page: z.coerce.number().int().positive().catch(1),
  }),
};

export default products.createPage(() => {
  const { sort, page } = products.useSearchParams();
  return <div>Sorted by {sort}, page {page}</div>;
});
```

When validation fails (e.g. `/user/abc` with the numeric validation above), `useParams()` returns `null` and `useSearchParams()` returns an empty object `{}`, letting you handle the error in your component or fall through to a fallback route.

## Route matchers

Matchers are predicates that gate **whether** a route matches a URL in the first place. They run during routing, before any page code loads, and they let sibling routes divide the same parameter space by shape.

Use the matcher syntax by adding an `=alias` suffix inside a parameter bracket in the file name. The alias is the key you'll use in `export const matchers`:

| Bracket form | Example | Matcher receives |
|--------------|---------|------------------|
| `[name=alias]` | `[id=numeric].page.tsx` | `string` |
| `[[name=alias]]` | `[[tab=known]].page.tsx` | `string` |
| `[...name=alias]` | `[...slug=nested].page.tsx` | `string[]` |
| `[[...name=alias]]` | `[[...path=valid]].page.tsx` | `string[]` |

Export a `matchers` object from the page (or middleware or API route) whose keys are the aliases used in the path. Each value is a predicate that returns `true` to accept the URL and `false` to reject it. Rejected URLs fall through to sibling routes — they're not 404s by themselves.

```tsx filename="pages/product/[sku=uppercase].page.tsx"
import { productSkuUppercase } from "@lazarv/react-server/routes";

export const matchers = productSkuUppercase.createMatchers({
  uppercase: (value) => /^[A-Z0-9-]+$/.test(value),
});

export default productSkuUppercase.createPage(({ sku }) => {
  return <h1>Product {sku}</h1>;
});
```

Place a sibling `[sku].page.tsx` next to it to catch rejected values:

```tsx filename="pages/product/[sku].page.tsx"
import { product } from "@lazarv/react-server/routes";

export default product.createPage(({ sku }) => {
  return <h1>Fallback: {sku}</h1>;
});
```

The generated route names (`productSkuUppercase` and `product`) come directly from the derivation rule described in [Route naming](/router/define-routes#routes-module-naming): a matcher-gated segment contributes both its param name and its alias, while a bare dynamic segment is stripped. No explicit `route` override needed.

Now `/product/ABC-123` renders the matcher-gated page, and `/product/abc-123` falls through to the sibling. The runtime tries matcher-gated routes first — more specific routes always rank above less specific siblings at the same path shape.

**Catch-all matchers receive arrays.** The alias suffix works identically on `[...slug]` and `[[...slug]]`, but the predicate gets the whole segment array, not a string:

```tsx filename="pages/docs/[...slug=nested].page.tsx"
import { docsSlugNested } from "@lazarv/react-server/routes";

export const matchers = docsSlugNested.createMatchers({
  nested: (slug) => slug.length >= 2,
});

export default docsSlugNested.createPage(({ slug }) => {
  return <article>{slug.join("/")}</article>;
});
```

Here `/docs/intro` falls through (length 1) but `/docs/guide/install` matches.

**Typed aliases.** The aliases you can pass to `createMatchers` are extracted from the route path at type-generation time. A path like `[id=numeric]` produces `{ numeric?: (value: string) => boolean }`; a path like `[...slug=nested]` produces `{ nested?: (value: string[]) => boolean }`. Using an alias that isn't in the path is a type error.

**Matchers vs validation.** They solve different problems:

- **Matchers** gate **routing**. A rejection means "this URL is not for this route" — sibling routes and fallbacks get a chance. No page code loads.
- **Validation** gates **the matched route's params**. A rejection means "this route matched, but the params don't satisfy the schema" — `useParams()` returns `null`, the page still renders and must handle the error.

Use matchers when several sibling routes share the same parameter slot but differ in its shape. Use validation when a single route needs to constrain the already-extracted params before rendering.

**Scope.** Matchers are honored on pages, outlet pages, middlewares, and API route handlers. Layouts and error/loading/fallback boundaries don't participate in matcher dispatch — they're selected by containment and by the page they wrap.

## Routes module

When you use the file-system based router, the runtime automatically generates a virtual `@lazarv/react-server/routes` module. This module exports a named route descriptor for every route in your application — derived from your file structure, updated in real time during development, and regenerated on each build.

```tsx
import { index, about, user, dashboard } from "@lazarv/react-server/routes";
```

Each exported descriptor is a full [`RouteDescriptor`](/router/typed-router#api-route-descriptor) with all the same capabilities as one created by `createRoute`:

- **`.Link`** — typed Link component with compile-time param checking
- **`.href(params?)`** — build a URL pathname from typed params
- **`.useParams()`** — read typed, validated params (works in server and client components)
- **`.useSearchParams()`** — read typed, validated search params
- **`.SearchParams`** — route-scoped search param transform boundary

Plus context-aware helper functions for typing your page components:

- **`.createPage(component)`** — typed page component with validated params as props
- **`.createLayout(component)`** — typed layout with `children` and branded outlet props
- **`.createLoading(component)`** — typed loading fallback component
- **`.createError(component)`** — typed error boundary component
- **`.createFallback(component)`** — typed fallback component
- **`.createMiddleware(handler)`** — typed middleware with request params
- **`.createMatchers(matchers)`** — typed matcher map (only on routes whose path contains an `=alias` bracket). Aliases are extracted from the path; catch-all aliases receive `string[]`, single aliases receive `string`. See [Route matchers](/router/define-routes#route-matchers).

These helpers are identity functions at runtime — they return the component unchanged. They exist purely for TypeScript to infer the correct prop types from your route path, `validate` export, and outlet structure.

A companion virtual module — `@lazarv/react-server/outlets` — is generated alongside `@lazarv/react-server/routes` and exposes a typed, server-preloading bound `ReactServerComponent` for every outlet in your file-router. See [typed outlet components](#routes-module-bound-outlets) below.

### Route naming

Route names are derived automatically from the file path using camelCase:

| File path | Exported name |
|-----------|--------------|
| `pages/page.tsx` or `pages/index.tsx` | `index` |
| `pages/about.tsx` | `about` |
| `pages/user/[id].page.tsx` | `user` |
| `pages/user/posts.page.tsx` | `userPosts` |
| `pages/dashboard/settings.page.tsx` | `dashboardSettings` |
| `pages/product/[sku=uppercase].page.tsx` | `productSkuUppercase` |
| `pages/docs/[...slug=nested].page.tsx` | `docsSlugNested` |

Dynamic segments like `[id]` are stripped from the name. Purely dynamic paths like `pages/[id].page.tsx` use the parameter name (`id`) as the route name. When a segment carries a matcher alias (e.g. `[sku=uppercase]`), both the param name and the alias contribute to the derived name — so a matcher-gated page and its bare sibling get distinct names automatically, without numeric suffixes. Numeric suffixes are still applied as a last-resort fallback only when the derivation produces a true collision.

You can override the auto-derived name by exporting a `route` constant from your page file:

```tsx
export const route = "userProfile";
```

The exported name then becomes `userProfile` instead of the auto-derived name:

```tsx
import { userProfile } from "@lazarv/react-server/routes";
```

### Typed pages

Use `createPage` to get typed props inferred from the route's path params and `validate` export:

```tsx filename="pages/user/[id].page.tsx"
import { z } from "zod";
import { user } from "@lazarv/react-server/routes";

export const route = "user";

export const validate = {
  params: z.object({ id: z.string().regex(/^\d+$/, "ID must be numeric") }),
};

// TypeScript infers: ({ id: string }) => ReactNode
export default user.createPage(({ id }) => {
  return <h1>User {id}</h1>;
});
```

Without `validate`, the param types come from the path pattern — `[id]` gives you `{ id: string }`, `[...slug]` gives you `{ slug: string[] }`. With `validate`, types are inferred from the schema's output type, which can be narrower (e.g. a regex-constrained string, a number after coercion).

For pages without dynamic params, `createPage` receives an empty props object:

```tsx filename="pages/about.tsx"
import { about } from "@lazarv/react-server/routes";

export default about.createPage(() => {
  return <h1>About</h1>;
});
```

### Typed layouts and outlets

Use `createLayout` to get typed `children` and outlet props. Outlets are created by placing files in `@outletName` directories under a layout's directory:

```
pages/
├── dashboard/
│   ├── (dashboard).layout.tsx
│   ├── index.page.tsx
│   ├── @sidebar/
│   │   └── nav.page.tsx
│   └── @content/
│       ├── @content.default.tsx    # default content when no route matches
│       └── feed.page.tsx
```

The layout receives typed outlet props — each outlet is a branded `React.ReactElement` that TypeScript tracks separately to prevent mixing:

```tsx filename="pages/dashboard/(dashboard).layout.tsx"
import { dashboard } from "@lazarv/react-server/routes";

export default dashboard.createLayout(({ children, sidebar, content }) => {
  return (
    <div>
      <h1>Dashboard</h1>
      <div style={{ display: "grid", gridTemplateColumns: "200px 1fr" }}>
        <aside>{sidebar}</aside>
        <main>
          <div>{content}</div>
          <div>{children}</div>
        </main>
      </div>
    </div>
  );
});
```

**Nullable outlets:** If an outlet directory has a `@outletName.default.tsx` file, the outlet is always rendered (non-nullable). If there's no default, the outlet is `ReactElement | null` — TypeScript enforces null checks.

In the example above, `content` has a default (`@content.default.tsx`) so it's always a `ReactElement`. If `sidebar` had no default, it would be typed as `ReactElement | null`.

### Typed outlet components

The previous section showed the *receiving* side — a layout consuming outlet slots that are populated by the file-router from neighbouring `@outletName/` directories. The *producing* side has its own typed entry point: a virtual `@lazarv/react-server/outlets` module that exports one namespace per outlet declared in the file-router.

```tsx
import { sidebar, content } from "@lazarv/react-server/outlets";

<sidebar.Outlet url="/dashboard/nav" />
<content.Outlet url="/dashboard/feed" />
```

Each namespace exposes a single `Outlet` component (PascalCase, JSX-callable) bound to its outlet name. Compared to the underlying `` form, this gives you three things:

- **No stringly-typed outlet name.** The outlet identifier is bound at the import site; you can't typo it, and unknown outlet names fail at compile time.
- **Typed `url`.** The `url` prop accepts the same union as `Link.to` — every static and dynamic route in the file-router. Typos are rejected, and dynamic segments must already be concrete (e.g. `/user/42`, not `/user/[id]`).
- **Branded return type.** The component returns `Outlet<"sidebar">` (or whichever name), so it satisfies a `createLayout` slot of the same name without casts.

By default the outlet preloads on the server: when the bound component is rendered in a server component, the runtime resolves the `url` against the file-router manifest, finds the matching `@outletName/...page.tsx` (or the `@outletName.default.tsx` fallback), renders it, and passes the result as `children` to `ReactServerComponent`. The SSR HTML therefore contains the outlet content on first paint — no client round-trip.

Two opt-outs:

- **`defer={true}`** — skip preload and let the client fetch the URL after hydration. Useful when the outlet content depends on browser-only state, or when you want to avoid blocking the parent page's render on the outlet resolution.
- **Explicit `children`** — when you pass your own children, preload is bypassed entirely and your tree is used as-is.

```tsx filename="pages/panels.page.tsx"
import { sidebar, content } from "@lazarv/react-server/outlets";

export default function Panels() {
  return (
    <div style={{ display: "grid", gridTemplateColumns: "200px 1fr" }}>
      <aside>
        {/* Server-resolves /dashboard/nav into the sidebar slot */}
        <sidebar.Outlet url="/dashboard/nav" />
      </aside>
      <main>
        {/* `defer` opts into client-side fetch after hydration */}
        <content.Outlet url="/dashboard/feed" defer />
      </main>
    </div>
  );
}
```

The runtime writes a `react-server-outlets.d.ts` file alongside `react-server-routes.d.ts` in the `.react-server` directory. It declares the `@lazarv/react-server/outlets` module with one namespace per outlet name discovered in your manifest. Outlet names that aren't valid JavaScript identifiers (e.g. hyphenated directory names) are skipped from this module — those outlets remain reachable via the stringly-typed `` form.

### Typed loading and error components

Loading and error components follow the same pattern:

```tsx filename="pages/user/[id].loading.tsx"
import { user } from "@lazarv/react-server/routes";

export default user.createLoading(() => {
  return <div>Loading user profile...</div>;
});
```

```tsx filename="pages/user/[id].error.tsx"
import { user } from "@lazarv/react-server/routes";

export default user.createError(({ error }) => {
  return <div>Error loading user: {error.message}</div>;
});
```

### Using Link and href

The route descriptors from the routes module work just like `createRoute` descriptors — use `.Link` for navigation and `.href()` for URL building:

```tsx filename="pages/(root).layout.tsx"
import { index, about, user, dashboard } from "@lazarv/react-server/routes";

export default index.createLayout(({ children }) => {
  return (
    <html lang="en">
      <body>
        <nav>
          <index.Link>Home</index.Link>
          <about.Link>About</about.Link>
          <user.Link params={{ id: "42" }}>User 42</user.Link>
          <dashboard.Link>Dashboard</dashboard.Link>
        </nav>
        {children}
      </body>
    </html>
  );
});
```

Routes without dynamic segments (like `index`, `about`) don't accept `params`. Routes with dynamic segments (like `user`) require them — TypeScript enforces this at compile time.

### Generated types

The runtime writes a `react-server-routes.d.ts` file into the `.react-server` directory. This file declares the `@lazarv/react-server/routes` module with a per-route interface:

```ts
// Auto-generated — do not edit manually
declare module "@lazarv/react-server/routes" {
  import type { RouteDescriptor, ValidateSchema } from "@lazarv/react-server/router";

  // ── /user/[id] ──
  interface UserRoute extends RouteDescriptor<"/user/[id]",
    typeof import("../pages/user/[id].page").validate extends
      { params: ValidateSchema<infer T> } ? T : { id: string },
    Record<string, string>
  > {
    createPage(
      component: (props: /* inferred params type */) => React.ReactNode
    ): typeof component;
    createLoading(
      component: () => React.ReactNode
    ): typeof component;
  }
  export const user: UserRoute;
}
```

The generated types include only the helpers that exist for each route — if a route has a page file, it gets `createPage`; if it has a layout, it gets `createLayout` with the correct outlet props; and so on. This file is regenerated automatically during development and builds, so it always matches your file structure.

> **Note:** check out the [typed-file-router](https://github.com/lazarv/react-server/tree/main/examples/typed-file-router) example for a complete working application using the routes module with validation, layouts, outlets, loading states, and client-only routes.

> For the programmatic typed routing API (`createRoute`, `createRouter`, schema validation strategies, parse functions, and more), see the [typed router](/router/typed-router#create-route) page.

## Typed routes

When you use the file-system based router with TypeScript, the runtime automatically generates type definitions from your file structure. These types cover:

- All static and dynamic route paths for `Link`, `navigate`, `replace`, and `prefetch`
- Route parameter types extracted from dynamic segments like `[id]` and `[...slug]`
- Available outlet names for `ReactServerComponent`

To enable type checking, add the generated types to your TypeScript configuration:

```json filename="tsconfig.json"
{
  "compilerOptions": {
    // ...
  },
  "include": [/* ... */, ".react-server/**/*.ts"]
}
```

That's it. The runtime regenerates types automatically during development whenever your file structure changes, and during builds. Your IDE will immediately show autocompletion for all route paths and flag invalid routes at compile time.

For example, given this file structure:

```
app/
├── page.tsx          # /
├── about.tsx         # /about
├── user/
│   ├── [id].tsx      # /user/:id
│   └── page.tsx      # /user
└── posts/
    └── [...slug].tsx # /posts/*
```

The `Link` component and `navigate` function will autocomplete `/`, `/about`, `/user`, and `/posts` as static routes, and provide typed params for `/user/[id]` and `/posts/[...slug]`.

For the programmatic typed routing API (`createRoute`, `createRouter`, typed hooks, schema validation, and more), see [typed router](/router/typed-router#create-route).