skip to content
luminary.blog
by Oz Akan
sveltekit framework sketch

SvelteKit Refresher

Comprehensive refresher for SvelteKit framework with routing, data loading, and deployment strategies

/ 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.js or +server.ts files
  • Layouts: Shared UI across routes
  • Data loading: load functions 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.js

Key Directories

  • src/routes/: File-based routing system
  • src/lib/: Reusable components and utilities (accessible via $lib)
  • static/: Static assets served at root
  • src/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

src/routes/products/[[category]]/+page.svelte
<!-- Matches both /products and /products/electronics -->

Rest Parameters

src/routes/docs/[...path]/+page.svelte
<!-- 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:

src/routes/blog/+page.js
export async function load({ fetch, params, url }) {
const response = await fetch('/api/posts');
const posts = await response.json();
return {
posts
};
}
src/routes/blog/+page.svelte
<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:

src/routes/blog/[slug]/+page.server.js
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

src/routes/+layout.svelte
<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>&copy; 2025 My Site</p>
</footer>

Nested Layouts

src/routes/blog/+layout.svelte
<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

src/routes/blog/+layout.js
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

src/routes/contact/+page.server.js
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 };
}
};
src/routes/contact/+page.svelte
<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

+page.server.js
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

src/routes/api/posts/+server.js
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

src/routes/api/posts/[id]/+server.js
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>
<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)

.env
DATABASE_URL=postgresql://localhost:5432/mydb
SECRET_KEY=super-secret
// +page.server.js
import { env } from '$env/dynamic/private';
console.log(env.DATABASE_URL); // Works on server
console.log(env.SECRET_KEY); // Never sent to browser

Public Variables (Client-Side)

Must be prefixed with PUBLIC_:

.env
PUBLIC_API_URL=https://api.example.com
PUBLIC_SITE_NAME=My App
// Anywhere (client or server)
import { env } from '$env/dynamic/public';
console.log(env.PUBLIC_API_URL); // Works everywhere

Static 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)

src/hooks.server.js
// Run on every request
export 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 requests
export 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 errors
export async function handleError({ error, event }) {
console.error('Error:', error);
return {
message: 'Something went wrong'
};
}

Client Hooks (hooks.client.js)

src/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

svelte.config.js
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-cloudflare

Prerendering

Page-Level Prerendering

// +page.js or +page.server.js
export const prerender = true; // Prerender this page

Disabling Prerendering

export const prerender = false; // Don't prerender

Configuration Options

+page.server.js
export const prerender = true;
export const ssr = false; // Client-side only
export const csr = true; // Enable client-side rendering
export const trailingSlash = 'always'; // URL formatting

Error Handling

Custom Error Pages

src/routes/+error.svelte
<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 functions
import { 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

Terminal window
npm run build

Preview Production Build

Terminal window
npm run preview

Platform-Specific Deployment

Vercel

Terminal window
npm install -D @sveltejs/adapter-vercel

Netlify

Terminal window
npm install -D @sveltejs/adapter-netlify

Cloudflare Pages

Terminal window
npm install -D @sveltejs/adapter-cloudflare

Docker (Node.js)

FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["node", "build"]

Best Practices

Component Organization

src/lib/components/Button.svelte
<!-- 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.js for 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 $lib for 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.js files
  • Layouts: Shared UI across routes
  • Data loading: load functions 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/posts

Special 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 fetch for 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 - secure
export 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:

+page.server.js
export const actions = {
default: async ({ request }) => {
const data = await request.formData();
const email = data.get('email');
// Process form
await sendEmail(email);
return { success: true };
}
};
+page.svelte
<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.svelte

Layouts:

  • Persist across navigation (don’t re-render)
  • Can have their own load functions
  • 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):

.env
DATABASE_URL=secret
// +page.server.js
import { env } from '$env/dynamic/private';
console.log(env.DATABASE_URL); // Works

Public (client-side): Must be prefixed with PUBLIC_

.env
PUBLIC_API_URL=https://api.example.com
// anywhere
import { env } from '$env/dynamic/public';
console.log(env.PUBLIC_API_URL); // Works in browser

Static (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:

+error.svelte
<script>
import { page } from '$app/stores';
</script>
<h1>{$page.status}: {$page.error.message}</h1>

Error hooks:

hooks.server.js
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.