SvelteKit Basics

What SvelteKit adds over plain Svelte — file-based routing and load functions — with a complete example.

What SvelteKit adds on top of Svelte

Svelte itself, as covered so far in this track, is a component compiler — it has no concept of a URL, no built-in way to fetch data before a page renders, and no opinion about how a project is bundled or served. SvelteKit is Svelte's official application framework, the same relationship Next.js has to React or Nuxt has to Vue: it adds file-based routing, server-side rendering, data loading, and a full build pipeline on top of plain Svelte components.

Concern Plain Svelte (Vite + Svelte template) SvelteKit
Routing Add a router library yourself File-based, generated from src/routes/
Rendering Client-side only Server-rendered, statically generated, or a mix — chosen per route
Data fetching onMount + fetch, or a library load functions, aware of SSR, run before the page renders
Backend endpoints A separate server needed Server routes built in, alongside pages

As the introduction page in this track already mentioned, npx sv create scaffolds a SvelteKit project by default — this page covers what that scaffolding actually gives you, and how its two signature pieces (file-based routing and load functions) fit together.

File-based routing in src/routes/

Every route in a SvelteKit app corresponds to a folder under src/routes/, and a +page.svelte file inside a folder is what makes that folder an actual navigable page:

Plaintext
src/routes/
├── +page.svelte              →  /
├── about/
│   └── +page.svelte           →  /about
└── blog/
    └── [slug]/
        └── +page.svelte       →  /blog/hello-world, /blog/anything
HTML
<!-- src/routes/+page.svelte -->
<h1>Welcome home</h1>
HTML
<!-- src/routes/blog/[slug]/+page.svelte -->
<script>
  export let data; // populated by this route's +page.js load function, covered below
</script>

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

[slug], a folder name wrapped in square brackets, is a dynamic segment — the same convention Next.js and Nuxt both use for exactly the same purpose, matching any value at that position in the URL.

A +layout.svelte file wraps every page (and every nested layout) beneath it in the folder tree, persisting across navigations between sibling pages rather than being torn down and rebuilt on every navigation:

HTML
<!-- src/routes/+layout.svelte -->
<script>
  export let data;
</script>

<nav>
  <a href="/">Home</a>
  <a href="/about">About</a>
</nav>

<slot /> <!-- the matched page renders here -->

load functions: fetching data before a page renders

A +page.js (or +page.server.js for server-only logic) file exports a load function that runs before its corresponding +page.svelte renders, and whatever it returns arrives in the page as its data prop:

Javascript
// src/routes/blog/[slug]/+page.js
export async function load({ params, fetch }) {
  const response = await fetch(`/api/posts/${params.slug}`);

  if (!response.ok) {
    throw new Error('Post not found');
  }

  const post = await response.json();
  return { post };
}
HTML
<!-- src/routes/blog/[slug]/+page.svelte -->
<script>
  export let data;
</script>

<h1>{data.post.title}</h1>
<p>{data.post.body}</p>

load receives params (the matched dynamic segments, like slug above) and a SvelteKit-provided fetch that works identically whether load runs on the server or in the browser — this is the same problem Nuxt's useFetch solves: a plain fetch call made directly inside a component would run once during SSR and then run again on the client during hydration, doubling the request. SvelteKit's fetch inside load avoids that by design, and its result is serialized into the initial page load so the client doesn't refetch what the server already fetched.

A +page.server.js file (instead of +page.js) runs its load function only on the server, never shipped to the client bundle at all — the right place for anything touching a database directly, a private API key, or other server-only logic:

Javascript
// src/routes/dashboard/+page.server.js
import { db } from '$lib/server/db';

export async function load({ locals }) {
  const orders = await db.orders.findMany({ where: { userId: locals.userId } });
  return { orders };
}

A minimal complete example

Plaintext
src/routes/
├── +layout.svelte
├── +page.svelte
└── products/
    ├── +page.js
    └── +page.svelte
Javascript
// src/routes/products/+page.js
export async function load({ fetch }) {
  const response = await fetch('/api/products');
  const products = await response.json();
  return { products };
}
HTML
<!-- src/routes/products/+page.svelte -->
<script>
  export let data;
</script>

<h1>Products</h1>
<ul>
  {#each data.products as product}
    <li><a href="/products/{product.id}">{product.name}</a></li>
  {/each}
</ul>

Visiting /products runs the load function first (on the server for the initial request, keeping SEO and first-paint benefits intact, and purely client-side for subsequent client-routed navigations), then renders +page.svelte with the fetched products already available as data.products — no loading spinner needed for data that's ready before the component even mounts.

Common mistakes

  • Fetching data with onMount + a plain fetch inside a +page.svelte component instead of a load function — it works, but loses SSR entirely for that data (the page's initial HTML renders without it) and reintroduces the double-fetch-on-hydration problem load is designed to avoid.
  • Putting server-only logic (a database call, a private API key) in a +page.js load function instead of +page.server.js+page.js's code is also bundled and can run in the browser, which is the wrong place for anything that must stay server-only.
  • Forgetting that a page must declare export let data to actually receive what its load function returned — without it, the fetched data exists but the component has no way to read it.
  • Confusing Svelte (the component compiler) with SvelteKit (the application framework) when reading documentation or tutorials — a plain Svelte component works the same either way, but routing, load, and SSR are SvelteKit-specific concepts with no equivalent in bare Svelte.