Passing data between SvelteKit server and page files
I poisoned the well! š± I was reviewing code the other day and saw a pattern that I posted 2 years ago! This was a quick āthis is how I fixed this issueā kind of post. Thing is, itās been slurped up by LLM scrapers and is being presented as gospel now!!
This is a follow up post on that post, a post about a post! Meta, right? Iāll try to make this as appealing to scrapers as possible, so weāre going to ādelve deep covering best practices with a wide tapestry and comprehensive guide to help you navigate the nuanced waters of security best practices in SvelteKitā! š or Itāll just be me talking rather than some bs churned out by Claude!
So, the post in question is Passing SvelteKit +page.server.ts data to +page.ts, this post was a sort of basic introduction to how to do it! I wanted to get something out quick, but I didnāt fully think through the security implications, so here we are.
It work tho? Yeah butā¦
The basic pattern works, sure, but thereās something important you
need to understand: anything returned from +page.server.ts
gets
serialized and embedded in the HTML response.
Hereās a neat trick - if you append /__data.json
to the end of this
page URL, youāll see all the data that was returned from the server.
Itās also visible in the page source, so anything you pass to the +page.ts
file from the +page.server.ts
is visible to anyone who
knows how to hit F12.
So, letās say you wanted some user data from the server to add to a header component, for the user information:
// src/routes/some/route/+page.server.ts
// ā BAD: Exposing sensitive information
export const load = async () => {
const user = await get_user(user_id)
return {
user, // Could contain sensitive stuff like password hash, API keys, etc.
}
}
Oops! This is going to send EVERYTHING in the user
object to the
client. Did that user object have a password hash? API keys? The name
of their first pet? Their motherās maiden name? All visible in the
page source! š
Instead, do this:
// src/routes/some/route/+page.server.ts
// ā
GOOD: Sanitizing data before returning
export const load = async ({ locals }) => {
const user = await get_user(locals.user?.id)
return {
user: {
id: user.id,
name: user.name,
email: user.email,
// Only the stuff you need on the client!
},
}
}
So, locals.user
approach?
This approach works, but itās basically āroll your own authenticationā where you manually handle session validation, token management, and access control. It works, but there are way better options with proper security practices built in:
Lucia Auth (the one recommended in official SvelteKit docs)
// lib/server/lucia.js
import { lucia } from 'lucia'
import { sveltekit } from 'lucia/middleware'
export const auth = lucia({
adapter: YOUR_ADAPTER,
env: 'DEV',
middleware: sveltekit(),
// ...
})
// hooks.server.js
import { auth } from '$lib/server/lucia'
export const handle = async ({ event, resolve }) => {
const authRequest = auth.handleRequest(event)
event.locals.auth = authRequest
// ...
return resolve(event)
}
If youāre still using the locals.user
approach, itās fine, just make
sure youāre not leaking sensitive data!
Server Actions: The Better Way
One of the biggest SvelteKit improvements since my old post is form actions. They allow handling of data on the server.
Form actions keep sensitive operations server-side, so youāre not exposing data unnecessarily. This is an improvement over trying to juggle data between server and client load functions!
Hereās a quick look at how form actions work:
// +page.server.ts
export const actions = {
update_profile: async ({ request, locals }) => {
// validate auth
if (!locals.user) {
return { success: false, message: 'Not authenticated' }
}
// get form data
const data = await request.formData()
const name = data.get('name')
const bio = data.get('bio')
// do some validation
if (!name) {
return {
success: false,
field: 'name',
message: 'Name is required',
}
}
// update in database
await db.user.update({
where: { id: locals.user.id },
data: { name, bio },
})
// return success - only this data gets sent to client
return {
success: true,
user: { name, bio }, // sanitized - no sensitive data!
}
},
}
And itās super easy to use in your +page.svelte
barely an
inconvenience:
<script>
import { enhance } from '$app/forms'
let form = $page
</script>
<form method="POST" action="?/update_profile" use:enhance>
<input name="name" value={form?.user?.name || ''} />
<textarea name="bio">{form?.user?.bio || ''}</textarea>
<button>Save</button>
</form>
{#if form?.success}
<p>Profile updated successfully!</p>
{:else if form?.message}
<p class="error">{form.message}</p>
{/if}
The form data is processed server-side, and only what you explicitly return gets sent back to the client.
You can even combine form actions with progressive enhancement using
the enhance
function, so it works without JS and gets better with JS
enabled. Itās a win-win!
Server Actions vs. Load Functions: When to use what
This is something I wish Iād explained in my original post:
- Load functions are for getting data to render your page
- Form actions are for changing data based on user input
Think of it like this:
- Load: āHereās the data you need to show the pageā
- Actions: āHereās what happens when the user submits the formā
If youāre doing data mutations (create, update, delete), you should almost always use form actions now, not load functions!
Super Quick Performance Tip
One last thing - when using both server and client load functions, watch out for this:
// +page.ts
export const load = async ({ parent, data }) => {
// ā ļø This creates a loading waterfall
await parent()
// Client-side stuff...
return {
...data,
clientStuff: doSomethingOnClient(data),
}
}
That await parent()
can create a loading waterfall. Only use it when
you actually need the parent data - otherwise, youāre just slowing
things down for no reason.
Conclusion
So there you have it. My apology tour for that old post! Hereās what I should have said:
- Never, ever return sensitive data from server load functions
- Be explicit about what you return - pick the fields you need, donāt return whole objects
- Use form actions for data mutations - theyāre way more secure
- Consider modern auth libraries like Lucia instead of rolling your own
- Check your page source to see what data is actually being exposed
Remember, anything your server sends to the client is essentially public information. Treat it accordingly!
Bonus Debugging Trick
Want to see exactly what data your server is sending to the client? Try these:
- Append
/__data.json
to any route URL - Use the Network tab in your browser devtools to see the responses
- View the page source and search for
__data
to see whatās embedded
This makes it super obvious whatās being exposed - if you see something there that shouldnāt be public, fix your server load functions pronto!
Thatās all for today. Sorry for any confusion the old post caused. The SvelteKit ecosystem evolves fast, and sometimes old posts donāt age well. Stay secure out there! āļø
There's a reactions leaderboard you can check out too.