teta.so
SvelteKitRoutingTutorial

SvelteKit Routing Explained: Pages, Layouts, and Route Groups

AI Generated

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>&copy; 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.svelte handle failures gracefully
  • Two types of load functions give you flexibility in data fetching
  • API routes with +server.ts let 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

Source: Teta Engineering

This content was generated with AI from public sources. It represents analysis and commentary, not original journalism.

Build your next app with AI

Try Teta — create sites and apps with AI agents.

Get started free