LCM Logo
Web Development

SvelteKit

A concept-first tour of SvelteKit — routing, load functions, server vs client code, and form actions — building on Svelte.

SvelteKit is the official Svelte framework: it takes Svelte's component model and adds the pieces a real app needs — file-based routing, server rendering, data loading, and a production build — with sensible defaults. This guide assumes you know the Svelte basics; if not, start with the Svelte guide. Examples target SvelteKit 2 with Svelte 5.

SvelteKit 2 defaults: file-based routing under src/routes/, Vite as the bundler, server-side rendering on by default, and Svelte 5 with runes. New projects scaffold with TypeScript and Tailwind out of the box.

Creating a project

The Svelte CLI scaffolds a project with everything wired up — TypeScript, routing, and a dev server.

shell
npx sv create my-app
cd my-app
npm run dev   # Vite dev server on http://localhost:5173

File-based routing

Routes are folders under src/routes/, not configuration. Each folder becomes a URL segment, and a +page.svelte inside it makes that segment a routable page. A +layout.svelte wraps every page beneath it with shared UI (nav, footer) that persists across navigation; it renders its children through a {@render children()} snippet.

src/routes/+layout.svelte
<script lang="ts">
  let { children } = $props();
</script>

<!-- Wraps all routes; persists across navigations -->
<nav>My site</nav>
{@render children()}
src/routes/about/+page.svelte
<!-- Becomes the /about route -->
<h1>About us</h1>

Server vs client code

The core mental shift is where code runs. Files marked with .server run only on the server — they can talk to your database or read secrets, and their code never ships to the browser. A +page.server.ts is the safe place for privileged work; a plain +page.svelte is the interactive UI that runs in the browser.

src/routes/+page.server.ts
// Runs only on the server — safe for secrets and direct DB access
export function load() {
  return { message: "Rendered on the server." };
}
src/routes/+page.svelte
<script lang="ts">
  let { data } = $props(); // data comes from the load function above
</script>

<p>{data.message}</p>

Dynamic routes

A folder named in square brackets captures part of the URL as a parameter. A load function receives it via params, and the page reads the result through its data prop.

src/routes/blog/[slug]/+page.ts
export function load({ params }: { params: { slug: string } }) {
  return { slug: params.slug };
}
src/routes/blog/[slug]/+page.svelte
<script lang="ts">
  let { data } = $props();
</script>

<h1>Post: {data.slug}</h1>

Loading data

Data fetching lives in a load function, not in the component. A load in +page.ts runs on both server and client; a load in +page.server.ts runs only on the server. Whatever it returns becomes the page's data prop — no $effect, no loading-state plumbing.

src/routes/penguins/+page.ts
export async function load({ fetch }) {
  const res = await fetch("https://example.com/penguins.json");
  const penguins: { id: number; species: string }[] = await res.json();
  return { penguins };
}
src/routes/penguins/+page.svelte
<script lang="ts">
  let { data } = $props();
</script>

<ul>
  {#each data.penguins as p (p.id)}
    <li>{p.species}</li>
  {/each}
</ul>

SvelteKit gives load an enhanced fetch that, among other things, lets server-side requests run in-process and forwards cookies — so the same code works on both sides.

Mutating data with form actions

A form action is an async function exported from a +page.server.ts that handles a form submission on the server. The form posts to it directly, so you get working mutations even before any JavaScript loads — and use:enhance upgrades it to a smooth client-side submit when it does.

src/routes/new/+page.server.ts
export const actions = {
  default: async ({ request }) => {
    const data = await request.formData();
    const title = data.get("title");
    // ...write to your database here
  },
};
src/routes/new/+page.svelte
<script lang="ts">
  import { enhance } from "$app/forms";
</script>

<form method="POST" use:enhance>
  <input name="title" />
  <button type="submit">Create</button>
</form>

Linking and navigation

Use ordinary <a> tags. SvelteKit's router intercepts clicks on internal links, does client-side transitions instead of full reloads, and prefetches the target in the background so navigation feels instant.

Nav.svelte
<a href="/about">About</a>

You can tune prefetching per link with data-sveltekit-preload-data if you need finer control.

Page metadata

Set per-page <head> tags — titles, descriptions, Open Graph — with the <svelte:head> element. For dynamic values, combine it with data from your load function for SEO and sharing.

src/routes/about/+page.svelte
<svelte:head>
  <title>About us</title>
  <meta name="description" content="Who we are." />
</svelte:head>

<h1>About us</h1>

SvelteKit 2 details worth knowing

A few conventions catch newcomers out: data loading belongs in load functions, not in component $effects; $app/stores is deprecated in favor of the reactive page, navigating, and updated objects from $app/state (which also work inside .svelte.ts files); and the .server.ts suffix is what guarantees code stays off the client. When in doubt about where code runs, check the filename.

On this page