Why the Load Event Provides Its Own fetch
Every SvelteKit load function receives an event object, and among its properties is one that looks almost too familiar to deserve a second glance: fetch. It is a function that makes HTTP requests. You already know how to use it. What could there possibly be to explain?
Quite a lot, as it turns out.
The fetch provided by the load event is not the browser’s native fetch. It is a specialized version built by SvelteKit that carries five distinct capabilities not present in the global version. Using the global fetch inside a load function is not just a stylistic mistake. In some scenarios it produces incorrect behavior. In others it silently undermines the very thing SvelteKit is trying to do for you: delivering a fully rendered page on the first request without redundant network activity.
This article builds a precise understanding of why SvelteKit replaces fetch, what each of those five capabilities does, and how they interact in a real application. It also covers the distinction between fetching from external APIs and from your own internal +server.ts routes, because those two scenarios have different characteristics worth understanding. By the end you will have a clear mental model of what happens to a fetch call from the moment a load function runs on the server through the moment the same page becomes interactive in the browser.
What Global fetch Cannot Do
To understand why SvelteKit needs its own fetch, consider what happens when a user navigates to a page that loads data.
On the server, SvelteKit runs your load function, calls any fetch requests inside it, assembles the resulting data, renders the component tree to HTML, and sends the result to the browser. The browser receives a fully formed page and displays it immediately. That is the SSR promise: fast first paint, content visible before JavaScript executes.
Then the JavaScript bundle arrives. SvelteKit needs to hydrate the page, turning the static HTML into a live, interactive Svelte application. Hydration requires that the client-side component tree matches the server-rendered HTML exactly. If the data used during hydration differs from the data used during server rendering, you get a mismatch.
Now consider what happens if your load function uses the native global fetch during SSR. The server fetches data, renders HTML with it, and sends that HTML to the browser. When the client bundle runs and tries to hydrate, it needs that same data. If it re-runs the load function to get it, that means another network request from the browser. Two requests for the same data: one on the server, one on the client. That is the double-fetch problem.
Even ignoring hydration, native fetch on the server does not know anything about SvelteKit’s internal routing. A relative URL like /api/products means nothing in Node.js, where there is no concept of a current origin. You would have to hardcode an absolute URL, which is either fragile in development or requires injecting environment variables for the current host. And if that /api/products endpoint is one of your own +server.ts files, you would be making an actual HTTP request to yourself when SvelteKit could simply call the handler directly without ever touching the network.
These are the problems SvelteKit’s fetch solves. Each one corresponds to a distinct capability.
How It Works
Before walking through each capability, it helps to understand where fetch sits in the load function lifecycle.
A load function is fundamentally a data acquisition function. SvelteKit calls it at the right moment, in the right context, with the right tools. The load event object bundles together everything the function needs: route parameters, URL information, cookies, the parent layout’s data, the platform environment, and fetch. You do not import fetch or instantiate it. You destructure it from the event.
// The fetch you use comes from the event, not from the global scope.
export const load: PageServerLoad = async ({ params, fetch }) => {
const response = await fetch(`/api/articles/${params.slug}`)
const article = await response.json()
return { article }
} This is intentional. SvelteKit controls the lifetime of that fetch instance. It knows when the load function was called, whether it is running on the server or the client, and what requests have already been made. That context is what enables the five capabilities.
When the browser receives the page, the serialized data is already embedded in the HTML. The client-side SvelteKit runtime reads that data out of the page rather than re-fetching it. Load functions run only once for the initial request.
The Five Superpowers of event.fetch
The five Superpowers capabilities are distinct enough that each deserves its own section, but it helps to see them together first before diving into each one.
SP1: Relative URLs Resolve Correctly
On the server, there is no concept of a current page origin. Node.js does not know what hostname your application is running on. If you use native fetch with a relative URL inside a load function during SSR, you get an error because Node’s built-in fetch requires absolute URLs.
SvelteKit’s fetch resolves relative URLs against the current request’s origin automatically. The URL /api/products becomes http://localhost:5173/api/products in development and https://yourdomain.com/api/products in production, without any configuration on your part.
// src/routes/shop/+page.server.ts
import type { PageServerLoad } from './$types'
import { error } from '@sveltejs/kit'
export const load: PageServerLoad = async ({ fetch }) => {
// This works on the server. SvelteKit resolves it against
// the current request origin. Native fetch would throw here
// because '/api/products' is not a valid absolute URL in Node.
const response = await fetch('/api/products')
if (!response.ok) {
error(response.status, 'Failed to load products')
}
const products = await response.json()
return { products }
} This means you write the same URL in development and production. You never concatenate process.env.ORIGIN with a path. The URL you write in the load function is the same URL a browser would use to reach the same endpoint, which is a natural and consistent mental model.
SP2: Credentials Flow Automatically
Authentication in SvelteKit typically involves a session cookie. The browser sends this cookie with every request automatically, but on the server, when you are making a fetch call inside a load function, that cookie is not present on the fetch call unless you explicitly forward it.
SvelteKit’s fetch forwards cookies and authorization headers from the incoming request to same-origin fetch calls inside load functions. This means your API endpoints receive the same authentication context as the original page request, with no manual cookie extraction or header forwarding required.
// src/routes/account/+page.server.ts
import type { PageServerLoad } from './$types'
export const load: PageServerLoad = async ({ fetch }) => {
// SvelteKit forwards the session cookie from the original
// browser request to this fetch call automatically.
// The /api/account endpoint can read it and verify the session.
const response = await fetch('/api/account/profile')
if (response.status === 401) {
// Session missing or expired. Return null rather than throwing
// here so the page can render a login prompt.
return { profile: null }
}
const profile = await response.json()
return { profile }
} This automatic credential forwarding only applies to same-origin requests. When you fetch from an external API on a different domain, no cookies or authorization headers are forwarded. That is the correct behavior: you would not want your session cookies sent to a third-party service.
If you need to authenticate a cross-origin request, you supply the credentials explicitly in the fetch call, exactly as you would with native fetch.
// src/routes/weather/+page.server.ts
import type { PageServerLoad } from './$types'
import { WEATHER_API_KEY } from '$env/static/private'
export const load: PageServerLoad = async ({ params, fetch }) => {
// Cross-origin request to an external API.
// No cookies are forwarded. Credentials are supplied explicitly.
const weatherUrl = new URL('https://api.openweathermap.org/data/2.5/weather')
weatherUrl.searchParams.set('q', params.city)
weatherUrl.searchParams.set('appid', WEATHER_API_KEY)
const response = await fetch(weatherUrl.toString())
if (!response.ok) {
return { weather: null }
}
const weather = await response.json()
return { weather }
} SP3: Internal Routes Are Bypassed
When you fetch a URL that matches one of your own SvelteKit +server.ts endpoints, SvelteKit does not make an actual HTTP request. It calls the route handler function directly, in-process, without touching the network.
This matters in two ways. First, it eliminates an unnecessary network round trip: the serialization, transmission, and deserialization of data that was already in memory. Second, it means your internal API endpoints can use server-only imports such as $env/static/private and internal utilities, and those imports remain safe even when the fetch call looks like a network request from the outside.
// src/routes/api/articles/[slug]/+server.ts
import type { RequestHandler } from './$types'
import { json, error } from '@sveltejs/kit'
import { CMS_TOKEN } from '$env/static/private'
export const GET: RequestHandler = async ({ params }) => {
// This handler is called directly when fetched from within the app.
// It is also callable as a real HTTP endpoint from external clients.
const response = await fetch(`https://cms.example.com/api/articles/${params.slug}`, {
headers: { Authorization: `Bearer ${CMS_TOKEN}` }
})
if (!response.ok) {
error(404, 'Article not found')
}
const article = await response.json()
return json(article)
} // src/routes/blog/[slug]/+page.server.ts
import type { PageServerLoad } from './$types'
import { error } from '@sveltejs/kit'
export const load: PageServerLoad = async ({ params, fetch }) => {
// SvelteKit calls the +server.ts handler directly.
// No HTTP request is made. The handler runs as a function call.
const response = await fetch(`/api/articles/${params.slug}`)
if (!response.ok) {
error(404, 'Article not found')
}
const article = await response.json()
return { article }
} This design lets you build your own API routes as real, callable HTTP endpoints that external clients or mobile apps can use, while also using them internally from load functions without any performance penalty. You get a clean API surface and efficient internal data access from the same route handler file.
SP4: Responses Are Captured for Hydration
This is the capability that prevents the double-fetch problem.
When a load function runs during SSR and calls event.fetch, SvelteKit captures the response. It serializes that response data and embeds it in the HTML page sent to the browser, inside a script block that SvelteKit reads during hydration.
When the browser loads the page and the JavaScript bundle runs, SvelteKit’s client-side runtime intercepts any event.fetch calls that would reproduce a request already made on the server. Instead of making a real network request, it reads the captured response from the embedded page data.
The practical consequence is that your load function code is the same in both environments. You do not write separate server-side and client-side data fetching paths. You write one load function, and SvelteKit handles the difference transparently.
// src/routes/blog/[slug]/+page.ts
// This is a universal load function (no .server). It runs on the server
// for the initial request and on the client during subsequent navigations.
import type { PageLoad } from './$types'
import { error } from '@sveltejs/kit'
export const load: PageLoad = async ({ params, fetch }) => {
// During SSR: SvelteKit makes the request and captures the response.
// During client navigation: SvelteKit makes the request from the browser.
// During hydration of an SSR page: SvelteKit uses the captured response.
// The code here is identical in all three scenarios.
const response = await fetch(`/api/articles/${params.slug}`)
if (!response.ok) {
error(404, 'Article not found')
}
const article = await response.json()
return { article }
} If you used the global fetch here instead of event.fetch, the hydration interception would not occur. The client would make a real network request, the page would momentarily re-render with freshly fetched data, and if that data differs from what was rendered on the server, you get a hydration mismatch. The global fetch bypasses the entire SSR capture mechanism.
SP5: Fetch Is Environment-Aware
The fifth capability is less dramatic but worth naming explicitly: SvelteKit’s fetch works correctly in every environment where load functions run, without any modification.
In Node.js running a production server, it handles the absence of a native global fetch. In the browser, it delegates to the native browser fetch but with the interception layer for hydration. In Vitest or Playwright tests, it can be mocked or intercepted uniformly. In Cloudflare Workers or other edge environments, it respects the platform’s fetch implementation.
You do not need to detect the environment, import polyfills, or write conditional logic. The same event.fetch call works everywhere your load function runs.
Fetching from External APIs vs Internal Routes
The distinction between fetching from an external API and fetching from your own SvelteKit routes deserves explicit attention because the two scenarios have different characteristics.
External APIs are real HTTP endpoints on different servers. The full network stack is involved: DNS resolution, TCP connections, TLS handshakes, request serialization, transmission, response deserialization. For external API calls, you manage authentication with explicit headers, handle rate limiting, deal with network errors, and parse the response format the API owner defines.
Internal routes are your own +server.ts handlers. As described earlier in SP3, SvelteKit bypasses the network for these. The handler function is called directly. The request and response objects are constructed in memory. The only serialization involved is the JSON encoding and decoding you perform in the handler itself.
A practical consequence of this distinction: when you need data from an external service, placing a +server.ts route in front of it creates a useful layer where you can normalize the response, handle errors uniformly, inject authentication without exposing API keys to the client, and cache results.
// src/routes/api/github/repos/+server.ts
// Acts as a thin adapter between external GitHub API and internal consumers.
import type { RequestHandler } from './$types'
import { json, error } from '@sveltejs/kit'
import { GITHUB_TOKEN } from '$env/static/private'
interface GitHubRepo {
id: number
name: string
full_name: string
description: string | null
stargazers_count: number
language: string | null
html_url: string
}
interface NormalizedRepo {
id: number
name: string
description: string
stars: number
language: string
url: string
}
export const GET: RequestHandler = async ({ url }) => {
const username = url.searchParams.get('username')
if (!username) {
error(400, 'username query parameter is required')
}
const response = await fetch(
`https://api.github.com/users/${username}/repos?per_page=10&sort=stars`,
{
headers: {
Authorization: `Bearer ${GITHUB_TOKEN}`,
Accept: 'application/vnd.github.v3+json'
}
}
)
if (!response.ok) {
error(response.status, `GitHub API error: ${response.statusText}`)
}
const repos: GitHubRepo[] = await response.json()
// Normalize to a stable internal shape. If GitHub changes their API,
// only this file needs to change.
const normalized: NormalizedRepo[] = repos.map((repo) => ({
id: repo.id,
name: repo.name,
description: repo.description ?? 'No description provided',
stars: repo.stargazers_count,
language: repo.language ?? 'Unknown',
url: repo.html_url
}))
return json(normalized)
} // src/routes/profile/[username]/+page.server.ts
import type { PageServerLoad } from './$types'
import type { NormalizedRepo } from '$lib/types'
export const load: PageServerLoad = async ({ params, fetch }) => {
// Fetches from our internal adapter route, not directly from GitHub.
// GITHUB_TOKEN never reaches this file or the browser.
// The response is the NormalizedRepo shape we control.
const response = await fetch(`/api/github/repos?username=${params.username}`)
if (!response.ok) {
return { repos: [] as NormalizedRepo[] }
}
const repos: NormalizedRepo[] = await response.json()
return { repos }
} This pattern keeps API keys server-only, gives you a single place to update when the external API changes, and lets the load function work with a shape you designed rather than a shape imposed by a third party.
Practical Example: Fetching a Typed JSON API
The following is a complete working example that combines all five capabilities. It models a product listing page that fetches from an internal route, types the response correctly using TypeScript, handles errors gracefully, and works correctly across SSR, hydration, and client navigation.
Defining the shared types
// src/lib/types/product.ts
export interface Product {
id: string
name: string
price: number
category: string
inStock: boolean
imageUrl: string
}
export interface ProductListResponse {
products: Product[]
total: number
page: number
pageSize: number
} The internal API route
// src/routes/api/products/+server.ts
import type { RequestHandler } from './$types'
import { json, error } from '@sveltejs/kit'
import type { Product, ProductListResponse } from '$lib/types/product'
// Simulates a data source. In a real application this would query
// whatever data store your application uses.
const PRODUCTS: Product[] = [
{
id: '1',
name: 'Mechanical Keyboard',
price: 149,
category: 'peripherals',
inStock: true,
imageUrl: '/images/keyboard.jpg'
},
{
id: '2',
name: 'Curved Monitor',
price: 549,
category: 'displays',
inStock: true,
imageUrl: '/images/monitor.jpg'
},
{
id: '3',
name: 'Ergonomic Chair',
price: 399,
category: 'furniture',
inStock: false,
imageUrl: '/images/chair.jpg'
},
{
id: '4',
name: 'USB-C Hub',
price: 79,
category: 'peripherals',
inStock: true,
imageUrl: '/images/hub.jpg'
},
{
id: '5',
name: 'Webcam 4K',
price: 199,
category: 'peripherals',
inStock: true,
imageUrl: '/images/webcam.jpg'
},
{
id: '6',
name: 'Standing Desk',
price: 699,
category: 'furniture',
inStock: true,
imageUrl: '/images/desk.jpg'
}
]
const PAGE_SIZE = 3
export const GET: RequestHandler = async ({ url }) => {
const category = url.searchParams.get('category')
const pageParam = url.searchParams.get('page')
const page = Math.max(1, parseInt(pageParam ?? '1', 10) || 1)
let filtered = category ? PRODUCTS.filter((p) => p.category === category) : PRODUCTS
const total = filtered.length
const start = (page - 1) * PAGE_SIZE
const products = filtered.slice(start, start + PAGE_SIZE)
if (page > 1 && products.length === 0) {
error(404, 'No products found for this page')
}
const result: ProductListResponse = {
products,
total,
page,
pageSize: PAGE_SIZE
}
return json(result)
} The load function
// src/routes/shop/+page.server.ts
import type { PageServerLoad } from './$types'
import type { ProductListResponse } from '$lib/types/product'
import { error } from '@sveltejs/kit'
export const load: PageServerLoad = async ({ url, fetch }) => {
const category = url.searchParams.get('category')
const page = url.searchParams.get('page') ?? '1'
// Build the internal API URL, forwarding the same query parameters
// the user supplied to the page URL.
const apiUrl = new URL('/api/products', url.origin)
if (category) apiUrl.searchParams.set('category', category)
apiUrl.searchParams.set('page', page)
const response = await fetch(apiUrl.toString())
if (!response.ok) {
error(response.status, 'Failed to load products')
}
const data: ProductListResponse = await response.json()
return {
products: data.products,
total: data.total,
page: data.page,
pageSize: data.pageSize,
selectedCategory: category
}
} The page component
<!-- src/routes/shop/+page.svelte -->
<script lang="ts">
import type { PageProps } from './$types'
let { data }: PageProps = $props()
const categories = ['peripherals', 'displays', 'furniture']
const totalPages = $derived(Math.ceil(data.total / data.pageSize))
function buildPageUrl(page: number): string {
const params = new URLSearchParams()
if (data.selectedCategory) params.set('category', data.selectedCategory)
params.set('page', String(page))
return `/shop?${params.toString()}`
}
function buildCategoryUrl(category: string | null): string {
if (!category) return '/shop'
return `/shop?category=${category}`
}
</script>
<section class="shop">
<h1>Shop</h1>
<nav class="category-filter" aria-label="Filter by category">
<a href={buildCategoryUrl(null)} class:active={!data.selectedCategory}>All</a>
{#each categories as category (category)}
<a href={buildCategoryUrl(category)} class:active={data.selectedCategory === category}>
{category}
</a>
{/each}
</nav>
<p class="result-count">
Showing {data.products.length} of {data.total} products
{#if data.selectedCategory}in {data.selectedCategory}{/if}
</p>
<ul class="product-grid">
{#each data.products as product (product.id)}
<li class="product-card">
<img src={product.imageUrl} alt={product.name} />
<h2>{product.name}</h2>
<p class="price">${product.price}</p>
{#if !product.inStock}
<span class="badge out-of-stock">Out of stock</span>
{/if}
</li>
{/each}
</ul>
{#if totalPages > 1}
<nav class="pagination" aria-label="Page navigation">
{#if data.page > 1}
<a href={buildPageUrl(data.page - 1)}>Previous</a>
{/if}
<span>Page {data.page} of {totalPages}</span>
{#if data.page < totalPages}
<a href={buildPageUrl(data.page + 1)}>Next</a>
{/if}
</nav>
{/if}
</section> Notice that in the shop route the +page.svelte component has no fetch calls and no loading state management. Data arrives through $props() as a completed, typed object.
The $derived rune computes the total page count reactively from the data without any manual re-computation logic.
Navigation between pages and categories uses plain anchor links, which trigger SvelteKit’s client-side navigation and re-run the load function on the client using event.fetch, with all five superpowers still in effect.
Common Mistakes and Anti-Patterns
Using the global fetch instead of event.fetch
// Avoid
import type { PageLoad } from './$types'
export const load: PageLoad = async ({ params }) => {
// This uses the global fetch, not SvelteKit's enhanced version.
// During SSR it may fail with relative URLs. During hydration
// it bypasses the captured response and makes a second network request.
const response = await fetch(`/api/articles/${params.slug}`)
return { article: await response.json() }
} // Preferred
import type { PageLoad } from './$types'
export const load: PageLoad = async ({ params, fetch }) => {
// Destructure fetch from the event. This is SvelteKit's enhanced version.
const response = await fetch(`/api/articles/${params.slug}`)
return { article: await response.json() }
} The global fetch is not aware of SvelteKit’s lifecycle. It cannot resolve relative URLs on the server, does not forward credentials, does not call internal route handlers directly, and does not participate in SSR response capture. The fix is always to destructure fetch from the load event rather than calling the global.
Ignoring the response status
// Wrong
export const load: PageServerLoad = async ({ params, fetch }) => {
const response = await fetch(`/api/products/${params.id}`)
// If the API returns 404 or 500, response.json() may throw or return
// an error object. The page renders with garbage data or crashes silently.
const product = await response.json()
return { product }
} // Correct
import { error } from '@sveltejs/kit'
export const load: PageServerLoad = async ({ params, fetch }) => {
const response = await fetch(`/api/products/${params.id}`)
if (!response.ok) {
// Throw a SvelteKit error. This renders the nearest +error.svelte
// boundary with the appropriate HTTP status code.
error(response.status, `Product ${params.id} could not be loaded`)
}
const product = await response.json()
return { product }
} response.ok is true for 2xx status codes. Any other status means something went wrong. Calling error() from @sveltejs/kit terminates the load and renders the error boundary, which is the correct behavior when data required to render the page is unavailable.
Constructing URLs with string concatenation
// Wrong: fragile and injection-prone
export const load: PageServerLoad = async ({ url, fetch }) => {
const search = url.searchParams.get('q')
// String concatenation does not encode special characters.
// A search query containing '&' or '?' will corrupt the URL.
const response = await fetch(`/api/search?q=${search}&page=1`)
return { results: await response.json() }
} // Correct: use the URL constructor for safe parameter encoding
export const load: PageServerLoad = async ({ url, fetch }) => {
const search = url.searchParams.get('q') ?? ''
const apiUrl = new URL('/api/search', url.origin)
apiUrl.searchParams.set('q', search)
apiUrl.searchParams.set('page', '1')
const response = await fetch(apiUrl.toString())
return { results: await response.json() }
} The URL constructor and URLSearchParams handle encoding automatically. Special characters in search queries, slugs with unicode, and any user-supplied values are handled correctly without manual encoding.
Performance and Scaling Considerations
The SSR response capture mechanism works per-request. Each incoming request to your SvelteKit application triggers its own set of load functions, each with their own fetch instance. Captured responses are not shared across requests or cached beyond the lifetime of a single request.
If a load function fetches data that is expensive to compute and identical for all users, such as a list of product categories or a site-wide configuration, fetching it on every SSR request is wasteful. The solution is caching at the API layer, not in the load function. Your +server.ts route handler can implement an in-memory cache or use HTTP cache headers to let intermediaries cache the response.
// src/routes/api/categories/+server.ts
import { json } from '@sveltejs/kit'
import type { RequestHandler } from './$types'
// In-memory cache shared across requests to the same server instance.
// This is appropriate for data that changes rarely and is identical for all users.
let categoriesCache: string[] | null = null
let cacheSetAt = 0
const CACHE_TTL_MS = 60 * 1000 // one minute
export const GET: RequestHandler = async () => {
const now = Date.now()
if (categoriesCache && now - cacheSetAt < CACHE_TTL_MS) {
return json(categoriesCache)
}
const response = await fetch('https://api.example.com/categories')
const categories: string[] = await response.json()
categoriesCache = categories
cacheSetAt = now
return json(categories)
} For user-specific data, caching at the server level is not appropriate since each user’s response is different. The SSR capture mechanism already prevents double-fetching for any given user’s page load, so no additional optimization is needed there.
When multiple load functions in a route hierarchy each fetch independent data, SvelteKit runs layout and page load functions with concurrency where possible. You do not need to manually parallelize requests across different load files. Within a single load function, if you need multiple independent requests, use Promise.all to avoid sequential fetching.
// src/routes/dashboard/+page.server.ts
import type { PageServerLoad } from './$types'
export const load: PageServerLoad = async ({ fetch }) => {
// These requests are independent. Running them in parallel
// reduces total load time to the duration of the slowest request
// rather than the sum of all requests.
const [statsResponse, activityResponse] = await Promise.all([
fetch('/api/dashboard/stats'),
fetch('/api/dashboard/activity')
])
if (!statsResponse.ok || !activityResponse.ok) {
return { stats: null, activity: [] }
}
const [stats, activity] = await Promise.all([statsResponse.json(), activityResponse.json()])
return { stats, activity }
} When NOT to Use This Pattern
The event.fetch pattern is the right tool for loading data that a page needs before it can render. It is not the right tool for everything.
Client-only interactions do not belong in load functions. If a user action triggers a data refresh, such as clicking a refresh button or submitting a search form, that interaction is handled by the component using standard fetch, form actions, or SvelteKit’s invalidate function. The load function provides the initial data; user-initiated updates are a separate concern.
Real-time data such as WebSocket connections or server-sent events cannot be modeled as a single fetch call in a load function. Load functions run once per navigation. For real-time features, establish the connection in a $effect inside the component and manage the lifetime of the connection there.
Very large datasets should not be fetched entirely in a load function if only a portion will be displayed. Use pagination from the start, as demonstrated in the practical example, and fetch only the data the current view requires.
Optional or secondary data that a page can render without should not block the initial render by being fetched in a load function. SvelteKit supports streaming deferred data using promises, which allows a page to render with critical data immediately while secondary data loads in the background. That pattern is covered in a dedicated article in this series.
Conclusion
SvelteKit’s event.fetch looks like a familiar function but carries five capabilities that fundamentally change how data loading works across SSR, hydration, and client navigation. Relative URLs resolve correctly in Node.js. Credentials forward automatically to same-origin endpoints. Internal route handlers are called directly without a network round trip. Responses are captured and embedded in the page payload, eliminating the double-fetch problem during hydration. And the whole mechanism is environment-aware, working correctly in Node, the browser, and edge runtimes.
Using the global fetch in a load function bypasses all of these capabilities. It is not a stylistic choice. It is a category error: using a tool that was designed for a different context. The load event gives you fetch for a reason, and that reason is the entire network behavior of your SvelteKit application.
The practical implications are straightforward: always destructure fetch from the load event, check response.ok before parsing, use the URL constructor for building parameterized URLs, place cross-origin API calls behind internal +server.ts routes to keep credentials server-only, and rely on Promise.all for independent parallel requests within a single load function.
Key Takeaways
Key takeaways:
- Always destructure
fetchfrom the load event object. The globalfetchdoes not have any of SvelteKit’s five capabilities. - Relative URLs work in load functions because SvelteKit resolves them against the current request origin automatically.
- Same-origin requests forward cookies and authorization headers from the incoming browser request without any manual forwarding.
- Fetching an internal
+server.tsroute bypasses the network entirely. The handler function is called directly. - SSR response capture prevents the double-fetch problem. Data fetched on the server is embedded in the page and reused on the client during hydration.
event.fetchworks identically in Node, the browser, and edge runtimes. No environment detection or polyfills are needed.- Always check
response.okand useerror()from@sveltejs/kitto handle failed responses correctly.
Further Reading
- SvelteKit Routing Documentation — official reference for
+server.tsroute handlers - SvelteKit Load Documentation — official reference for load function event properties including
fetch