Why Load Functions Exist

Every web application eventually needs to fetch data before it can render something meaningful to the user. A product page needs the product. A dashboard needs the metrics. A user profile needs the account details. The question is not whether you will fetch data — it is where and when you will do it, and what happens while you wait.

SvelteKit has a specific, well-reasoned answer to that question: the load function. Before a page component renders, SvelteKit gives you a dedicated place to fetch everything that page needs, and it guarantees that data will be ready before any markup hits the screen. This is not bolted-on middleware or a lifecycle hook you have to remember to call — it is a first-class part of how SvelteKit routes work.

This article covers what load functions are, why the framework was designed around them, and how to write one correctly from the beginning. By the end you will have a complete understanding of the file conventions involved, how data flows from a load function into your page component, and how SvelteKit’s generated TypeScript types make the whole thing fully type-safe without any manual effort.

A Note on Data Access in This Series

One deliberate choice applies to every article in this series: no ORMs, no query builders, no database client libraries appear anywhere in the code examples. All data access is expressed through SvelteKit’s fetch calls to API endpoints.

This keeps every example database-agnostic — the patterns work identically whether your backend uses PostgreSQL, SQLite, MongoDB, a third-party REST API, or anything else. The focus is on how SvelteKit load functions work, not on any particular data layer sitting behind them.

If you are using Prisma, Drizzle, or a similar tool in your own application, the load function structure is identical; the only difference is what sits inside the function body.

The Problem

To appreciate what load functions solve, it helps to think about the alternatives.

The most obvious approach, and the one developers coming from client-side frameworks often reach for first, is to fetch data inside the component itself. You define some reactive state, kick off a fetch request when the component mounts, and update the state when the data arrives. In the meantime, you render a loading spinner or a skeleton screen.

<!-- src/routes/articles/[slug]/+page.svelte -->
<!-- This pattern works, but it has real costs. -->

<script lang="ts">
	import { onMount } from 'svelte'

	let article = $state<Article | null>(null)
	let loading = $state(true)
	let error = $state<string | null>(null)

	onMount(async () => {
		try {
			const res = await fetch(`/api/articles/${slug}`)
			article = await res.json()
		} catch (e) {
			error = 'Failed to load article'
		} finally {
			loading = false
		}
	})
</script>

