This episode covers the data flow in SvelteKit: load functions on server and client, fetching data with fetch inside load, form handling with server actions and progressive enhancement, plus error and redirect handling. You will understand how data flows from the server to components.

Static pages are only the beginning. Real applications need data: article lists, user details, or search results. Episode 5 covers the two core SvelteKit mechanisms that drive data: load functions to fetch data before rendering, and server actions to process input from forms.
Both are designed for a single experience: the same mental model on both server and client. The same load function can run during SSR to deliver fast HTML, then run again on the client for instant subsequent navigations.
By the end of this episode you can build pages that fetch dynamic data and safely process user input, without writing a manual API endpoint for every case.
The +page.server.js file contains a load function that only runs on the server. This is the right place to access a database, read secrets, or perform a redirect before the page is rendered.
export const load = async (event) => {
const { params, locals } = event;
const artikel = await cariArtikel(params.slug);
if (!artikel) {
throw redirect(303, "/artikel");
}
return { artikel };
};The returned data is serialized and sent to the client. Values that can't be serialized, like class instances or functions, never reach the browser.
The +page.js file (without .server) runs on the server during SSR and on the client during navigation. It's suited for public data that may be fetched from the browser.
export const load = async ({ fetch }) => {
const res = await fetch("/api/artikel");
const daftar = await res.json();
return { daftar };
};During SSR, the built-in fetch in a load function runs on the server and follows the original request's cookies. On the client, the same fetch runs normally. This makes the code identical on both sides.
The fetch function available as a load argument is smarter than the global fetch: it forwards cookies, runs on the server during SSR, and detects internal requests to your own routes so they don't add network load.
Use event.fetch when you want to call your application's own +server.js endpoints, or fetch data from an external API with the same headers. The result can be cached and streamed together with the page.
The rule of thumb:
+page.server.js.+page.js.src/lib module and call it from both sides.To prevent sensitive data from leaking, never load secrets in a universal load — +page.js code is sent to the browser and can be read by anyone.
SvelteKit offers server actions: functions in +page.server.js that handle form submissions. The form is sent as a POST, and the result updates client state without a full reload.
export const actions = {
login: async ({ request }) => {
const form = await request.formData();
const email = String(form.get("email") ?? "");
const password = String(form.get("password") ?? "");
if (!email || !password) {
return { error: "Email dan password wajib diisi" };
}
return { success: true };
}
};In the component, point the form at an action using the action attribute containing a question mark followed by the action name, then add the use:enhance directive for progressive enhancement.
<script>
import { enhance } from "$app/forms";
let { form } = $props();
</script>
<form method="POST" action="?/login" use:enhance>
<input name="email" type="email" placeholder="Email" />
<input name="password" type="password" placeholder="Password" />
<button type="submit">Masuk</button>
</form>
{#if form?.error}
<p class="error">{form.error}</p>
{/if}With use:enhance, the submission is sent via fetch: the form doesn't lose state, animations can be added, and the action result is immediately available. Without JavaScript, the form still works because it falls back to the native POST mechanism — this is the essence of progressive enhancement.
To move the user inside a load or action, throw a redirect:
import { redirect } from "@sveltejs/kit";
export const actions = {
logout: async (event) => {
event.cookies.delete("session", { path: "/" });
throw redirect(303, "/login");
}
};Use status code 303 after POST (the PRG pattern), 301 for permanent redirects, and 307 when the method must be preserved. throw redirect(303, "/login") always stops function execution.
For genuinely failing situations, use the error helper from @sveltejs/kit:
import { error } from "@sveltejs/kit";
export const load = async ({ params }) => {
const artikel = await cariArtikel(params.slug);
if (!artikel) {
throw error(404, "Artikel tidak ditemukan");
}
return { artikel };
};Thrown errors are rendered by +error.svelte at the nearest layout level. You can show a friendly message, status code, and a back button — much better than the browser's default error page.
Key takeaways:
+page.server.js is server-only, +page.js also runs on the client.event.fetch in load forwards cookies, runs on the server during SSR, and is efficient for internal requests.+page.server.js handle form submissions without a manual endpoint.use:enhance provides progressive enhancement; without JavaScript the form still works.throw redirect stops execution and moves the user; throw error shows a structured error page.+page.js is sent to the browser, so never put secrets there.In the next episode we shift to the presentation side: reactive UI & components. You'll learn Svelte component basics, reactive statements and bindings, slots and context modules, and scoped styling with CSS.