SvelteKit Guide 2026: Routing, Load Functions, Forms and Deploy
A hands-on SvelteKit guide for 2026 — file-based routing, load functions, form actions, and deploying to Vercel or Cloudflare in under an hour.
Why SvelteKit in 2026
If you've been watching the JavaScript ecosystem long enough, you've seen plenty of frameworks promise to fix everything. SvelteKit is different — not because of the hype, but because it quietly delivers. Svelte 5 shipped in late 2024 and the runes system changed how reactivity works at a compiler level, and SvelteKit 2.x builds on top of that with a cleaner adapter story and faster cold-start times.
Honestly, the thing that keeps pulling devs toward SvelteKit is the output size. A standard SvelteKit page with SSR ships maybe 12–15 KB of JS to the client versus the 60–80 KB baseline you're fighting with a typical React app. That matters on mobile, it matters in Core Web Vitals, and it matters when you're building something you actually want to maintain. Less runtime means fewer things going wrong.
That said, SvelteKit isn't without tradeoffs. The ecosystem is smaller than React's, some npm packages assume a browser DOM in ways that break SSR, and the mental model around load functions confuses people coming from Next.js. This guide covers all of that — routing, data loading, form actions, and getting something live — with the specifics you actually need in 2026.
Worth noting: we're working with SvelteKit 2.5+ throughout. If you're on 1.x, some of this will look slightly different, particularly around snapshot APIs and the new $app/navigation hooks.
File-Based Routing: The Mental Model
SvelteKit uses a src/routes directory. Every folder is a route segment, and the files inside it are what SvelteKit actually serves. The four files you'll spend 90% of your time with: +page.svelte, +page.ts (or .js), +layout.svelte, and +server.ts. That's it. Once that clicks, the rest is just variations.
src/routes/
+page.svelte # → /
about/
+page.svelte # → /about
blog/
+page.svelte # → /blog
[slug]/
+page.svelte # → /blog/:slug
+page.ts # load function for this route
(auth)/ # route group — no URL segment
login/
+page.svelte # → /login
register/
+page.svelte # → /registerRoute groups (the folders in parentheses) are one of those features that sounds trivial but saves you real pain. You can share a layout between /login and /register without adding auth to the URL. Same trick works for wrapping your marketing pages in one layout and your dashboard in another — no hacks needed.
Dynamic segments use [brackets]. Optional segments use [[double brackets]]. Rest segments use [...spread]. SvelteKit matches the most specific route first, so you won't accidentally catch /blog/new with [slug] if you've defined new/+page.svelte explicitly. In practice, this resolution order saves you from a class of routing bugs that Next.js required explicit config to handle until 13.x.
Quick aside: named params also work in filenames, not just directories — src/routes/blog-[slug].svelte is valid. Most teams stick to directory-per-segment though, because it keeps +page.ts and +page.svelte co-located cleanly.
Load Functions: SSR Data That Actually Makes Sense
Load functions are SvelteKit's answer to getServerSideProps. They run on the server, return data, and that data gets typed and passed directly to your +page.svelte as the data prop. The typing story here is genuinely good — SvelteKit generates types from your routes automatically if you run svelte-kit sync.
// src/routes/blog/[slug]/+page.ts
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ params, fetch }) => {
const res = await fetch(`/api/posts/${params.slug}`);
if (!res.ok) {
throw error(404, 'Post not found');
}
const post = await res.json();
return { post };
};<!-- src/routes/blog/[slug]/+page.svelte -->
<script lang="ts">
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
</script>
<h1>{data.post.title}</h1>
<div>{@html data.post.content}</div>The fetch you get inside load is not the browser's fetch — it's a SvelteKit-aware wrapper that handles cookies, sets the right origin headers during SSR, and deduplicates requests. This is why calling your own /api routes from a load function works server-side without manually constructing absolute URLs. A lot of people miss this and write code that breaks in production.
Server-only load functions go in +page.server.ts instead of +page.ts. The difference: server load functions can import from $lib/server/* safely (never shipped to the client), access your database directly, and read private environment variables via $env/static/private. Universal load functions run on both server and client, so they can't touch any of that. Look, this distinction trips up almost everyone on their first SvelteKit project — just remember that anything with .server. in the name stays on the server, full stop.
Form Actions: Progressive Enhancement Without the Boilerplate
SvelteKit's form actions are the feature that made me actually rethink how I build forms. The pitch: you write a plain HTML <form> with a method="POST", define named actions in +page.server.ts, and it works with zero JavaScript. Then you add use:enhance and it progressively enhances into a fetch-based submission without a page reload. You get the accessibility and fallback of a plain HTML form with the UX of a React-controlled form, for about a third of the code.
// src/routes/contact/+page.server.ts
import type { Actions } from './$types';
import { fail } from '@sveltejs/kit';
export const actions: Actions = {
default: async ({ request }) => {
const data = await request.formData();
const email = data.get('email')?.toString();
const message = data.get('message')?.toString();
if (!email || !email.includes('@')) {
return fail(422, { email, error: 'Invalid email' });
}
// send email, save to DB, whatever
await sendContactEmail({ email, message });
return { success: true };
}
};<!-- src/routes/contact/+page.svelte -->
<script lang="ts">
import { enhance } from '$app/forms';
import type { PageData, ActionData } from './$types';
let { data, form }: { data: PageData; form: ActionData } = $props();
</script>
<form method="POST" use:enhance>
<input name="email" type="email" />
<textarea name="message"></textarea>
{#if form?.error}
<p class="error">{form.error}</p>
{/if}
<button type="submit">Send</button>
</form>Named actions let you have multiple actions on one page — think a settings page with separate forms for password and profile. You reference them with ?/actionName in the action attribute: <form method="POST" action="?/updatePassword">. Clean.
One more thing — use:enhance accepts a callback if you need control over the submission lifecycle. You can show loading states, cancel the default behavior, or update the UI optimistically before the server responds. It's a lot more composable than it first appears, and you won't need a form library like React Hook Form for most cases.
Layouts and Error Boundaries
Layouts in SvelteKit nest automatically. src/routes/+layout.svelte wraps everything. src/routes/blog/+layout.svelte wraps just the blog routes. Each layout renders a <slot /> (Svelte 4) or {@render children()} (Svelte 5 runes) where the child page goes. You can stack as many layout levels as you need and each one can have its own +layout.ts or +layout.server.ts to load shared data.
<!-- src/routes/+layout.svelte (Svelte 5 style) -->
<script lang="ts">
import type { LayoutData } from './$types';
import Nav from '$lib/components/Nav.svelte';
let { data, children } = $props();
</script>
<Nav user={data.user} />
<main>
{@render children()}
</main>Error pages work on the same principle. +error.svelte in any route directory catches errors thrown in that segment and its children. SvelteKit passes a $page.error store with message and status. You can style a 404 completely differently from a 500 just by checking that status. In practice most teams drop one +error.svelte at the root and call it done, but having route-level error boundaries is there when you need them.
Worth noting: if a load function throws a redirect(303, '/login'), SvelteKit handles the redirect for you on both server and client. Same error(404, ...) pattern — throw it anywhere in a load function and SvelteKit routes to the right +error.svelte. You don't have to manually check response codes and redirect yourself.
API Routes and the +server.ts Pattern
Sometimes you need a plain JSON endpoint — for a webhook, a third-party integration, or because your SvelteKit app is also serving a mobile client. That's what +server.ts is for. It exports named HTTP method handlers: GET, POST, PUT, DELETE, PATCH. You return a Response object, which means you're working with the standard Web API directly.
// src/routes/api/posts/+server.ts
import type { RequestHandler } from './$types';
import { json } from '@sveltejs/kit';
import { db } from '$lib/server/db';
export const GET: RequestHandler = async ({ url }) => {
const limit = Number(url.searchParams.get('limit') ?? 10);
const posts = await db.post.findMany({ take: limit });
return json(posts);
};
export const POST: RequestHandler = async ({ request, locals }) => {
if (!locals.user) {
return new Response('Unauthorized', { status: 401 });
}
const body = await request.json();
const post = await db.post.create({ data: body });
return json(post, { status: 201 });
};The locals object is populated by your hooks.server.ts file, which is where you'd typically attach the current user from a session cookie. Think of hooks.server.ts as middleware — it runs before every request and can set locals, add headers, or short-circuit entirely. If you're building auth, that's where it lives.
One thing that catches people: +server.ts and +page.svelte cannot coexist in the same directory for the same route. You need to choose — either a page or an API endpoint at that path. Most teams put API routes under src/routes/api/ to keep things clean.
Deploying SvelteKit in 2026
SvelteKit uses adapters to target different deployment environments. The default @sveltejs/adapter-auto detects Vercel, Netlify, and Cloudflare Workers automatically. For a standard Vercel deploy in 2026, you need basically nothing — push to GitHub, connect the repo in Vercel's dashboard, and it just works. The auto adapter handles SSR, edge functions, and static assets correctly.
// svelte.config.js
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
export default {
preprocess: vitePreprocess(),
kit: {
adapter: adapter(),
alias: {
$lib: './src/lib',
$components: './src/lib/components'
}
}
};For Cloudflare Pages specifically, swap in @sveltejs/adapter-cloudflare. You get access to platform.env inside your load functions for KV, R2, Durable Objects — the whole Cloudflare stack. Edge-deployed SvelteKit pages cold-start in under 50ms worldwide. That's genuinely hard to beat for anything latency-sensitive.
If you're pre-rendering a mostly static site (blog, docs, marketing), @sveltejs/adapter-static outputs plain HTML files you can deploy anywhere — S3, GitHub Pages, any CDN. You lose dynamic server routes, but your Time to First Byte hits single-digit milliseconds. Add export const prerender = true at the top of any +page.ts to opt individual pages in, or set it globally in svelte.config.js.
Honestly, the adapter story is one of SvelteKit's real strengths compared to Next.js. Switching from Vercel to Cloudflare to a Node server is one config line change, not a multi-day refactor. Your code doesn't care where it runs. If you want to see what well-built UI looks like across different visual styles — not just framework choices — check out what Empire UI does with components for glassmorphism, neobrutalism, and more. The same principle applies: your design system shouldn't be locked to one runtime either. Take a look at the glassmorphism generator for a quick example of that portability in action.
FAQ
+page.ts runs on both server and client (universal), so it can't access private env vars or server-only imports. +page.server.ts runs only on the server — use it for database access, secrets, and auth checks.
No. SvelteKit uses Svelte components, not React. You can't drop in a React component library directly. That said, most UI patterns have Svelte equivalents, and the ecosystem has grown considerably since 2024.
Use hooks.server.ts to read a session cookie, validate it, and attach the user to event.locals on every request. Then check locals.user inside your +page.server.ts load functions or server routes to gate access.
Yes — Svelte 5 runes scale better than Svelte 4's reactive declarations for complex state, and SvelteKit's layout system handles large route trees cleanly. The main limitation is still ecosystem breadth compared to React.