
SvelteKit Refresher
/ 12 min read
Table of Contents
What is SvelteKit?
SvelteKit is the official application framework for Svelte, similar to Next.js for React or Nuxt for Vue. It’s a full-stack framework that provides everything you need to build production-ready web applications.
Key Features
- File-based routing: Automatic routing from file structure
- SSR/SSG: Server-side rendering and static site generation
- API routes: Backend endpoints in
+server.jsor+server.tsfiles - Layouts: Shared UI across routes
- Data loading:
loadfunctions for fetching data - Form actions: Server-side form handling with progressive enhancement
- Adapters: Deploy to any platform (Vercel, Netlify, Node.js, etc.)
- Code splitting: Automatic route-based code splitting
- Zero config: Works out of the box with sensible defaults
Project Structure
my-app/├── src/│ ├── routes/│ │ ├── +layout.svelte│ │ ├── +page.svelte│ │ ├── about/│ │ │ └── +page.svelte│ │ └── blog/│ │ ├── +page.svelte│ │ └── [slug]/│ │ └── +page.svelte│ ├── lib/│ │ └── index.js│ └── app.html├── static/├── package.json└── svelte.config.jsKey Directories
src/routes/: File-based routing systemsrc/lib/: Reusable components and utilities (accessible via$lib)static/: Static assets served at rootsrc/app.html: HTML template wrapper
Routing
Basic Routes
<!-- src/routes/+page.svelte (/) --><h1>Home Page</h1><!-- src/routes/about/+page.svelte (/about) --><h1>About Page</h1>Dynamic Routes
<!-- src/routes/blog/[slug]/+page.svelte (/blog/hello-world) --><script> import { page } from '$app/stores';</script>
<h1>Blog Post: {$page.params.slug}</h1>Optional Parameters
<!-- Matches both /products and /products/electronics -->Rest Parameters
<!-- Matches /docs/a, /docs/a/b, /docs/a/b/c, etc. -->Special Files
+page.svelte: Page component+page.js: Universal data loader+page.server.js: Server-only data loader+layout.svelte: Layout wrapper+layout.js: Layout data loader+server.js: API endpoint+error.svelte: Error page[param]: Dynamic route parameter[[optional]]: Optional parameter[...rest]: Rest parameters
Loading Data
Universal Load Functions (+page.js)
Universal loads run on both server and client:
export async function load({ fetch, params, url }) { const response = await fetch('/api/posts'); const posts = await response.json();
return { posts };}<script> let { data } = $props();</script>
{#each data.posts as post} <article> <h2>{post.title}</h2> <p>{post.excerpt}</p> </article>{/each}Server-Only Load Functions (+page.server.js)
Server loads only run on the server:
import { error } from '@sveltejs/kit';import { db } from '$lib/server/database';
export async function load({ params }) { const post = await db.getPost(params.slug);
if (!post) { throw error(404, 'Post not found'); }
return { post };}When to use server-only loads:
- Database access
- Private API keys
- Sensitive data
- Server-only libraries
Load Function Parameters
export async function load({ fetch, // Fetch with SSR support params, // Route parameters url, // URL object route, // Route info parent, // Parent load data depends, // Manual dependencies untrack // Opt out of tracking}) { // ...}Layouts
Root Layout
<script> import '../app.css';</script>
<nav> <a href="/">Home</a> <a href="/about">About</a> <a href="/blog">Blog</a></nav>
<main> <slot /></main>
<footer> <p>© 2025 My Site</p></footer>Nested Layouts
<div class="blog-layout"> <aside> <h3>Categories</h3> <ul> <li><a href="/blog/tech">Tech</a></li> <li><a href="/blog/life">Life</a></li> </ul> </aside>
<div class="content"> <slot /> </div></div>Layout Data
export async function load({ fetch }) { const categories = await fetch('/api/categories').then(r => r.json()); return { categories };}Resetting Layouts
<!-- src/routes/admin/+layout@.svelte --><!-- The @ resets to root layout, ignoring parent layouts -->Form Actions
Basic Form Action
import { fail } from '@sveltejs/kit';
export const actions = { default: async ({ request }) => { const data = await request.formData(); const email = data.get('email'); const message = data.get('message');
if (!email) { return fail(400, { email, missing: true }); }
// Send email logic here await sendEmail(email, message);
return { success: true }; }};<script> import { enhance } from '$app/forms'; let { form } = $props();</script>
<form method="POST" use:enhance> <label> Email: <input name="email" type="email" value={form?.email ?? ''} class:error={form?.missing} /> </label>
<label> Message: <textarea name="message"></textarea> </label>
<button type="submit">Send</button></form>
{#if form?.success} <p class="success">Message sent successfully!</p>{/if}
{#if form?.missing} <p class="error">Email is required</p>{/if}Named Actions
export const actions = { login: async ({ request }) => { // Login logic },
register: async ({ request }) => { // Registration logic }};<form method="POST" action="?/login" use:enhance> <!-- Login form --></form>
<form method="POST" action="?/register" use:enhance> <!-- Registration form --></form>Progressive Enhancement
The use:enhance directive provides:
- Client-side validation
- Loading states
- Optimistic UI updates
- Error handling
- Works without JavaScript (progressive enhancement)
<script> import { enhance } from '$app/forms';
let loading = $state(false);</script>
<form method="POST" use:enhance={() => { loading = true;
return async ({ result, update }) => { await update(); loading = false; }; }}> <button disabled={loading}> {loading ? 'Sending...' : 'Send'} </button></form>API Routes
Basic API Route
import { json } from '@sveltejs/kit';
export async function GET() { const posts = await getPosts(); return json(posts);}
export async function POST({ request }) { const data = await request.json(); const post = await createPost(data); return json(post, { status: 201 });}Dynamic API Routes
import { json, error } from '@sveltejs/kit';
export async function GET({ params }) { const post = await getPost(params.id);
if (!post) { throw error(404, 'Post not found'); }
return json(post);}
export async function PUT({ params, request }) { const data = await request.json(); const post = await updatePost(params.id, data); return json(post);}
export async function DELETE({ params }) { await deletePost(params.id); return new Response(null, { status: 204 });}Request Handling
export async function POST({ request, cookies, locals }) { // Parse different content types const json = await request.json(); const formData = await request.formData(); const text = await request.text();
// Access headers const auth = request.headers.get('authorization');
// Set cookies cookies.set('session', sessionId, { path: '/', httpOnly: true, secure: true, maxAge: 60 * 60 * 24 * 7 // 1 week });
// Access user data (set in hooks) console.log(locals.user);
return json({ success: true });}SvelteKit Stores
Page Store
<script> import { page } from '$app/stores';
$effect(() => { console.log('Current route:', $page.route.id); console.log('URL params:', $page.params); console.log('Search params:', $page.url.searchParams); console.log('Page data:', $page.data); });</script>
<p>Current path: {$page.url.pathname}</p>Navigation Store
<script> import { navigating } from '$app/stores';</script>
{#if $navigating} <div class="loading-bar">Loading...</div>{/if}Updated Store
<script> import { updated } from '$app/stores';</script>
{#if $updated} <div class="toast"> New version available! <button onclick={() => location.reload()}> Reload </button> </div>{/if}Environment Variables
Private Variables (Server-Only)
DATABASE_URL=postgresql://localhost:5432/mydbSECRET_KEY=super-secret
// +page.server.jsimport { env } from '$env/dynamic/private';
console.log(env.DATABASE_URL); // Works on serverconsole.log(env.SECRET_KEY); // Never sent to browserPublic Variables (Client-Side)
Must be prefixed with PUBLIC_:
PUBLIC_API_URL=https://api.example.comPUBLIC_SITE_NAME=My App
// Anywhere (client or server)import { env } from '$env/dynamic/public';
console.log(env.PUBLIC_API_URL); // Works everywhereStatic Environment Variables
For build-time constants:
import { PUBLIC_API_URL } from '$env/static/public';import { SECRET_KEY } from '$env/static/private';Hooks
Server Hooks (hooks.server.js)
// Run on every requestexport async function handle({ event, resolve }) { // Check authentication const session = event.cookies.get('session'); if (session) { event.locals.user = await getUserFromSession(session); }
// Modify request/response const response = await resolve(event, { transformPageChunk: ({ html }) => html.replace('%lang%', 'en') });
return response;}
// Handle fetch requestsexport async function handleFetch({ request, fetch }) { // Modify external requests if (request.url.startsWith('https://api.example.com/')) { request = new Request( request.url, { ...request, headers: { ...request.headers, 'Authorization': `Bearer ${API_KEY}` } } ); }
return fetch(request);}
// Handle errorsexport async function handleError({ error, event }) { console.error('Error:', error);
return { message: 'Something went wrong' };}Client Hooks (hooks.client.js)
export async function handleError({ error, event }) { // Log to error tracking service console.error('Client error:', error);
return { message: 'An error occurred' };}Configuration
Basic Configuration
import adapter from '@sveltejs/adapter-auto';
export default { kit: { adapter: adapter(),
// Path aliases alias: { $components: 'src/components', $utils: 'src/utils' },
// CSP configuration csp: { directives: { 'script-src': ['self'] } },
// Environment directory env: { dir: '.' } }};Adapters
Static Adapter (SSG)
import adapter from '@sveltejs/adapter-static';
export default { kit: { adapter: adapter({ pages: 'build', assets: 'build', fallback: 'index.html', // For SPA mode precompress: true }) }};Node Adapter (SSR)
import adapter from '@sveltejs/adapter-node';
export default { kit: { adapter: adapter({ out: 'build', precompress: true, envPrefix: 'MY_APP_' }) }};Auto Adapter
import adapter from '@sveltejs/adapter-auto';
// Automatically detects platform:// - Vercel → @sveltejs/adapter-vercel// - Netlify → @sveltejs/adapter-netlify// - Cloudflare Pages → @sveltejs/adapter-cloudflarePrerendering
Page-Level Prerendering
// +page.js or +page.server.jsexport const prerender = true; // Prerender this pageDisabling Prerendering
export const prerender = false; // Don't prerenderConfiguration Options
export const prerender = true;export const ssr = false; // Client-side onlyexport const csr = true; // Enable client-side renderingexport const trailingSlash = 'always'; // URL formattingError Handling
Custom Error Pages
<script> import { page } from '$app/stores';</script>
<div class="error-page"> <h1>{$page.status}</h1> <p>{$page.error.message}</p>
{#if $page.status === 404} <p>Page not found</p> <a href="/">Go home</a> {:else} <p>Something went wrong</p> {/if}</div>Throwing Errors
// In load functionsimport { error } from '@sveltejs/kit';
export async function load({ params }) { const item = await db.get(params.id);
if (!item) { throw error(404, { message: 'Item not found' }); }
return { item };}Redirects
import { redirect } from '@sveltejs/kit';
export async function load({ locals }) { if (!locals.user) { throw redirect(302, '/login'); }
return { user: locals.user };}Deployment
Build for Production
npm run buildPreview Production Build
npm run previewPlatform-Specific Deployment
Vercel
npm install -D @sveltejs/adapter-vercelNetlify
npm install -D @sveltejs/adapter-netlifyCloudflare Pages
npm install -D @sveltejs/adapter-cloudflareDocker (Node.js)
FROM node:18-alpineWORKDIR /appCOPY package*.json ./RUN npm ci --only=productionCOPY . .RUN npm run buildEXPOSE 3000CMD ["node", "build"]Best Practices
Component Organization
<!-- Use PascalCase for component names --><script> let { variant = 'primary', size = 'medium', disabled = false, onclick } = $props();</script>
<button class="btn btn-{variant} btn-{size}" {disabled} {onclick}> <slot /></button>
<style> .btn { padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; }</style>Performance Tips
- Use
+page.server.jsfor server-only data - Implement proper loading states
- Use form actions for mutations
- Leverage prerendering for static pages
- Use
depends()for manual cache invalidation - Implement proper error boundaries
- Use
$libfor shared code
Security Best Practices
- Never expose private env vars
- Use CSRF protection (built-in with form actions)
- Validate all user input
- Use server-only loads for sensitive data
- Implement proper authentication in hooks
- Use CSP headers
- Sanitize user-generated content
Interview Questions and Answers
Q1: What is SvelteKit and how does it relate to Svelte?
A1: SvelteKit is the official application framework for Svelte, similar to how Next.js relates to React:
Svelte: Component framework for building UIs SvelteKit: Full-stack framework that adds:
- File-based routing: Automatic routing from file structure
- SSR/SSG: Server-side rendering and static site generation
- API routes: Backend endpoints in
+server.jsfiles - Layouts: Shared UI across routes
- Data loading:
loadfunctions for fetching data - Form actions: Server-side form handling
- Adapters: Deploy to any platform
SvelteKit is to production apps what Svelte is to components.
Q2: Explain SvelteKit’s file-based routing system.
A2: SvelteKit uses file structure to define routes:
src/routes/├── +page.svelte → /├── about/+page.svelte → /about├── blog/│ ├── +page.svelte → /blog│ └── [slug]/│ └── +page.svelte → /blog/:slug└── api/ └── posts/+server.js → /api/postsSpecial files:
+page.svelte: Page component+page.js: Page data loader+page.server.js: Server-only data loader+layout.svelte: Layout wrapper+server.js: API endpoint[param]: Dynamic route parameter[[optional]]: Optional parameter
Clean, intuitive, and powerful.
Q3: What is the purpose of load functions in SvelteKit?
A3: load functions fetch data before rendering a page. They run:
- On the server during SSR
- In the browser during client-side navigation
+page.js (universal):
export async function load({ fetch, params }) { const res = await fetch(`/api/posts/${params.slug}`); return { post: await res.json() };}+page.server.js (server-only):
export async function load({ params }) { const post = await db.getPost(params.slug); return { post };}Use server-only loads for:
- Database access
- Private API keys
- Sensitive data
Data is available via $props() in the page component.
Q4: What is the difference between +page.js and +page.server.js?
A4:
+page.js (Universal loader):
- Runs on server AND client
- Can’t access server-only APIs
- Code is sent to browser
- Use
fetchfor external APIs
+page.server.js (Server-only loader):
- Only runs on server
- Can access databases, private keys
- Code never sent to browser
- More secure for sensitive operations
// +page.server.js - secureexport async function load() { const data = await db.query('SELECT * FROM users'); return { data };}Choose server-only when you need security or server resources.
Q5: What are SvelteKit form actions and when would you use them?
A5: Form actions handle server-side form submissions progressively enhanced:
export const actions = { default: async ({ request }) => { const data = await request.formData(); const email = data.get('email');
// Process form await sendEmail(email);
return { success: true }; }};<script> import { enhance } from '$app/forms'; let { form } = $props();</script>
<form method="POST" use:enhance> <input name="email" type="email" /> <button>Submit</button></form>
{#if form?.success} <p>Email sent!</p>{/if}Benefits:
- Works without JavaScript (progressive enhancement)
- Automatic revalidation
- Built-in error handling
- No API routes needed for forms
Q6: How do layouts work in SvelteKit?
A6: Layouts wrap pages and share UI across routes:
<!-- src/routes/+layout.svelte (root layout) --><nav> <a href="/">Home</a> <a href="/about">About</a></nav>
<main> <slot /> <!-- Page content goes here --></main>
<footer>© 2025</footer>Nested layouts:
src/routes/├── +layout.svelte (applies to all)└── blog/ ├── +layout.svelte (applies to /blog/*) └── +page.svelteLayouts:
- Persist across navigation (don’t re-render)
- Can have their own
loadfunctions - Can be reset with
@syntax:+layout@.svelte - Enable shared state and UI patterns
Q7: How do you handle environment variables in SvelteKit?
A7: SvelteKit uses two types of environment variables:
Private (server-only):
DATABASE_URL=secret
// +page.server.jsimport { env } from '$env/dynamic/private';console.log(env.DATABASE_URL); // WorksPublic (client-side): Must be prefixed with PUBLIC_
PUBLIC_API_URL=https://api.example.com
// anywhereimport { env } from '$env/dynamic/public';console.log(env.PUBLIC_API_URL); // Works in browserStatic (compile-time):
import { PUBLIC_API_URL } from '$env/static/public';Private vars never reach the browser. Public vars are bundled into client code. This prevents accidentally exposing secrets.
Q8: What are SvelteKit hooks and what are they used for?
A8: Hooks run on every request and allow you to modify behavior:
Server hooks (hooks.server.js):
export async function handle({ event, resolve }) { // Authentication event.locals.user = await getUserFromSession(event.cookies.get('session'));
const response = await resolve(event); return response;}Use cases:
- Authentication/authorization
- Request logging
- Response headers
- Session management
- API proxying
Client hooks (hooks.client.js):
export async function handleError({ error, event }) { // Client-side error tracking logToService(error);}Hooks provide centralized request/response handling.
Q9: What is the difference between SSR, SSG, and CSR in SvelteKit?
A9:
SSR (Server-Side Rendering):
- Renders pages on each request
- Dynamic content
- Use for personalized/real-time data
- Default in SvelteKit
SSG (Static Site Generation):
- Pre-renders pages at build time
- Static HTML files
- Use
export const prerender = true - Best for blogs, docs, marketing pages
CSR (Client-Side Rendering):
- Renders in browser
- Use
export const ssr = false - For highly interactive apps
You can mix all three in one app!
Q10: How do you handle errors in SvelteKit?
A10: Multiple ways to handle errors:
Throwing errors in load:
import { error } from '@sveltejs/kit';
export async function load({ params }) { const item = await db.get(params.id); if (!item) throw error(404, 'Not found'); return { item };}Custom error page:
<script> import { page } from '$app/stores';</script><h1>{$page.status}: {$page.error.message}</h1>Error hooks:
export async function handleError({ error, event }) { console.error(error); return { message: 'Something went wrong' };}Errors automatically bubble up to nearest error boundary.
This refresher covers the essential concepts of SvelteKit. SvelteKit’s file-based routing and seamless full-stack integration make it a powerful framework for building modern web applications.