This episode covers security in data fetching: securing API requests with auth headers, handling secrets and server-only config, CSRF and XSS protection, input sanitization, plus secure deployment configuration.

After covering authentication in episode 12, episode 13 focuses on the security of fetching and sending data. This is the most commonly overlooked layer of defense: API requests that leak tokens, secrets embedded in the client bundle, and unsanitized input.
SvelteKit provides a clear boundary between server and client, but that boundary is only useful if it is used correctly. All sensitive data must stay on the server side, and every path where user data enters must be verified.
After this episode, you can write applications that hold secrets properly, reject suspicious requests, and sanitize input before storing or rendering it.
External API calls that need a token are made from server load functions, not from the browser. The token is read from an environment variable and injected via the Authorization header, never visible to JavaScript on the client.
export const load = async ({ fetch }) => {
const res = await fetch("https://api.example.com/me", {
headers: {
Authorization: `Bearer ${process.env.API_TOKEN}`
}
});
if (!res.ok) {
throw error(502, "Gagal mengambil data dari layanan eksternal");
}
return { data: await res.json() };
};Server load functions are never sent to the browser; only the returned values are serialized. Your tokens stay safe as long as you do not store secrets inside returned values.
Inspect every value a load function returns. Do not return user objects containing passwordHash, apiKey, or raw tokens. Select only the fields the UI needs, or explicitly build a new object so the UI-facing shape does not drag internal data along.
export const load = async ({ locals }) => {
const user = locals.user;
return {
profil: {
id: user.id,
nama: user.nama,
avatar: user.avatar
}
};
};In SvelteKit, variables prefixed with PUBLIC_ can be accessed from the client via import.meta.env.PUBLIC_NAMA. All secrets must live in prefix-free environment variables and be read only on the server, for example through a module in src/lib/server.
const env = import.meta.env;
export const config = {
databaseUrl: env.DATABASE_URL,
apiToken: env.API_TOKEN
};
if (!config.databaseUrl || !config.apiToken) {
throw new Error("Environment variable tidak lengkap");
}Files in src/lib/server can only be imported from server code. Importing them from a client component fails at build time, so secrets hiding there cannot leak by accident.
Validating the environment at startup prevents the application from running with a wrong configuration. The validation schema can be written by hand or built with a library like Zod. With npm install zod, you can check the presence and format of each variable once, then reuse the result across the server.
The sameSite cookie attribute blocks most CSRF attacks, but for requests sent cross-origin, verifying the Origin header adds another layer of defense. Sensitive server actions should ensure the request origin matches the application origin.
import { error } from "@sveltejs/kit";
export const actions = {
ubahEmail: async ({ request }) => {
const origin = request.headers.get("origin");
const izinkan = process.env.ALLOWED_ORIGINS.split(",");
if (!origin || !izinkan.includes(origin)) {
throw error(403, "Origin tidak diizinkan");
}
const form = await request.formData();
const email = String(form.get("email") ?? "");
return { sukses: true };
}
};Svelte escapes every value rendered through curly braces, so XSS via common expressions is prevented. The danger appears when you use {@html} to render raw HTML — that value must be sanitized first with a library like sanitize-html or DOMPurify before being rendered.
<script>
import DOMPurify from "dompurify";
import { browser } from "$app/environment";
let { konten } = $props();
let htmlAman = browser ? DOMPurify.sanitize(konten) : konten;
</script>
{@html htmlAman}User input that reaches SQL must go through a parameterized query, never concatenated directly into a string. This way the input's content is treated as data, not as part of the command, so SQL injection loses its medium.
import db from "$lib/server/db";
export const load = async ({ url }) => {
const nama = url.searchParams.get("nama") ?? "";
const hasil = await db.query(
"SELECT * FROM pengguna WHERE nama ILIKE $1",
[`%${nama}%`]
);
return { hasil };
};The same principle applies to other libraries: always use the built-in parameter mechanism of the ORM or database driver, and avoid building queries from strings that could contain user input.
Complete your deployment with security headers: Content-Security-Policy to restrict script sources, Strict-Transport-Security to enforce HTTPS, and X-Content-Type-Options: nosniff. SvelteKit does not add these headers automatically, so configure them in your hosting platform or platform middleware.
Key takeaways:
Authorization header from the server, not the client.src/lib/server and validated when the application starts.sameSite cookies plus origin verification fend off CSRF.{@html}.In the next episode we get into caching & performance: caching headers and revalidation, static generation, prerendering, streaming, edge caching and CDN integration, plus performance budgets and monitoring.