Guide
SvelteKit fundamentals explained
A regional publisher wants editors to draft articles, preview SEO metadata, and schedule posts — with public guide pages that load fast on mobile and rank in search. Hand-rolling Svelte with a separate Express API means two repos, duplicate validation, and hydration glue. SvelteKit is the official full-stack framework for Svelte: file-based routes, load functions that run on server or client, progressive- enhancement form actions, and adapters that compile the same app to Node, static hosting, Vercel, or Cloudflare Workers. Because Svelte compiles components to lean JavaScript, SvelteKit sites often ship smaller client bundles than comparable Next.js or Nuxt stacks for content-heavy products. This guide covers SvelteKit’s routing conventions, data loading, mutations, hooks, rendering modes, a Harbor Archive publishing portal worked example, a framework decision table, common pitfalls, and a production checklist alongside our Vite fundamentals guide and SSR vs CSR rendering guide.
What SvelteKit is (and how it extends Svelte)
Svelte is a UI compiler. SvelteKit is the application framework that answers routing, data fetching, deployment, and SEO:
- Routing — directories under
src/routes/map to URLs; special files (+page.svelte,+layout.svelte) define UI shells. - Data loading —
+page.server.tsand+page.tsexportloadfunctions whose return values become page props. - Mutations —
actionsin+page.server.tshandle form POSTs with or without JavaScript. - Rendering — per-route
export const prerender,ssr, andcsrflags mix static, server-rendered, and client-only pages. - Build output —
adapter-node,adapter-static,adapter-vercel, and others target your host without rewriting the app.
SvelteKit 2 (current major line) builds on
Vite for dev
HMR and production bundling. TypeScript is first-class; run
npm create svelte@latest and pick the skeleton with TypeScript and
ESLint from day one.
File-based routing conventions
SvelteKit uses route groups and special filenames instead of a central router config:
src/routes/+page.svelte— homepage at/src/routes/guides/[slug]/+page.svelte— dynamic segment; access$page.params.slugsrc/routes/(app)/dashboard/+layout.svelte— layout shared by nested routes; parentheses create a group without affecting the URLsrc/routes/+layout.server.ts— root layout load (session user, feature flags)src/routes/+error.svelte— error boundary UI
Layouts nest: child +page.svelte renders inside parent
+layout.svelte via <slot /> (Svelte 4) or
{@render children()} (Svelte 5). Use +layout.ts when
layout data must run on both server and client (e.g. theme preference from a cookie
readable in the browser).
Route parameters and matchers
Optional segments use double brackets: [[lang]]/about. Rest parameters
capture tails: docs/[...path]. Custom matchers in
src/params/slug.ts reject invalid slugs before load runs, saving
database round-trips on garbage URLs.
Load functions: server, universal, and dependencies
The load function is SvelteKit’s data layer. Two file types matter:
+page.server.ts— runs only on the server. Safe for database queries, private API keys, and reading HttpOnly cookies. Return value is serialized to the client (watch payload size).+page.ts— universal load. Runs on server during SSR and again on client navigations. Use for public fetches that must re-run when the user clicks an in-app link.
// src/routes/guides/[slug]/+page.server.ts
import { error } from '@sveltejs/kit'
import { db } from '$lib/server/db'
export async function load({ params }) {
const post = await db.guide.findUnique({ where: { slug: params.slug } })
if (!post) throw error(404, 'Guide not found')
return { post }
}
In +page.svelte, access data with the data prop (SvelteKit
2 passes it explicitly). Use export let data or destructuring in
<script>.
Invalidation and streaming
After a mutation, call invalidate('app:guides') or
invalidateAll() from $app/navigation to re-run matching
loads. For slow secondary data, return a Promise from
load and use {#await} in the template — SvelteKit
streams deferred chunks when supported.
Depends on parent data: export const load = async ({ parent }) => { const { user } = await parent(); ... }
chains layout auth into page queries without duplicating session lookups.
Form actions and progressive enhancement
SvelteKit colocates mutations with pages via actions in
+page.server.ts:
export const actions = {
publish: async ({ request, locals }) => {
const data = await request.formData()
const title = String(data.get('title') ?? '').trim()
if (!title) return fail(400, { title, missing: true })
await db.guide.create({ data: { title, authorId: locals.user.id } })
return { success: true }
}
}
In the page, <form method="POST" action="?/publish"> works
without JavaScript — critical for resilience and accessibility. With JS
enabled, use use:enhance for optimistic UI and no full-page reload.
The form store from $app/forms exposes
form.message and validation errors returned via fail().
Prefer actions over client-side fetch for editor workflows: CSRF
protection is built in, secrets stay server-side, and the same handler serves
no-JS and enhanced clients. Reserve +server.ts route handlers for
webhooks, JSON APIs consumed by third parties, or SSE streams.
Hooks, locals, and environment
src/hooks.server.ts runs on every request:
handle— authenticate sessions, setevent.locals.user, add security headers, short-circuit unauthorized routes.handleFetch— rewrite internalfetchduring SSR (e.g. attach service token when the server calls your own API).handleError— report errors to logging without leaking stack traces to users.
Use $env/static/private for server-only secrets and
$env/static/public for values safe in the browser. Never import
private env modules into +page.svelte — the build will fail if
you try, but dynamic imports can still leak; keep DB clients in
$lib/server/.
Rendering modes and adapters
Per-route exports control how HTML is produced:
| Setting | Effect | Typical use |
|---|---|---|
prerender = true |
Static HTML at build time | Marketing pages, public guides with known slugs |
ssr = true (default) |
Server renders HTML per request | Personalized dashboards, auth-gated content |
csr = false |
Disable client-side rendering for the page | Rare; mostly embedded widgets or legacy constraints |
trailingSlash in config |
Canonical URL shape | Match CDN or static host expectations |
Adapters in svelte.config.js emit deployable output:
@sveltejs/adapter-node— standalone Node server behind nginx@sveltejs/adapter-static— fully prerendered sites on S3 or GitHub Pages@sveltejs/adapter-vercel/adapter-cloudflare— serverless and edge functions
Hybrid apps prerender public content at build time while keeping
/dashboard/** dynamic SSR. Use entries() in
+page.server.ts to tell the prerender crawler which dynamic slugs exist.
Worked example: Harbor Archive publishing portal
Harbor Archive runs its editorial stack on SvelteKit with
adapter-node and PostgreSQL via Drizzle. The architecture optimizes
for fast public guide pages and a low-friction editor experience:
- Route tree:
routes/+page.svelte(marketing) →routes/guides/[slug]/+page.svelte(public article, prerendered) →routes/(editor)/drafts/+page.svelte(auth-gated list) →routes/(editor)/drafts/[id]/+page.svelte(WYSIWYG editor). - Public guide load:
+page.server.tsfetches markdown body and SEO fields;entries()enumerates 800+ slugs at build for static generation. - Root layout auth:
+layout.server.tsreads session cookie, setslocals.user; editor group layout redirects anonymous users to/login. - Autosave action:
saveDraftaction validates title length and slug uniqueness; returnsfail(400, { errors })inline without losing form state viause:enhance. - Publish action: transactional write to
guidestable + invalidates CDN tag; triggersinvalidateAll()on success so the drafts list refreshes. - Preview mode: universal
+page.tson preview route fetches draft by token query param (client navigation after save). - Assets: uploaded images land in S3;
hooks.server.tssigns short-lived URLs for editor thumbnails only.
Vitest tests load and actions by importing modules
directly. Playwright covers publish flow end-to-end. Bundle analysis shows the
public guide page ships ~42 KB gzip JS versus ~118 KB on their prior React SPA
for the same template — compile-time Svelte and aggressive code-splitting
per route explain the gap.
Framework decision table
| Choose SvelteKit when… | Prefer Next.js when… | Prefer Nuxt when… |
|---|---|---|
| Bundle size and TTFB matter for content sites | React hiring pool and RSC ecosystem are priorities | Team is invested in Vue and Pinia patterns |
| You want colocated load + actions with minimal boilerplate | Vercel-first Image Optimization and edge middleware are defaults | Nitro modules and VueUse integrations are already standard |
| Progressive-enhancement forms are a first-class requirement | Large App Router + Server Actions content exists for your stack | Hybrid routeRules with mature CMS modules are needed |
| Design system favors HTML-centric templates over JSX | npm package you need is React-only | Existing Vue component library must be reused verbatim |
| Static prerender + small dynamic editor shell fits the product | Complex client islands inside a server-rendered shell dominate | Auto-imports and useFetch ergonomics beat Svelte syntax |
Versus Remix: both emphasize Web Standards and forms. SvelteKit pairs with Svelte’s compiled output; Remix pairs with React loaders/actions. Versus plain Svelte + Vite SPA: add SvelteKit when you need SSR, file routing, or server mutations in one repo.
Common pitfalls
- Fetching in onMount for SSR pages — users see empty content then a flash; move data into
load. - Importing
$lib/server/dbinto components — leaks server code to the client bundle; keep DB access in+page.server.tsand+server.tsonly. - Returning huge objects from load — serialized payload bloats HTML; paginate lists and strip internal fields.
- Skipping
fail()on validation errors — throws 500; returnfail(400, { fieldErrors })so forms repopulate. - Prerendering auth-gated routes — build emits empty shells or leaks data; set
prerender = falseon private segments. - Wrong adapter for the host —
adapter-staticcannot serve dynamic POST actions without a separate backend. - Ignoring Svelte 5 migration — runes (
$state,$derived) replace legacyexport letand$:; plan upgrades before greenfield depends on deprecated patterns.
Production checklist
- Scaffold with
npm create svelte@latest; enable TypeScript, ESLint, and Prettier. - Organize
$lib/server/for DB, auth, and secrets; keep$lib/isomorphic utilities separate. - Every public page gets data from
load; mutations useactionswithuse:enhance. hooks.server.tssets sessionlocals; editor routes check auth in layout server load.- Configure
prerender+entries()for known slugs; leave dashboards on SSR. - Pick adapter matching deploy target; health check at
src/routes/health/+server.tsreturning 200. - Set
<svelte:head>title and meta from load data; verify with view-source on prerendered pages. - Vitest for load/actions; Playwright smoke on login, save, and publish paths.
- CDN caches immutable assets from
/_app/immutable/; dynamic HTML bypasses cache. - Monitor server logs for
handleErrorspikes after deploys.
Key takeaways
- SvelteKit layers routing, data loading, and deployment on top of compiled Svelte components.
- +page.server.ts load functions and actions keep secrets server-side and support no-JS forms.
- Adapters target Node, static hosts, and serverless without rewriting application code.
- Hybrid prerender + SSR fits content sites with authenticated editor shells.
- Choose SvelteKit when lean bundles, progressive enhancement, and Svelte ergonomics align with the product.
Related reading
- Svelte fundamentals explained — compile-time reactivity, stores, and component syntax
- Vite fundamentals explained — dev server, HMR, and build pipeline under SvelteKit
- SSR, CSR, SSG, and ISR explained — rendering modes and when each fits
- Next.js fundamentals explained — the React full-stack counterpart for comparison