Reference: Route files

Reference

Route files

Grimoire builds its route table by scanning src/routes. Every file in it is convention-named; the path relative to src/routes is the URL, minus the + prefix on the filename.

The files

FileDoes
+page.tsxThe page component. Its default export renders the route.
+page.server.tsload (data) and form actions. Never ships to the browser.
+layout.tsxWraps this directory and everything under it.
+layout.server.tsload for a layout. Never ships to the browser.
+error.tsxError boundary for this directory and below.
+server.tsAn API route. Replaces the page with a handler.

Extensions can be .tsx, .ts, .jsx or .js.

A route needs a +page.tsx or a +server.ts. +page.server.ts on its own is not a route — a data file with nothing to render. (This is worth knowing because a route made only of +page.server.ts files is silently absent from the build, with no error to explain it.)

Static routes

src/routes/index.tsx            →  /
src/routes/about.tsx            →  /about
src/routes/spells/index.tsx     →  /spells

Both index and +page name the directory itself, so these two are the same route:

src/routes/spells/index.tsx
src/routes/spells/+page.tsx

Dynamic segments

Square brackets mark a parameter:

src/routes/books/[id].tsx          →  /books/:id
src/routes/users/[id]/posts.tsx    →  /users/:id/posts

params in load and actions is a plain object of strings:

export function load({ params }: LoadContext) {
	return { book: getBook(Number(params.id)) };
}

Segments combine with a directory however you like — [id].tsx and [id]/index.tsx produce the same path, so pick one style per project.

Optional parameters

Double brackets make a segment optional, and it must be trailing:

src/routes/docs/[[...slug]].tsx    →  /docs        (no slug)
                                     /docs/state   (slug = "state")

An optional segment matches the bare parent path too, which is how a docs site serves both /docs and /docs/anything from one file. The value is "" when absent, so normalise before using it.

Rest parameters

[...name] captures zero or more trailing segments:

src/routes/files/[...path].tsx     →  /files/a/b/c.png   (path = "a/b/c.png")

Route groups

Parenthesised folders group files without affecting the URL:

src/routes/(marketing)/pricing.tsx   →  /pricing
src/routes/(app)/dashboard.tsx       →  /dashboard

Use them to share a layout between sibling sections, or to keep a folder of components beside the routes that use them.

Validated parameters

[name=matcher] constrains a segment. The matcher is a module in src/params exporting match:

src/params/lang.ts
export function match(value: string): boolean {
	return ["en", "de", "fr"].includes(value);
}
src/routes/[lang=lang]/about.tsx    →  /:lang/about

The route only matches when src/params/lang.ts accepts the value. A request for /de/about works; /es/about falls through to the next matching route, then to 404. Put matcher files in src/params, beside src/routes, not inside it.

Precedence

When several routes could match, the most specific wins:

  1. Static segments
  2. Required parameters
  3. Optional parameters
  4. Rest parameters

So /spells/fire (static) beats /spells/[name], which beats /docs/[[...slug]], which beats /files/[...path]. Two routes that resolve to the same path are a build error, not a silent shadow.

Layouts

A +layout.tsx wraps its own directory and every directory below it, nearest first:

src/routes/+layout.tsx            wraps everything
src/routes/admin/+layout.tsx      wraps /admin and below
src/routes/admin/+layout.server.ts  its data

Layouts nest by composition — each receives the child as children and renders it. A layout's load can read its ancestors' data through parent(); a page reads all of its layouts' data the same way. Both are merged outermost-first, so a nearer layout can override a key from a further one.

Layouts apply to the loading shell too, which is why a streaming page arrives already inside its correct chrome.

Errors

+error.tsx catches errors from the route and everything under it, so you can scope a boundary as tightly as you want. Throwing error(404, "message") in a load lands in the nearest +error.tsx.

Server routes

+server.ts replaces the page with HTTP handlers and never renders HTML:

src/routes/api/search/+server.ts
export function GET({ url }: { url: URL }) {
	return Response.json({ q: url.searchParams.get("q") });
}

Export one function per method — GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD. A method with no export is a 405, not a fallthrough to the page.

Server routes are matched before pages, so a +server.ts and a +page.tsx can share a path without colliding: the server route answers API calls, the page answers the browser.

Next

Edit this page