Fetch Google Sheets Data in SvelteKit
Use SvelteKit server load functions to pull Google Sheet data at request time or at build time — without exposing your API key to the browser.
SvelteKit's +page.server.ts load functions run exclusively on the server. That makes them a
natural fit for fetching from SheetsAPI: your API key never touches the
browser, the data arrives pre-rendered in the HTML, and Svelte's reactivity handles the rest.
This guide walks through fetching sheet data in a SvelteKit app — from a basic load function to URL-driven filtering and static site generation.
How SvelteKit server loads work
A +page.server.ts file exports a load function that SvelteKit calls on the server before
rendering the page. Whatever you return becomes the data prop your +page.svelte receives.
Because it runs server-side, you can safely read environment variables, call private APIs, and
return shaped data without any of that logic reaching the client.
src/routes/products/
+page.server.ts ← runs on the server, calls SheetsAPI
+page.svelte ← receives `data`, renders HTML
Environment setup
Add your keys to .env (never commit this file):
GKIT_USER_KEY=YOUR_USER_KEY
GKIT_API_KEY=sk_your_key_here
SvelteKit exposes server-only env vars through $env/static/private or $env/dynamic/private.
Use static/private when the values are known at build time (the common case):
// src/lib/server/sheets.ts
import { GKIT_USER_KEY, GKIT_API_KEY } from "$env/static/private";
const BASE_URL = "https://sheetsapi.gkit.mreshank.com/api/spreadsheets";
export type SheetsMeta = {
total: number;
limit: number;
offset: number;
};
export type SheetsResponse<T> = {
data: T[];
meta: SheetsMeta;
};
type QueryOptions = {
search?: string;
sort?: string;
limit?: number;
offset?: number;
fields?: string[];
};
export async function fetchSheet<T>(
sheetName: string,
options: QueryOptions = {},
fetchFn: typeof fetch = fetch
): Promise<SheetsResponse<T>> {
const params = new URLSearchParams();
if (options.search) params.set("search", options.search);
if (options.sort) params.set("sort", options.sort);
if (options.limit != null) params.set("limit", String(options.limit));
if (options.offset != null) params.set("offset", String(options.offset));
if (options.fields?.length) params.set("fields", options.fields.join(","));
const url = `${BASE_URL}/${GKIT_USER_KEY}/${sheetName}?${params}`;
const res = await fetchFn(url, {
headers: { Authorization: `Bearer ${GKIT_API_KEY}` },
});
if (!res.ok) {
throw new Error(`SheetsAPI ${res.status}: ${sheetName}`);
}
return res.json() as Promise<SheetsResponse<T>>;
}Placing this in src/lib/server/ means SvelteKit will refuse to import it from client code —
a helpful guard against accidentally leaking your API key.
The fetchFn parameter is there so you can pass SvelteKit's own fetch (which handles
relative URLs and SSR correctly) from inside load functions.
Basic load function
// src/routes/products/+page.server.ts
import type { PageServerLoad } from "./$types";
import { fetchSheet } from "$lib/server/sheets";
type Product = {
name: string;
price: string;
category: string;
};
export const load: PageServerLoad = async ({ fetch }) => {
const { data: products, meta } = await fetchSheet<Product>(
"Products",
{ sort: "name", limit: 100 },
fetch
);
return { products, meta };
};<!-- src/routes/products/+page.svelte -->
<script lang="ts">
import type { PageData } from "./$types";
export let data: PageData;
</script>
<h1>Products ({data.meta.total})</h1>
<ul>
{#each data.products as product}
<li>{product.name} — ${product.price}</li>
{/each}
</ul>No onMount, no fetch in the browser, no loading spinner. The HTML arrives with data already
baked in.
Passing URL search params to SheetsAPI
When you want users to filter or sort via the URL — e.g. /products?category=Audio&sort=-price
— read url.searchParams in the load function and forward them to SheetsAPI:
// src/routes/products/+page.server.ts
import type { PageServerLoad } from "./$types";
import { fetchSheet } from "$lib/server/sheets";
type Product = { name: string; price: string; category: string };
export const load: PageServerLoad = async ({ fetch, url }) => {
const category = url.searchParams.get("category");
const sort = url.searchParams.get("sort") ?? "name";
const limit = Number(url.searchParams.get("limit") ?? 50);
const offset = Number(url.searchParams.get("offset") ?? 0);
const { data: products, meta } = await fetchSheet<Product>(
"Products",
{
search: category ? `category:${category}` : undefined,
sort,
limit,
offset,
},
fetch
);
return { products, meta, category, sort };
};SvelteKit automatically re-runs the load function when the URL changes — so a <select> that
pushes ?category=Audio to the URL triggers a server-side re-fetch with no extra client code.
Prerendering for static sites
If your sheet data changes infrequently, you can prerender the page at build time. Add one line:
// src/routes/products/+page.server.ts
export const prerender = true;
// ... rest of the load function unchangedSvelteKit calls the load function during vite build, bakes the data into static HTML, and
deploys the result to any CDN. The sheet is only read at build time — not on every request.
For content that changes daily, set up a rebuild webhook or a scheduled CI job rather than relying on visitors to see fresh data.
Global data with +layout.server.ts
Some data belongs to every page: navigation items, a site-wide announcement, a list of
categories for a sidebar filter. A +layout.server.ts load function runs once and makes its
return value available to all child routes through $page.data or by passing it as a prop.
// src/routes/+layout.server.ts
import type { LayoutServerLoad } from "./$types";
import { fetchSheet } from "$lib/server/sheets";
type NavItem = { label: string; url: string };
export const load: LayoutServerLoad = async ({ fetch }) => {
const { data: navItems } = await fetchSheet<NavItem>(
"Navigation",
{ sort: "order", fields: ["label", "url"] },
fetch
);
return { navItems };
};<!-- src/routes/+layout.svelte -->
<script lang="ts">
import type { LayoutData } from "./$types";
export let data: LayoutData;
</script>
<nav>
{#each data.navItems as item}
<a href={item.url}>{item.label}</a>
{/each}
</nav>
<slot />Your content team now manages the site navigation from a Google Sheet. No deploys needed when
a link changes — just trigger a rebuild or use export const prerender = false if you want
nav to refresh on every request.
Error handling
Wrap the fetchSheet call and use SvelteKit's error helper to return a proper HTTP error
response instead of crashing the page:
// src/routes/products/+page.server.ts
import { error } from "@sveltejs/kit";
import type { PageServerLoad } from "./$types";
import { fetchSheet } from "$lib/server/sheets";
type Product = { name: string; price: string; category: string };
export const load: PageServerLoad = async ({ fetch }) => {
try {
const { data: products, meta } = await fetchSheet<Product>(
"Products",
{},
fetch
);
return { products, meta };
} catch (err) {
const message = err instanceof Error ? err.message : "Unknown error";
throw error(502, `Could not load products: ${message}`);
}
};SvelteKit renders your +error.svelte component with the status and message, so users see a
clean error page rather than a broken layout.
When to use dynamic vs. static loading
| Scenario | Approach |
|---|---|
| Data changes frequently (orders, live inventory) | prerender = false (default), SSR on each request |
| Data changes a few times a day (blog posts, docs) | SSR + HTTP cache headers or a CDN layer |
| Data changes rarely (nav, pricing) | prerender = true, rebuild on change |
| Data is user-specific | Server load with session, never prerender |
SheetsAPI works equally well in all four scenarios — the distinction is in how SvelteKit decides when to call your load function, not in how the API behaves.
Next steps
You now have a clean pattern: SheetsAPI handles Google authentication and exposes a typed REST endpoint; your SvelteKit load function calls it server-side and passes the data to your components. The API key never leaves the server, the data arrives pre-rendered, and your Google Sheet remains the single source of truth.
Connect your first sheet → or read the SheetsAPI quickstart for a full walkthrough of the dashboard setup.