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:
src/routes/
├── +page.svelte → /
├── about/
│ └── +page.svelte → /about
└── blog/
└── [slug]/
└── +page.svelte → /blog/hello-world, /blog/anything
<!-- src/routes/+page.svelte -->
<h1>Welcome home</h1>
<!-- 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:
<!-- 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:
// 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 };
}
<!-- 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:
// 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
src/routes/
├── +layout.svelte
├── +page.svelte
└── products/
├── +page.js
└── +page.svelte
// src/routes/products/+page.js
export async function load({ fetch }) {
const response = await fetch('/api/products');
const products = await response.json();
return { products };
}
<!-- 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 plainfetchinside a+page.sveltecomponent instead of aloadfunction — it works, but loses SSR entirely for that data (the page's initial HTML renders without it) and reintroduces the double-fetch-on-hydration problemloadis designed to avoid. - Putting server-only logic (a database call, a private API key) in a
+page.jsloadfunction 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 datato actually receive what itsloadfunction 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.