Server-Only Load Functions
Using +page.server.js for data that must never reach the browser, like database queries and secrets.
3 min de lecture
A +page.js load function runs in both the browser and the server, which means its code — and anything it imports — ends up in the client-side JavaScript bundle. That's fine for calling a public API, but wrong for querying a database directly or using a secret API key. For that, SvelteKit has +page.server.js.
The difference in one example
// src/routes/dashboard/+page.server.js
import { db } from '$lib/server/database';
export async function load({ locals }) {
const projects = await db.query('SELECT * FROM projects WHERE user_id = ?', [locals.user.id]);
return { projects };
}This code, and the db client it imports, is guaranteed to never be bundled for the browser — SvelteKit enforces this at build time, not just by convention. If you tried to import $lib/server/database from a +page.js or a .svelte file, the build would fail with an explicit error, rather than silently leaking your database credentials into client JavaScript.
Both files can coexist
A route can have a +page.js and a +page.server.js side by side — the server one always runs first, and its return value is merged into what the universal one receives as a data parameter:
// +page.server.js — runs only on the server
export async function load() {
return { secretConfig: 'server-only-value' };
}// +page.js — runs on server and client
export async function load({ data }) {
// `data` here is whatever +page.server.js returned
return { ...data, timestamp: Date.now() };
}In practice, most routes need only one or the other, not both — reach for +page.server.js alone unless you specifically need logic that must re-run in the browser on client-side navigation too.
locals: passing data from hooks
locals is how server-only request data — like the currently authenticated user, set in hooks.server.js — reaches a load function without a database round-trip on every page:
// src/routes/profile/+page.server.js
import { redirect } from '@sveltejs/kit';
export async function load({ locals }) {
if (!locals.user) {
redirect(303, '/login');
}
return { user: locals.user };
}redirect(), like error() from the previous lesson, throws a special exception SvelteKit intercepts — here producing an HTTP redirect rather than rendering the page at all. This is the standard way to protect a route: check locals at the top of load and redirect before any data fetching happens.
Why not just do this in +page.js with a server check?
You might be tempted to write if (browser) return; inside a universal load function to skip server-only logic. Resist it — that pattern still ships the sensitive imports to the client bundle, it just skips running them there. +page.server.js is a build-time guarantee, not a runtime check: the file itself is never sent to the browser, so there's no code to inspect, tamper with, or accidentally execute client-side.
Choosing between +page.js and +page.server.js comes down to one question: does this data-fetching code need a secret, a database connection, or anything else that must never leave the server? If yes, +page.server.js. If it's just calling a public API, either works, but +page.js allows that fetch to happen client-side on subsequent navigations, which can feel snappier for non-sensitive data.