Load Functions
Overview
Every page in SvelteKit can define a load function that runs before the page renders. This function fetches data, checks permissions, and prepares everything the page needs. The page component receives the returned data as a prop and never has to think about where the data came from or how it was fetched.
SvelteKit offers two types of load functions: universal load in +page.ts and server-only load in +page.server.ts. The difference is where they run and what they can access.
Universal Load: +page.ts
A universal load function runs on the server during SSR and on the client during client-side navigation. It can import and use any module that works in both environments.
// src/routes/blog/+page.ts
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ fetch }) => {
const response = await fetch('/api/posts');
const posts = await response.json();
return { posts };
};
<!-- src/routes/blog/+page.svelte -->
<script lang="ts">
let { data } = $props();
</script>
<h1>Blog</h1>
{#each data.posts as post}
<article>
<a href="/blog/{post.slug}">{post.title}</a>
<p>{post.excerpt}</p>
</article>
{/each}
When a user first visits /blog, the load function runs on the server. When the user navigates to /blog from another page via a link, the load function runs on the client. The page component does not know or care which happened.
When to Use Universal Load
Universal load is the right choice when your data comes from a public API, when the fetching logic does not involve secrets, and when you want client-side navigation to avoid round-trips to your server.
// src/routes/weather/+page.ts
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ fetch, url }) => {
const city = url.searchParams.get('city') ?? 'London';
const response = await fetch(
`https://api.weather.example.com/current?city=${city}`
);
const weather = await response.json();
return { weather, city };
};
During client-side navigation, this fetches directly from the weather API without going through your server. Faster for the user, less load on your infrastructure.
Server-Only Load: +page.server.ts
A server-only load function runs exclusively on the server. It never ships to the client bundle. It can access databases, file systems, environment variables, and any server-only resource.
// src/routes/dashboard/+page.server.ts
import type { PageServerLoad } from './$types';
import { db } from '$lib/server/database';
import { ANALYTICS_API_KEY } from '$env/static/private';
export const load: PageServerLoad = async ({ locals }) => {
const user = locals.user;
const [orders, analytics] = await Promise.all([
db.order.findMany({
where: { userId: user.id },
orderBy: { createdAt: 'desc' },
take: 10
}),
fetch('https://analytics.example.com/api/summary', {
headers: { Authorization: `Bearer ${ANALYTICS_API_KEY}` }
}).then((r) => r.json())
]);
return { orders, analytics };
};
When to Use Server-Only Load
Use +page.server.ts when you need:
Database access → Direct queries with Prisma, Drizzle, etc.
Private API keys → Keys that must never appear in client JS
Sensitive data logic → Filtering out fields the client should not see
File system access → Reading files from the server's disk
Server-only libraries → Node.js built-ins, native modules
// src/routes/admin/users/+page.server.ts
import type { PageServerLoad } from './$types';
import { db } from '$lib/server/database';
import { error, redirect } from '@sveltejs/kit';
export const load: PageServerLoad = async ({ locals }) => {
if (!locals.user?.isAdmin) {
redirect(303, '/login');
}
const users = await db.user.findMany({
select: {
id: true,
name: true,
email: true,
createdAt: true
// password hash, tokens, etc. are excluded by select
}
});
return { users };
};
The select ensures that sensitive fields never leave the server. Even if someone inspects the network response, those fields are not included.
The Load Function Signature
Every load function receives an event object with the same core properties.
params
The dynamic route parameters from the URL.
// src/routes/blog/[slug]/+page.server.ts
export const load: PageServerLoad = async ({ params }) => {
// /blog/hello-world → params.slug = "hello-world"
const post = await db.post.findUnique({
where: { slug: params.slug }
});
return { post };
};
url
The full URL object. Useful for reading query parameters.
// src/routes/search/+page.ts
export const load: PageLoad = async ({ url, fetch }) => {
const query = url.searchParams.get('q') ?? '';
const page = parseInt(url.searchParams.get('page') ?? '1');
const response = await fetch(`/api/search?q=${query}&page=${page}`);
const results = await response.json();
return { results, query, page };
};
fetch
A special fetch that preserves cookies during SSR, resolves relative URLs, and prevents duplicate requests.
// src/routes/profile/+page.ts
export const load: PageLoad = async ({ fetch }) => {
// This fetch preserves the user's session cookie during SSR
// A regular fetch() would not send cookies on the server
const response = await fetch('/api/me');
const profile = await response.json();
return { profile };
};
Always use the provided fetch instead of the global fetch. The provided version handles cookies, relative paths, and deduplication correctly.
parent
Access data from parent layout load functions.
// src/routes/shop/[category]/+page.server.ts
export const load: PageServerLoad = async ({ params, parent }) => {
const parentData = await parent();
// parentData includes whatever the shop layout returned
const products = await db.product.findMany({
where: { categorySlug: params.category }
});
return { products };
};
Calling parent() waits for all ancestor load functions to complete. Use it only when you genuinely need the parent data.
Returning Data to the Page
The load function returns a plain object. Every property on that object becomes available in the page component through data.
// src/routes/profile/+page.server.ts
export const load: PageServerLoad = async ({ locals }) => {
const user = await db.user.findUnique({
where: { id: locals.user.id }
});
const recentActivity = await db.activity.findMany({
where: { userId: locals.user.id },
take: 20,
orderBy: { timestamp: 'desc' }
});
return {
user,
recentActivity,
memberSince: user.createdAt.toISOString()
};
};
<!-- src/routes/profile/+page.svelte -->
<script lang="ts">
let { data } = $props();
</script>
<h1>{data.user.name}</h1>
<p>Member since {data.memberSince}</p>
<h2>Recent Activity</h2>
<ul>
{#each data.recentActivity as activity}
<li>{activity.description} - {activity.timestamp}</li>
{/each}
</ul>
Data must be serializable. You cannot return functions, class instances with methods, or circular references from server load functions. SvelteKit serializes the data to send it from server to client.
Both Files at the Same Route
You can have both +page.ts and +page.server.ts at the same route. The server load runs first and its data is passed to the universal load via data.
// src/routes/dashboard/+page.server.ts
export const load: PageServerLoad = async ({ locals }) => {
const metrics = await db.metric.findMany({
where: { teamId: locals.user.teamId }
});
return { metrics };
};
// src/routes/dashboard/+page.ts
export const load: PageLoad = async ({ data }) => {
// data.metrics comes from the server load
const enriched = data.metrics.map((m) => ({
...m,
formatted: formatMetric(m.value, m.unit),
trend: calculateTrend(m.history)
}));
return { metrics: enriched };
};
This pattern is useful when you need server-only data access but also want client-side transformation or enrichment. The server load fetches, the universal load transforms.
Common Pitfalls
- Using global fetch instead of the provided fetch: The global
fetchdoes not forward cookies during SSR. Always destructure and use thefetchfrom the load event. - Returning non-serializable data from server load: Dates, Maps, Sets, and class instances do not serialize cleanly. Convert dates to strings, Maps to objects, and return plain data.
- Creating waterfalls with parent(): Calling
await parent()blocks until all ancestor load functions complete. If your page load does not actually need parent data, skip theparent()call entirely. - Putting secrets in +page.ts: Universal load functions ship to the client. API keys, database connection strings, and any private configuration must only appear in
+page.server.ts. - Over-fetching data: Return only the fields the page needs. Loading entire database records and sending them to the client wastes bandwidth and risks leaking sensitive fields.
Key Takeaways
+page.ts(universal load) runs on both server and client. Use it for public API calls and logic that benefits from running on the client during navigation.+page.server.ts(server-only load) runs exclusively on the server. Use it for database access, private API keys, and any data that must not reach the client.- The load function receives
params,url,fetch, andparent. Use the providedfetchfor cookie forwarding and deduplication. - Data returned from load is available in the page component as
data. Everything returned must be serializable. - You can combine both files at the same route: server load fetches sensitive data, universal load transforms it for the client.