Introduction
Data loading in SvelteKit is where server logic meets the framework's reactivity. Unlike client-side frameworks that fetch after render, SvelteKit load functions are built to fetch data before the page renders, keeping your UI synchronised and your waterfalls flat. This guide walks through SvelteKit load functions: a hands-on guide for engineers, covering universal loads, server-only secrets, and the patterns that keep production systems fast.
+page.ts — Universal loads that run everywhere
A +page.ts file exports a load function that runs on both server and browser. This is your default choice for public data: API calls, database queries that don't expose secrets, or computed values that need to be fresh.
Universal loads are useful when you want to reuse the same logic across environments. For example, transforming or filtering data fetched from a public endpoint. SvelteKit re-runs them on the client during navigation (unless you disable that), so they're reactive to route changes.
Here's a real example: loading a product from an API for an e-commerce detail page.
// src/routes/products/[slug]/+page.ts
import { error } from '@sveltejs/kit';
export const load = async ({ params, fetch }) => {
const res = await fetch(`/api/products/${params.slug}`);
if (!res.ok) {
error(404, 'Product not found');
}
const product = await res.json();
return { product };
};
Notice we pass fetch from the load context, not the global fetch. This is critical: SvelteKit's fetch knows about the server context, handles credentials properly, and doesn't make unnecessary requests during server-side rendering.
The returned object becomes the page's data prop. If you return { product }, the component receives it immediately. If the load throws or returns invalid data, the nearest error boundary catches it.
+page.server.ts — Server-only loads for secrets and databases
When you need to access databases, API keys, or session data, use +page.server.ts. This file only runs on the server, keeping sensitive logic hidden from the browser. It's the right place for authentication checks, database queries, and calls to internal services.
Here's a practical scenario: you're building a SaaS dashboard. Users can only see their own data, and you fetch it from your database using session cookies.
// src/routes/dashboard/+page.server.ts
import { error, redirect } from '@sveltejs/kit';
export const load = async ({ locals, params }) => {
const user = locals.user;
if (!user) {
redirect(303, '/login');
}
const posts = await db.post.findMany({
where: { authorId: user.id },
orderBy: { createdAt: 'desc' }
});
return { posts, user };
};
The locals object typically carries session data from your auth middleware. In this example, we're redirecting unauthenticated users early, then querying only the current user's posts. This logic never touches the browser, so your database connection strings and user IDs remain server-side only.
Server loads also handle errors gracefully. Throw an error with a status code and message, and SvelteKit renders your error page with that context:
// src/routes/posts/[slug]/+page.server.ts
export const load = async ({ params }) => {
const post = await db.post.findUnique({
where: { slug: params.slug }
});
if (!post) {
error(404, 'Post not found');
}
return { post };
};
If the post doesn't exist, SvelteKit stops rendering and shows your error boundary. You can catch that in a +error.svelte component and display a friendly message.
Using the data in your component
Once the load function returns data, it flows into your page component through the data prop. In modern Svelte (5+), use $props() to receive it:
// src/routes/posts/[slug]/+page.svelte
<script>
let { data } = $props();
</script>
<h1>{data.post.title}</h1>
<p>{data.post.content}</p>
<time>{new Date(data.post.createdAt).toLocaleDateString()}</time>
The component renders only after the load function completes. There's no flashing spinners or content layout shifts because the data is already there when the page mounts.
If you need to fetch additional data on the client—say, comments that load on scroll—you can still use fetch inside event handlers or reactive blocks. But your core, synchronous page data comes from the load function.
Parallel loading — Avoiding waterfalls
When a page needs multiple data sources, run them in parallel. A common mistake is awaiting queries one after another, which stacks latencies. Use Promise.all or object destructuring to run them at the same time.
Say you're loading a user profile page that needs user details, posts, and followers:
// src/routes/profile/[id]/+page.server.ts
export const load = async ({ params }) => {
const userId = parseInt(params.id);
// Bad: sequential
// const user = await db.user.findUnique({ where: { id: userId } });
// const posts = await db.post.findMany({ where: { authorId: userId } });
// const followers = await db.follower.findMany({ where: { userId } });
// Good: parallel
const [user, posts, followers] = await Promise.all([
db.user.findUnique({ where: { id: userId } }),
db.post.findMany({ where: { authorId: userId } }),
db.follower.findMany({ where: { userId } })
]);
if (!user) {
error(404, 'User not found');
}
return { user, posts, followers };
};
If each query takes 50ms, running them sequentially takes 150ms. In parallel, you hit the maximum latency of the slowest query, so roughly 50ms. For a ride-hailing or trading platform where latency compounds, this matters.
You can also use object shorthand to keep code readable:
const [user, posts, followers] = await Promise.all([
db.user.findUnique({ where: { id: userId } }),
db.post.findMany({ where: { authorId: userId } }),
db.follower.count({ where: { userId } })
]);
return { user, posts, followerCount: followers };
Or with async/await if your queries have conditional dependencies:
export const load = async ({ params, locals }) => {
const user = locals.user;
if (!user) {
redirect(303, '/login');
}
// These depend on user, so fetch them in parallel
const [posts, notifications] = await Promise.all([
db.post.findMany({ where: { authorId: user.id } }),
db.notification.findMany({ where: { userId: user.id }, take: 10 })
]);
return { posts, notifications };
};
Common mistakes
Using the global fetch in load functions. Always use the fetch parameter from the load context. It handles credentials, cookies, and server-side rendering properly.
Accessing browser APIs in server loads. +page.server.ts runs only on the server, so window, localStorage, and DOM APIs don't exist. If you need them, move that logic to the component or a +page.ts universal load (and handle the server-side case carefully).
Fetching data inside components instead of load functions. It's tempting to call fetch in onMount, but that delays rendering. Load functions run before the page mounts, so use them for initial data.
Ignoring error boundaries. Always throw or return errors from load functions. Don't return a fallback { error: null } object and handle it in the component. Let SvelteKit's error handling do its job.
Sequential queries when parallel works. If queries are independent, run them with Promise.all. Your users will notice the latency savings.
Key takeaways
- Use
+page.tsfor public data and logic that can run on the server or client. - Use
+page.server.tsfor secrets, databases, and session-aware queries—they never leak to the browser. - Always use the
fetchparameter from the load context, not the globalfetch. - Return data from the load function and receive it via
let { data } = $props()in your component. - Run independent queries in parallel with
Promise.allto avoid cascading latency. - Throw errors with status codes in load functions to trigger SvelteKit's error handling.
- Combine server loads with database middleware and session handling for secure, efficient data flows.






Comments
Loading comments…