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
| File | Does |
|---|---|
+page.tsx | The page component. Its default export renders the route. |
+page.server.ts | load (data) and form actions. Never ships to the browser. |
+layout.tsx | Wraps this directory and everything under it. |
+layout.server.ts | load for a layout. Never ships to the browser. |
+error.tsx | Error boundary for this directory and below. |
+server.ts | An 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 → /spellsBoth index and +page name the directory itself, so these two are the same route:
src/routes/spells/index.tsx
src/routes/spells/+page.tsxDynamic segments
Square brackets mark a parameter:
src/routes/books/[id].tsx → /books/:id
src/routes/users/[id]/posts.tsx → /users/:id/postsparams 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 → /dashboardUse 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:
export function match(value: string): boolean {
return ["en", "de", "fr"].includes(value);
}src/routes/[lang=lang]/about.tsx → /:lang/aboutThe 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:
- Static segments
- Required parameters
- Optional parameters
- 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 dataLayouts 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:
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
- Load functions — getting data in
- Form actions — handling POST