Introduction and topics overview
Today we're going to be talking about data fetching in SvelteKit. We're going to go through some conceptual high-level stuff on how this works, talk about layouts and how those edge cases kind of filter into the data fetching, and then finally we're going to go through a code example which shows all this stuff off. I have it linked down below; it's fully open source, you're free to take a look at that. I'll have timestamps on this video so you can kind of jump around to the parts that you need to see. And yeah, let's get into it.
Mental model: where code runs and resource types
The first thing I want to talk about here is going to be the mental model of data fetching that I sort of use. SvelteKit is a meta framework, so obviously it exists on both the server and the client, and there are a couple places where this gets a little bit confusing so I wanted to really kind of visualize this out and show you what you can and can't do in each place.
The two resources we're going to be talking about mostly today are going to be the database and an external public API. These two are really kind of stand-ins: the database is any backend resource which can only be accessed on your server because it needs environment variables or a private connection, and the external public API is some external service like maybe a Go backend for your application or some other random data API that you're fetching which you can do from the client — it's publicly available, maybe rate-limited or you need to pass an API header, but it can also be done from the client.
What I want to talk about here is where all of these four pieces live and what they can and can't fetch with regards to these two. The first one we'll talk about is the +page.server.ts and specifically this is the load function. We're not going to get really into form actions in this video; there's a lot of good resources on those already out there and they might be worth a dedicated video in the future, but for today we're just talking about the loading section.
In this case the +page.server.ts is able to fetch from the database and from an external public API, so this can do both of these things. The next thing would be the +server.ts — this is where we can expose normal HTTP endpoints like GET or POST requests. This is also able to fetch from the database and from an external public API.
Then it starts to get a little weirder because we have our +page.ts file here. This +page.ts exists right here because if you're running SvelteKit in the normal default mode, whenever you go to a page this +page.ts will actually be run client-side after navigation, so it can run on both the client and the server — it's called a universal function. So this cannot connect to the database, but it can connect to an external public API. Finally, of course, we have fetching from the client which will exist squarely on the client: you do maybe an onMount fetch request and that fetch could go out to a +server.ts or it could go out to an external public API, but this obviously can't directly access anything private; it has to fetch out to a private backend service like a +server.ts or some external-facing service.
Two trees: rendering tree vs data tree
Something I heard Rich Harris talking about in an interview a while ago is this idea of SvelteKit having two trees: a rendering tree and a data tree, and I kind of wanted to use that to explain how a lot of these things work because I know when you're new to SvelteKit and you're not used to the way this framework works there can be a lot of confusion with the way all of the different +whatever files work. There are a ton of them.
I'll go through and list these out for both trees: there's the pages, there's the +page.svelte, there's the +page.ts's, there's the +page.server's, there's the layouts — all these things which do very different things but they exist to make up these two different trees. The rendering tree is the tree that actually displays data out to the users; this is where the +page.svelte would live, this is where the +layout.svelte would live, so this is the stuff that actually just displays markup to the end user.
Over here on the data tree side of things this is where the +page.ts would live, the +page.server.ts would live, the +layout.server.ts would live, the +layout.ts, the +server.ts. So over here on the data tree this is where the data fetching actually happens.
Now for this example we won't talk too much about the +server.ts because those are just API endpoints — they're really not part of the SvelteKit data model, they're just API routes so we're not going to talk about that here really. What I want to go over is the load functions and form actions and how those relate to the pages and the layouts.
Let's use this as an example: in this app, in our rendering tree we have a node here and this is going to be the route. These boxes I've drawn are directories; SvelteKit uses file-based routing. So we would have our root directory and then a subdirectory right here. The first thing we'll have in here is within our rendering tree we're going to have a +page.svelte. This +page.svelte is going to result in whenever we go to just the root of our application we're going to display out the page that is marked by this +page.svelte.
We're also going to have a +layout.svelte. This layout.svelte will display at the root here, and since it's a layout it's going to also display for everything — it'll display around all of the children in the rendering tree. So if we go down into /sub, this parent layout.svelte is also going to exist and wrap around this sub. Layouts only work on children, so if we added another layout on /sub root (call it a green layout) and then we add a second sub, that second sub would be wrapped by both of these layout.sveltes: the initial one wrapping everything and then the green one, and then finally the +page.svelte in that node is what we'll actually render out at that route. That's how the rendering tree works: pages create the actual pages you see and layouts wrap pages.
Data tree mapping and cascading layout data
Getting over into the data tree, this is one thing that gets a little more complicated but it's why it's so important to visualize this as two different trees. We have the rendering tree creating the markup you see on screen and a parallel tree existing in the exact same directory. You can imagine it exists no matter what because those trees' shape is defined by the file structure. Even if I don't have any of these files created, we just have the markup, this data tree still exists — it's just not in use. You can always imagine those two trees are exactly the same; it's just a question of what we stick into each node.
The first thing we're going to create is a load function within our +page.server.ts on the root of the data tree. Whenever we're talking about the +page.server.ts or the +page.ts these map to the corresponding node in the other tree. So this data node and this rendering node are mapped together and the +page.server in the data tree maps to the +page.svelte in the rendering tree. The result of the load function in +page.server.ts will be available in that +page.svelte but it will not be available in other layout.sveltes or in other pages because they're not in the same place.
Another thing we can add is a +page.ts. This makes things a little different because it's a universal loader and we are able to have a +page.server.ts and a +page.ts existing in the same exact file path. If this happens you do have to add an extra step: the data from the +page.server is normally passed directly into the +page.svelte via the exported data, but since we have this +page.ts we're actually going to have to pass the data into the +page.ts and then from the +page.ts into our +page.svelte. There's one extra step there.
Now I'm going to add a +layout.server.ts. This is where things get trickier because the +layout.server runs similarly to the layout.svelte as in it's going to cascade all the way down the tree from here. The data from this +layout.server.ts is going to be available in all of these sub-roots. Within the load function itself we can await the parent data, and if we get the parent data we'll be able to, within a child +page.server.ts file, get the data from this +layout.server.ts. The important thing is the data from the layout.server cascades down the file system tree and is available to descendants; likewise a +layout.ts would do the same for public API data but cannot access private server-only resources.
So the layout will cascade down the tree and that is the key principle: you can return data from layout.server and descendants can await parent() to read it and extend it or use it.
Code walkthrough: example project and client fetching
Now that we've talked about how all this stuff works, let's go through the code example. This is the example application I built out for this. It's basically just a ton of different data sources that we're fetching. I open-sourced the project and linked it; there's a getting started in there. I ended up needing a database to really show off the database stuff and I used Turso because it's free and quick. I usually use Supabase for my projects, but in this case SQLite felt better and it was the first thing I saw on the Drizzle docs.
You can run this example locally. We're going to start with fetching from the client. The syntax to fetch from the client is simple: I'm doing it in an effect which is basically onMount in Svelte. Whenever we mount this page we're going to fetch /to/api, get the data type it, and then just console.log. If we inspect the console from +page.svelte we'll have our little user todos in there. This is not the preferred way to do data fetching; the other ways we'll show are better, but it's an option.
This is a good way to transition into the +server.ts routes. We can define API endpoints which are entirely server-side with +server.ts. In this case I'm going directly into my database from this API endpoint and returning it as JSON, then calling it from the client. We can also call these API endpoints from our load functions and form actions, which allows some cool patterns especially if you bring another framework like Hono in, but that's beyond this video's scope.
The next data fetching thing I want to talk about are sample todos and sample CSS colors. One of these is from a database and one is from an external API. If we go into our +page.server.ts remember this exists squarely on the server. In the load function we're grabbing user todos from the database and we're also fetching sample data from a public API. You can see console.logging the data in the terminal. Since we're using the special fetch from the load function this will be properly cached and it'll do the deduping on the backend — there's a lot of great stuff SvelteKit does under the hood to make this work.
Another thing important to talk about is streaming some data in. This is something you can do on platforms that support it: Vercel does, I don't think raw AWS Lambda does. That means you can't do it on Netlify or raw lambdas, which is unfortunate, but effectively the way this works is instead of just returning data we can return promises from our load function. If we return a promise, like delaying 500 milliseconds and then generating random words, this will arrive into our +page.svelte as a promise. In the markup we await data.randomStrings and provide a loading state, then when we get the array of strings we print them. This is extremely useful to instantly get important content for SEO and then stream heavier content like graphs or stock data later.
Passing data between +page.server, +page.ts, layouts and wrap-up
On this page we also have sample CSS colors and that's where the +page.ts comes in. Remember the +page.ts exists on the boundary of server and client, so it can fetch from public APIs but not from the database. I'm fetching a small external API to get random CSS color names. We're typing the data and then going through the step of passing data into the +page.svelte because when we have both +page.server.ts and +page.ts, the data from +page.server.ts won't be injected directly into +page.svelte — we have to pass it via the +page.ts. So I'm grabbing random strings, grabbing sample todos, and returning all of this down to the client. Random strings is a promise and we can pass promises down through pages.
In +page.svelte the syntax in SvelteKit 1.0/3.0 era is to use $props and deconstruct data; you'll have layout data, sample CSS colors, sample todos, and random strings, then print the markup. The fake layout data I used comes from +layout.server.ts; I hard-coded some data there and returned it from the server function. The layout.server runs on the server and the data from that layout.server.ts is available in descendants. You can get the layout data in a child page.server by calling parent() and destructuring it.
For example, in a child +page.server.ts you can call const { fakeLayoutData } = await parent(); and then use it or return derived values like fakeLayoutData.length. Then in the child +page.svelte you can render data.fakeLayoutData.length. Page.server.ts only exists for nodes at the same part of the tree, so data returned from a page.server at one node will not be available at a different node unless it's provided via a layout.server and passed down.
I think that covers most of what I wanted to go over today when it comes to data fetching. I know this was quite a long video but we went over a ton of important stuff. The codebase is linked down below with all the different examples and should be a good place to start. I'll also link images to the diagrams we made because those are useful. If you enjoyed this content, like and subscribe; we'll be talking much more about SvelteKit in the future. Thank you for watching and have a great day.