{#if loading}
	<p>Loading...</p>
{:else if error}
	<p>{error}</p>
{:else}
	<h1>{article?.title}</h1>
{/if}

This pattern works, but it comes with real costs in a server-rendered app. onMount never runs during SSR, it only runs in the browser. That means the server initially returns HTML without the article content, the client renders a loading state, and only after hydration does the fetch happen and the page finally fills in.

You end up with a worse experience for users (a visible “empty → loading → content” transition) and worse outcomes for SEO and social previews (the initial HTML is missing the meaningful content). You also pay for extra latency because the data is retrieved after the page has already been delivered.

A common “next step” is to introduce +server.ts endpoints and fetch from them in the component. That can be appropriate when you truly need a public API, but as a default data-loading strategy it’s often unnecessary overhead: you’re building and maintaining an HTTP endpoint mainly so your own frontend can call back into your own backend. In other words, you’ve created an extra hop such browser → your server → upstream data source, even though the server could have loaded the data before rendering in the first place.

Both approaches share the same underlying issue: data is fetched reactively (after the component has mounted) instead of declaratively (as a prerequisite for rendering).

reactively: The component renders first, then fetches data and updates itself when it arrives. This is the default pattern in client-side frameworks and works fine for purely client-rendered apps. but this can feel slow and jarring, especially on slower connections. It also means the initial HTML sent by the server is incomplete, which hurts SEO and social media previews.

declaratively: The component declares what data it needs, and the framework ensures that data is available before the component renders. This is the pattern SvelteKit encourages with load functions. It leads to faster perceived performance, better SEO, and a smoother user experience.

Declarative Data Loading is Better for Users and Developers

SvelteKit’s load functions fetch what the route needs up front, then render the page with complete data from the start.


Principles

SvelteKit organises your application into routes, and each route is a directory under src/routes. A route directory can contain several special files that SvelteKit treats with specific meaning. You are probably already familiar with +page.svelte, which contains the component that renders for that route.

The key insight is that +page.svelte is not the only file a route can have. SvelteKit supports a companion file called +page.js (or +page.ts for TypeScript) that sits alongside it. This file has one job: export a load function that fetches or derives whatever data the page needs. SvelteKit will call that function before rendering the component, wait for it to resolve, and then pass its return value into the component as a data prop.

The flow looks like this:

Loading diagram...

There is no manual wiring. You do not call the load function yourself, subscribe to any store, or pass data through props across component boundaries. SvelteKit handles the connection between the load function and the component automatically, using the file naming convention as the contract.

This model has an important consequence for server-side rendering. When a user first loads a page, the load function runs on the server. The component receives fully resolved data and renders complete HTML. When the user navigates to another page within the app, the load function runs in the browser instead, fetching only the data needed for that new route without a full page reload. You write the function once, and SvelteKit decides where to run it based on context.


Implementation

Step 1: Create the Route Structure

The file-pairing convention is what makes everything work, so it is worth being precise about it. For a route at /articles/[slug], you need two files in the same directory:

src/
└── routes/
    └── articles/
        └── [slug]/
            ├── +page.ts       ← the load function lives here
            └── +page.svelte   ← the component renders here

The relationship between the two files is simple: +page.ts runs first and hands its data to +page.svelte. SvelteKit wires them together automatically through file naming — no import, no explicit call, no store subscription.

Loading diagram...

The [slug] directory name is a dynamic segment. SvelteKit will capture whatever appears at that position in the URL and make it available inside your load function through the params object. If the URL is /articles/building-with-sveltekit, then params.slug will be 'building-with-sveltekit'.

Step 2: Write the Load Function

A load function is a named export from the +page.ts file. It receives a single argument that SvelteKit populates with context about the current request, and it returns a plain object containing whatever data the page needs.

Here is a realistic first example of fetching an article from a CMS API:

// src/routes/articles/[slug]/+page.ts

import type { PageLoad } from './$types'
import { error } from '@sveltejs/kit'

export const load: PageLoad = async ({ params, fetch }) => {
	const response = await fetch(`https://cms.example.com/api/articles/${params.slug}`)

	if (!response.ok) {
		// We will cover proper error handling in a later article.
		// error() signals a controlled failure and renders the nearest +error.svelte.
		error(response.status, `Failed to fetch article: ${response.status}`)
	}

	const article = await response.json()

	return {
		article
	}
}

There are several things worth examining carefully in this code.

params

The params object is typed based on your route’s dynamic segments. For this route, TypeScript will tell you that params.slug is a string. If you had a route like /shop/[category]/[productId], both params.category and params.productId would be available and typed.

The return value is a plain object. Anything you put here becomes available in the page component. You are not limited to a single key, returning multiple values is common and encouraged over making multiple separate load functions.

fetch

The fetch used here is not the native browser fetch. It is a special version provided by SvelteKit through the load event argument. You should always use this one rather than calling the global fetch directly.

SvelteKit’s version can make relative requests on the server (native fetch cannot), it inherits the current user’s cookies so credentialed requests work correctly, and during server-side rendering it captures the response and inlines it into the HTML so the browser does not have to make the same request again during hydration.

Don't Use the Global fetch in Load Functions

Using the global fetch instead of the one provided by SvelteKit is a common mistake in early SvelteKit code, and it breaks hydration consistency in ways that can be subtle to debug.

Step 3: Access the Data in the Page Component

Once the load function returns, SvelteKit makes its return value available to +page.svelte through a prop called data. You access it using $props(), which is Svelte 5’s rune for declaring props.

<!-- src/routes/articles/[slug]/+page.svelte -->

<script lang="ts">
	import type { PageProps } from './$types'

	let { data }: PageProps = $props()
</script>

<article>
	<h1>{data.article.title}</h1>
	<p class="meta">Published {data.article.publishedAt}</p>
	<div class="content">
		<!-- data.article.content is sanitised server-side before storage -->
		{@html data.article.content}
	</div>
</article>

The PageProps type is generated automatically by SvelteKit from your +page.ts file. It knows exactly what shape data has because it can see what your load function returns. This means data.article is fully typed, you get autocomplete, type checking, and refactoring support for the entire chain from load function to template, all without writing a single manual interface for the data shape.

This type generation happens through the ./$types import, which points to a file in SvelteKit’s hidden .svelte-kit/types directory. You never need to look at or edit this file. SvelteKit regenerates it every time you save, and your IDE reads it automatically.

Step 4: Understanding the Type System

SvelteKit generates several type aliases for each route, and knowing which one to use when is worth a moment of attention.

PageLoad

PageLoad is the type of the load function itself. You use it to type the load export in +page.ts. It ensures that the arguments SvelteKit passes in params, fetch, url, and the others are all correctly typed for your specific route.

// src/routes/articles/[slug]/+page.ts

// PageLoad types the load function and its arguments
import type { PageLoad } from './$types'

export const load: PageLoad = async ({ params }) => {
	// params.slug is typed as string because of the [slug] route segment
	return { slug: params.slug }
}

PageProps

PageProps is the type of the props that +page.svelte receives. It includes a data property whose type is inferred from what your PageLoad function returns. You use it to type the destructured props in your component’s $props() call.

<!-- src/routes/articles/[slug]/+page.svelte -->

<script lang="ts">
	// PageProps types the component's incoming data prop
	import type { PageProps } from './$types'

	let { data }: PageProps = $props()
	// data.slug is typed as string — inferred from the load function's return
</script>

If you are on an older version of SvelteKit (before 2.16.0), PageProps did not exist and you had to type data separately using PageData. The pattern looked like let { data }: { data: PageData } = $props(). If you encounter this in older code, it is functionally equivalent — just more verbose.


Common Mistakes and Anti-Patterns

Using the Global fetch Instead of the Load Function’s fetch

This is by far the most frequent mistake in early SvelteKit code. The global fetch and SvelteKit’s load fetch look identical at the call site, which makes the mistake easy to miss.

// src/routes/products/[id]/+page.ts

// AVOID: Using the global fetch
export const load: PageLoad = async ({ params }) => {
	const res = await fetch(`/api/products/${params.id}`)
	return { product: await res.json() }
}
// src/routes/products/[id]/+page.ts

// PREFERRED: Using the fetch provided by the load event
export const load: PageLoad = async ({ params, fetch }) => {
	const res = await fetch(`/api/products/${params.id}`)
	return { product: await res.json() }
}

The global fetch has three problems in a load function context.

  1. It cannot make relative URL requests on the server, you would need to provide the full origin, which is awkward to construct correctly.

  2. It does not inherit the user’s session cookies, so any authenticated request will fail or return data for an anonymous user.

  3. SvelteKit cannot intercept it to inline the response into the server-rendered HTML, which means the browser will make a second identical request during hydration. The provided fetch handles all three cases correctly with no additional configuration.

Fetching Data in the Component Instead of the Load Function

It is tempting to do a small secondary fetch inside the component itself, especially for data that feels “less important” as a related articles sidebar, a comment count, or an author bio. The problem is that any fetch inside the component runs after the component mounts, which means a loading state is unavoidable and the data is not available for server-side rendering.

<!-- AVOID: Fetching inside the component -->

<script lang="ts">
	import { onMount } from 'svelte'
	import type { PageProps } from './$types'

	let { data }: PageProps = $props()

	// This will not render on the server and creates a loading flash
	let relatedArticles = $state<Article[]>([])
	onMount(async () => {
		const res = await fetch(`/api/articles/related/${data.article.id}`)
		relatedArticles = await res.json()
	})
</script>
// PREFERRED: Return everything from the load function

// src/routes/articles/[slug]/+page.ts
export const load: PageLoad = async ({ params, fetch }) => {
	const [articleRes, relatedRes] = await Promise.all([
		fetch(`https://cms.example.com/api/articles/${params.slug}`),
		fetch(`https://cms.example.com/api/articles/${params.slug}/related`)
	])

	return {
		article: await articleRes.json(),
		relatedArticles: await relatedRes.json()
	}
}

The correct version uses Promise.all to fetch both pieces of data concurrently, so there is no waterfall penalty. Both datasets are available immediately when the component renders, including during server-side rendering. We will cover the finer points of parallel loading in a later article.

Forgetting to Use $types for Type Safety

It is possible to write a load function without importing PageLoad at all, TypeScript will infer some types and the code will run. But you lose the guarantee that your load function’s return type matches what the component expects, and you lose the precise typing on the load event arguments like params.

// src/routes/articles/[slug]/+page.ts

// WEAK: No explicit typing — TypeScript will infer loosely
export async function load({ params, fetch }) {
	const res = await fetch(`/api/articles/${params.slug}`)
	return { article: await res.json() }
}
// src/routes/articles/[slug]/+page.ts

// PREFERRED: Explicit PageLoad type — params.slug is typed, return is tracked
import type { PageLoad } from './$types'

export const load: PageLoad = async ({ params, fetch }) => {
	const res = await fetch(`/api/articles/${params.slug}`)
	return { article: await res.json() }
}

Always import from ./$types. It is generated for your specific route and provides the tightest possible typing. Skipping it might seem like a small shortcut, but it breaks the end-to-end type safety chain that makes refactoring and maintenance significantly safer in larger applications.


Performance and Scaling Considerations

Load functions are designed for performance from the start, but there are a few patterns to be aware of as your application grows.

SvelteKit tracks what each load function accesses, specifically which params and url properties it reads, and uses that information to decide whether the function needs to run again during client-side navigation. If you navigate from one blog post to another, SvelteKit knows that params.slug changed, so it reruns the page load function.

But if you navigate between different sections of the site that share the same layout, the layout’s load function only reruns if the data it depends on has changed. This dependency tracking is automatic and requires no configuration, but it means you should access exactly the params and url properties you need. Do not destructure everything from url if you only need url.pathname, as unnecessary reads create unnecessary dependencies.

For the fetch calls themselves, SvelteKit deduplicates requests during server-side rendering. If your load function and a layout’s load function both fetch the same URL, SvelteKit will only make the network request once. This deduplication relies on the enhanced fetch being used consistently, which means another reason the global fetch is problematic.

As your load functions grow more complex and return more data, TypeScript inference across the $types boundary remains fast because the generated types are simple object types, not complex conditional generics. Even very large return objects type-check efficiently.


When NOT to Use This Pattern

Load functions are the right tool for data that the page needs before it can render its primary content. They are not always the right tool for every piece of data in an application.

Data that changes in real time like a live stock ticker, a chat message feed or a presence indicator does not belong in a load function because load functions run once per navigation, not continuously. That kind of data is better served by a WebSocket connection or a Server-Sent Events stream managed inside the component.

Data that depends on user interaction rather than route parameters is also a poor fit. If the user is filtering a list by clicking chips, applying that filter through a load function would require a navigation event for every click. That is too heavy, client-side filtering of already-loaded data, or a search API called from within the component, handles this more naturally.

There is also a meaningful distinction between +page.ts and +page.server.ts that this article has intentionally deferred. The load function shown here runs both on the server and in the browser, which is fine for public APIs that do not require credentials or private environment variables. If your load function needs to access a database directly, read a server-only secret, or inspect a session cookie, it belongs in +page.server.ts instead. That distinction between universal versus server load is the subject of the next article in this series.


Conclusion

Load functions are SvelteKit’s answer to a question every web application has to answer: when and where does data get fetched? By making the load function a peer of the page component, with a clear contract between them and automatic type generation connecting the two, SvelteKit turns data fetching from an imperative side effect into a declarative prerequisite of rendering.

The practical result is pages that arrive fully rendered, with no loading flashes on first visit, no hydration mismatches, and a type system that validates the entire data pipeline from server to template without any manual type writing. The file convention of +page.ts alongside +page.svelte makes the relationship visible at a glance in any file explorer.

Understanding this foundation thoroughly makes every more advanced topic in SvelteKit’s data loading system easier to reason about, because all of it builds on the same basic model: functions that run before the page renders, return plain objects, and hand that data directly to the component.


Key Takeaways

The load function is a named export from +page.ts (or +page.js) that SvelteKit calls automatically before rendering the paired +page.svelte component. Its return value becomes the data prop of the component, typed end-to-end by the generated PageLoad and PageProps types from ./$types.

Always use the fetch provided by the load event argument rather than the global fetch. The provided version handles relative URLs on the server, inherits the user’s cookies for authenticated requests, and integrates with SvelteKit’s hydration system to prevent duplicate network requests.

Load functions run on the server during SSR and in the browser during client-side navigation. This dual-environment execution is what makes the pattern work, meaning the same code produces server-rendered HTML for first visits and fast, data-only updates for subsequent navigation.


What’s Next

Now that you understand how a single page loads its data, the next natural question is about scope. What happens when multiple pages in a section of your app all need the same data like a navigation menu, a user object, a set of permissions? That is what layout load functions solve, and they follow exactly the same pattern as page load functions with one important difference in how their data is shared. The next article in this series, Page Data vs Layout Data, covers exactly that.


Further Reading