SvelteKit Routing Explained: Pages, Layouts, and Route Groups
Introduction
Routing is one of SvelteKit's most powerful features. Instead of configuring routes in a separate file, SvelteKit uses the filesystem itself as the router. Every folder inside src/routes/ becomes a URL path, and special files like +page.svelte and +layout.svelte control what gets rendered.
In this tutorial, we will dive deep into SvelteKit's routing system, covering everything from basic pages to advanced patterns like route groups and dynamic parameters.
How File-Based Routing Works
SvelteKit maps your directory structure directly to URLs:
src/routes/
├── +page.svelte → /
├── about/
│ └── +page.svelte → /about
├── blog/
│ ├── +page.svelte → /blog
│ └── [slug]/
│ └── +page.svelte → /blog/:slug
└── settings/
├── +page.svelte → /settings
└── profile/
└── +page.svelte → /settings/profile
Each directory can contain several special files that SvelteKit recognizes:
| File | Purpose |
|---|---|
+page.svelte |
The page component |
+page.ts |
Universal load function |
+page.server.ts |
Server-only load function |
+layout.svelte |
Layout wrapper |
+layout.ts |
Layout load function |
+layout.server.ts |
Server-only layout load |
+error.svelte |
Error boundary |
+server.ts |
API endpoint |
Basic Pages
Every page in your app is defined by a +page.svelte file. The simplest possible page:
<h1>Hello World</h1>
That is it. No boilerplate, no configuration. Place this file at src/routes/+page.svelte and it renders at /.
Adding Metadata with svelte:head
Each page can define its own <head> content:
<svelte:head>
<title>My Page Title</title>
<meta name="description" content="A description for search engines" />
</svelte:head>
<h1>My Page</h1>
Dynamic Routes
Dynamic routes let you match URL segments that vary. Wrap a folder name in square brackets to create a parameter:
src/routes/blog/[slug]/+page.svelte
This matches /blog/hello-world, /blog/my-post, and any other path under /blog/.
Accessing Route Parameters
To access the dynamic parameter, use a load function. Create src/routes/blog/[slug]/+page.ts:
import type { PageLoad } from './$types';
export const load: PageLoad = ({ params }) => {
return {
slug: params.slug
};
};
Then use the data in your page component at src/routes/blog/[slug]/+page.svelte:
<script lang="ts">
let { data } = $props();
</script>
<h1>Blog Post: {data.slug}</h1>
Multiple Parameters
You can have multiple dynamic segments:
src/routes/blog/[category]/[slug]/+page.svelte
This matches /blog/tech/my-post where params.category is "tech" and params.slug is "my-post".
Rest Parameters
Use [...rest] to match any number of path segments:
src/routes/docs/[...path]/+page.svelte
This matches /docs/getting-started, /docs/api/reference/auth, and any depth of nesting. The params.path will contain the full remaining path as a string like "api/reference/auth".
Optional Parameters
Wrap a parameter in double brackets to make it optional:
src/routes/lang/[[locale]]/+page.svelte
This matches both /lang (where params.locale is undefined) and /lang/en (where params.locale is "en").
Layouts
Layouts wrap pages with shared UI. A +layout.svelte file applies to every page in its directory and all subdirectories.
Root Layout
Create src/routes/+layout.svelte to define a layout for your entire app:
<script lang="ts">
import type { Snippet } from 'svelte';
let { children }: { children: Snippet } = $props();
</script>
<header>
<nav>
<a href="/">Home</a>
<a href="/blog">Blog</a>
<a href="/settings">Settings</a>
</nav>
</header>
<main>
{@render children()}
</main>
<footer>
<p>© 2026 My App</p>
</footer>
The children snippet is where the page content (or nested layout content) gets rendered.
Nested Layouts
Layouts nest automatically. Consider this structure:
src/routes/
├── +layout.svelte → Root layout (header + footer)
└── settings/
├── +layout.svelte → Settings layout (sidebar)
├── +page.svelte → /settings
└── profile/
└── +page.svelte → /settings/profile
The settings layout at src/routes/settings/+layout.svelte:
<script lang="ts">
import type { Snippet } from 'svelte';
let { children }: { children: Snippet } = $props();
</script>
<div class="settings-layout">
<aside>
<nav>
<a href="/settings">General</a>
<a href="/settings/profile">Profile</a>
<a href="/settings/billing">Billing</a>
</nav>
</aside>
<section>
{@render children()}
</section>
</div>
<style>
.settings-layout {
display: grid;
grid-template-columns: 200px 1fr;
gap: 2rem;
}
</style>
Pages under /settings/ will be wrapped by both the root layout AND the settings layout. The nesting happens automatically.
Route Groups
Route groups let you organize routes without affecting the URL. Wrap a folder name in parentheses:
src/routes/
├── (marketing)/
│ ├── +layout.svelte → Marketing layout
│ ├── +page.svelte → / (home page)
│ ├── about/
│ │ └── +page.svelte → /about
│ └── pricing/
│ └── +page.svelte → /pricing
└── (app)/
├── +layout.svelte → App layout (with sidebar)
├── dashboard/
│ └── +page.svelte → /dashboard
└── projects/
└── +page.svelte → /projects
Notice that (marketing) and (app) do not appear in the URLs. They only affect which layout wraps which pages.
Why Use Route Groups?
Route groups solve a common problem: what if different sections of your site need completely different layouts? Without groups, every page would inherit the root layout. With groups, you can have:
- A clean marketing layout with a hero header for public pages
- A dashboard layout with a sidebar for authenticated pages
- An auth layout with centered card for login and signup pages
Breaking Out of a Layout
Sometimes you want a page to skip its parent layout. Use +page@.svelte to reset to the root layout, or +page@(group).svelte to reset to a specific group layout:
src/routes/(app)/dashboard/+page@(app).svelte → Uses (app) layout only
src/routes/(app)/fullscreen/+page@.svelte → Uses root layout only
Error Pages
SvelteKit provides built-in error handling through +error.svelte files. When a load function throws an error or returns an error status, SvelteKit looks for the nearest +error.svelte file.
Create src/routes/+error.svelte for a global error page:
<script lang="ts">
import { page } from '$app/state';
</script>
<svelte:head>
<title>Error {page.status}</title>
</svelte:head>
<div class="error">
<h1>{page.status}</h1>
<p>{page.error?.message || 'Something went wrong'}</p>
<a href="/">Go back home</a>
</div>
<style>
.error {
text-align: center;
padding: 4rem 2rem;
}
h1 {
font-size: 4rem;
color: #ff3e00;
}
</style>
Throwing Errors in Load Functions
Use the error helper from SvelteKit to trigger error pages:
import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params }) => {
const post = await getPost(params.slug);
if (!post) {
error(404, {
message: 'Post not found'
});
}
return { post };
};
+page.ts vs +page.server.ts
SvelteKit offers two types of load functions with important differences:
+page.ts (Universal Load)
Runs on both the server (during SSR) and the client (during navigation):
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ fetch }) => {
const response = await fetch('/api/posts');
const posts = await response.json();
return { posts };
};
Use this when:
- You need to call a public API
- The data can be fetched from anywhere
- You want the load function to run during client-side navigation without a server round-trip
+page.server.ts (Server Load)
Runs only on the server:
import type { PageServerLoad } from './$types';
import { db } from '$lib/server/database';
export const load: PageServerLoad = async () => {
const posts = await db.query('SELECT * FROM posts ORDER BY created_at DESC');
return { posts };
};
Use this when:
- You need to access a database directly
- You need to use secrets or API keys
- The data requires server-only modules
API Routes with +server.ts
You can create API endpoints alongside your pages using +server.ts:
// src/routes/api/posts/+server.ts
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async () => {
const posts = [
{ id: 1, title: 'First Post' },
{ id: 2, title: 'Second Post' }
];
return json(posts);
};
export const POST: RequestHandler = async ({ request }) => {
const body = await request.json();
// Process the data...
return json({ success: true }, { status: 201 });
};
Each exported function corresponds to an HTTP method: GET, POST, PUT, PATCH, DELETE.
Parameter Validation with Matchers
You can validate route parameters using matchers. Create a file at src/params/integer.ts:
import type { ParamMatcher } from '@sveltejs/kit';
export const match: ParamMatcher = (param) => {
return /^\d+$/.test(param);
};
Then use it in your route folder name:
src/routes/posts/[id=integer]/+page.svelte
This route only matches when id is a valid integer. If someone visits /posts/abc, it will not match this route and SvelteKit will continue looking for other matches or return a 404.
Summary
SvelteKit's routing system is both intuitive and powerful:
- File-based routing maps your directory structure to URLs
- Dynamic routes with
[param],[...rest], and[[optional]]handle variable paths - Layouts nest automatically and provide shared UI
- Route groups with
(name)organize routes without affecting URLs - Error boundaries with
+error.sveltehandle failures gracefully - Two types of load functions give you flexibility in data fetching
- API routes with
+server.tslet you build backend endpoints
Understanding these patterns is essential for building well-structured SvelteKit applications. As your app grows, route groups and nested layouts will keep your code organized and maintainable.
Next Steps
- Learn about Svelte 5 runes for state management
- Explore form actions for handling user input
- Master load functions for data fetching
Source: Teta Engineering
This content was generated with AI from public sources. It represents analysis and commentary, not original journalism.