SvelteKit: The Complete Guide to Building and Deploying Full-Stack Svelte Applications
SvelteKit is the official full-stack web framework for Svelte, designed to handle everything from simple static sites to complex server-rendered applications. Where Svelte itself is a component compiler that transforms declarative components into highly efficient vanilla JavaScript, SvelteKit builds on top of it to provide routing, server-side rendering, data loading, and deployment adapters. If you have worked with Next.js or Nuxt, SvelteKit occupies the same position in the Svelte ecosystem — but its compiler-based foundation means the output is typically smaller and faster than frameworks that ship a virtual DOM runtime to the browser.
Why SvelteKit Matters for Web Developers
The core principle that sets SvelteKit apart is Svelte's compiler-first architecture. Traditional frameworks like React and Vue ship a runtime library to the browser that reconciles a virtual DOM with the real one. Svelte eliminates that step entirely by compiling your components into direct DOM manipulation code at build time. The result is JavaScript bundles that are dramatically smaller and applications that feel faster to end users, particularly on low-powered devices or slower connections.
Performance by default is not a marketing claim with SvelteKit — it is an architectural consequence. Because there is no virtual DOM overhead, Svelte components update the DOM with surgical precision. Reactivity is built into the language through a special $: reactive statement syntax, not through hooks or observables. This means less boilerplate, fewer footguns around stale closures, and code that reads more like standard HTML and JavaScript than framework-specific abstractions.
SvelteKit also excels at flexibility in rendering strategies. A single application can mix server-side rendered pages, statically pre-rendered pages, and client-side only sections. You choose the rendering strategy per-route through exported configuration options, not through separate project setups. This makes SvelteKit practical for a wide range of applications: marketing sites with mostly static content, authenticated dashboards that need SSR for personalisation, and API-heavy tools that benefit from server functions running close to your data.
Finally, the developer experience is genuinely excellent. SvelteKit is built on Vite, which means instantaneous hot module replacement during development, fast cold starts, and access to the broad Vite plugin ecosystem. The file-based routing system is intuitive, the TypeScript integration is first-class, and the framework's conventions are consistent enough that you can navigate an unfamiliar SvelteKit project without getting lost.
Step 1: System Requirements
Before installing SvelteKit, confirm your environment meets the minimum requirements.
Operating system
SvelteKit works on macOS, Windows, and Linux. There are no platform-specific restrictions, though most deployment targets are Linux-based servers.
Node.js version
SvelteKit requires Node.js 18 or later. Node.js 20 LTS is the recommended version for production use. You can check your installed version with:
node --version
If you need to manage multiple Node.js versions, use a version manager like nvm or fnm:
# Install Node.js 20 LTS with nvm
nvm install 20
nvm use 20
Package manager
SvelteKit works with npm, pnpm, or Yarn. The examples in this guide use npm, but the commands translate directly.
Hardware
- RAM: 2 GB minimum, 4 GB recommended for comfortable development
- Disk: At least 1 GB free for the project, dependencies, and build output
- CPU: No specific requirements — build times are fast even on modest hardware due to Vite's architecture
Step 2: Install SvelteKit
SvelteKit projects are created using the official scaffolding tool. Run the following command and follow the interactive prompts:
npm create svelte@latest my-app
The CLI will ask several questions:
- Which Svelte app template? Choose "Skeleton project" for a minimal starting point, or "SvelteKit demo app" to see a working example
- Add type checking with TypeScript? Select "Yes, using TypeScript syntax" — TypeScript is strongly recommended
- Add ESLint for code linting? Yes
- Add Prettier for code formatting? Yes
- Add Playwright for browser testing? Optional, but recommended for production applications
- Add Vitest for unit testing? Yes if you plan to write unit tests
Once the scaffold is complete, install dependencies and start the development server:
cd my-app
npm install
npm run dev
The development server starts on http://localhost:5173 by default. Changes to .svelte files trigger instant hot module replacement without a full page reload.
Verifying the installation
Open your browser to http://localhost:5173. You should see the SvelteKit welcome page. Open src/routes/+page.svelte in your editor, change the heading text, and save — you will see the update appear in the browser immediately.
Step 3: Project Structure
A freshly scaffolded SvelteKit project has this structure:
my-app/
├── src/
│ ├── app.html # Root HTML template
│ ├── app.css # Global styles
│ ├── lib/ # Shared utilities, components, stores
│ │ └── index.ts
│ └── routes/ # File-based routing root
│ ├── +layout.svelte # Root layout (wraps all pages)
│ ├── +layout.ts # Layout load function
│ └── +page.svelte # Home page component
├── static/ # Static assets (copied as-is to build output)
├── svelte.config.js # SvelteKit configuration
├── vite.config.ts # Vite configuration
├── tsconfig.json # TypeScript configuration
└── package.json
The routes/ directory
SvelteKit uses a file-based routing system rooted at src/routes/. The URL structure of your application mirrors the directory structure exactly.
Key file types and their roles:
+page.svelte— The page component rendered for that route+page.ts— Runs on both server and client; exports aloadfunction for fetching page data+page.server.ts— Runs on the server only; exportsloadandactionsfunctions+layout.svelte— A layout component that wraps all child routes+layout.ts/+layout.server.ts— Load functions for layouts+server.ts— An API endpoint (handles GET, POST, etc.)+error.svelte— Custom error page for this route and its children
Route examples:
src/routes/
├── +page.svelte → /
├── about/
│ └── +page.svelte → /about
├── blog/
│ ├── +page.svelte → /blog
│ └── [slug]/
│ └── +page.svelte → /blog/any-slug
└── api/
└── posts/
└── +server.ts → /api/posts (REST endpoint)
The lib/ directory
src/lib/ is a special directory aliased to $lib in imports. Use it for shared components, utility functions, stores, and type definitions:
// Any file can import from $lib without relative paths
import { formatDate } from '$lib/utils/date';
import Button from '$lib/components/Button.svelte';
The static/ directory
Files placed in static/ are copied verbatim to the build output root. This is where you put favicons, robots.txt, and other assets that do not go through Vite's processing pipeline.
Step 4: Configure SvelteKit for Your Workflow
Choosing an adapter
Adapters are SvelteKit's mechanism for deploying to different targets. The adapter you choose determines how the build output is structured.
adapter-node — Builds a Node.js server. Use this for VPS, dedicated servers, and containerised deployments:
npm install -D @sveltejs/adapter-node
adapter-static — Generates a fully static site with no server component. Use this for JAMstack deployments:
npm install -D @sveltejs/adapter-static
adapter-auto — Detects the deployment target automatically (Vercel, Netlify, Cloudflare). The default for new projects, but not recommended for VPS deployments — always specify an adapter explicitly for production.
Configure the adapter in svelte.config.js:
import adapter from '@sveltejs/adapter-node';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: vitePreprocess(),
kit: {
adapter: adapter({
// Output directory — defaults to 'build'
out: 'build',
// Set to true to precompress assets with gzip and brotli
precompress: true,
// Environment variable prefix for runtime env vars
envPrefix: ''
})
}
};
export default config;
SSR and SSG configuration per route
You can control rendering behaviour per route by exporting configuration from +page.ts or +layout.ts:
// src/routes/blog/[slug]/+page.ts
// Disable SSR for this route — renders client-side only
export const ssr = false;
// Enable prerendering — page is generated at build time
export const prerender = true;
// Control trailing slash behaviour
export const trailingSlash = 'never';
For a fully static site using adapter-static, you must either prerender all routes or configure fallback:
// svelte.config.js for a static site
import adapter from '@sveltejs/adapter-static';
const config = {
kit: {
adapter: adapter({
fallback: '404.html'
})
}
};
Environment variables
SvelteKit distinguishes between public and private environment variables:
- Public variables (prefixed
PUBLIC_) are safe to expose to the browser - Private variables have no prefix and are only accessible in server-side code
# .env
DATABASE_URL=postgresql://localhost:5432/myapp
SECRET_API_KEY=super-secret-value
PUBLIC_SENTRY_DSN=https://example@sentry.io/123
Access them in your code:
// Server-side only (in +page.server.ts or +server.ts)
import { DATABASE_URL, SECRET_API_KEY } from '$env/static/private';
// Both server and client (in +page.svelte or +page.ts)
import { PUBLIC_SENTRY_DSN } from '$env/static/public';
SvelteKit will throw a build error if you attempt to import a private variable in a file that runs in the browser — a useful safety net.
Step 5: Core Features
File-based routing and layouts
Every +page.svelte file is a Svelte component. Layouts allow you to share UI across routes without repeating yourself:
<!-- src/routes/+layout.svelte -->
<script lang="ts">
import Header from '$lib/components/Header.svelte';
import Footer from '$lib/components/Footer.svelte';
</script>
<Header />
<main>
<!-- Child pages render here -->
<slot />
</main>
<Footer />
Nested layouts work by creating +layout.svelte files in subdirectories. A /blog/[slug] page will inherit both the root layout and any layout in /blog/.
Server-side data loading
The load function in +page.server.ts runs on the server before the page renders. It receives the request context and returns data as plain objects:
// src/routes/blog/[slug]/+page.server.ts
import type { PageServerLoad } from './$types';
import { error } from '@sveltejs/kit';
export const load: PageServerLoad = async ({ params, fetch }) => {
const response = await fetch(`/api/posts/${params.slug}`);
if (!response.ok) {
error(404, 'Post not found');
}
const post = await response.json();
return { post };
};
The returned data is available as the data prop in your page component:
<!-- src/routes/blog/[slug]/+page.svelte -->
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
</script>
<article>
<h1>{data.post.title}</h1>
<div>{@html data.post.content}</div>
</article>
Form actions for progressive enhancement
SvelteKit's form actions let you handle form submissions server-side, with progressive enhancement that works without JavaScript:
// src/routes/contact/+page.server.ts
import type { Actions } from './$types';
import { fail, redirect } from '@sveltejs/kit';
export const actions: Actions = {
default: async ({ request }) => {
const data = await request.formData();
const email = data.get('email') as string;
const message = data.get('message') as string;
if (!email || !message) {
return fail(400, {
error: 'Email and message are required',
values: { email, message }
});
}
// Process the form — send email, save to DB, etc.
await sendEmail({ email, message });
redirect(303, '/contact/thanks');
}
};
<!-- src/routes/contact/+page.svelte -->
<script lang="ts">
import { enhance } from '$app/forms';
import type { ActionData } from './$types';
export let form: ActionData;
</script>
<!-- enhance progressively adds fetch-based submission without page reload -->
<form method="POST" use:enhance>
{#if form?.error}
<p class="error">{form.error}</p>
{/if}
<label>
Email
<input type="email" name="email" value={form?.values?.email ?? ''} />
</label>
<label>
Message
<textarea name="message">{form?.values?.message ?? ''}</textarea>
</label>
<button type="submit">Send message</button>
</form>
API routes with +server.ts
Create REST endpoints by adding +server.ts files:
// src/routes/api/posts/+server.ts
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ url }) => {
const limit = Number(url.searchParams.get('limit') ?? 10);
const posts = await getPosts({ limit });
return json(posts);
};
export const POST: RequestHandler = async ({ request }) => {
const body = await request.json();
const post = await createPost(body);
return json(post, { status: 201 });
};
Building apps with SvelteKit? DeployHQ connects your Git repo to any server and deploys automatically when you push — SFTP, SSH, or cloud. Try it free.
Step 6: Advanced Features
SSR vs SSG vs CSR — choosing the right strategy
SvelteKit supports all three rendering modes and lets you mix them within a single project:
Server-Side Rendering (SSR) — Pages are rendered on the server for each request. Best for content that changes frequently or is personalised. Requires a Node.js server in production.
Static Site Generation (SSG) — Pages are pre-rendered at build time. Best for content that rarely changes. Works with adapter-static and can be served from a CDN with no server.
Client-Side Rendering (CSR) — The page is an empty shell; all rendering happens in the browser. Use this sparingly — it harms SEO and initial load performance.
Configure per-route:
// src/routes/dashboard/+page.ts
// Authenticated dashboard — SSR, not prerendered
export const prerender = false;
export const ssr = true; // this is the default
// ---
// src/routes/pricing/+page.ts
// Marketing page — prerender at build time
export const prerender = true;
Prerendering dynamic routes
For dynamic routes like /blog/[slug], SvelteKit needs to know which paths to prerender. Provide them via the entries function:
// src/routes/blog/[slug]/+page.ts
import type { EntryGenerator } from './$types';
export const prerender = true;
export const entries: EntryGenerator = async () => {
const posts = await getAllPostSlugs();
return posts.map(slug => ({ slug }));
};
Hooks for global request handling
Hooks run before every request and are ideal for authentication, logging, and adding response headers:
// src/hooks.server.ts
import type { Handle } from '@sveltejs/kit';
export const handle: Handle = async ({ event, resolve }) => {
// Check authentication
const session = event.cookies.get('session');
if (session) {
event.locals.user = await validateSession(session);
}
// Protect routes under /admin
if (event.url.pathname.startsWith('/admin') && !event.locals.user) {
return new Response('Unauthorised', { status: 401 });
}
const response = await resolve(event);
// Add security headers to every response
response.headers.set('X-Frame-Options', 'SAMEORIGIN');
response.headers.set('X-Content-Type-Options', 'nosniff');
return response;
};
Anything attached to event.locals in hooks is available in all subsequent load functions and server routes for that request.
Svelte stores for shared state
SvelteKit provides built-in page stores for accessing route information reactively:
<script lang="ts">
import { page } from '$app/stores';
import { navigating } from '$app/stores';
</script>
<!-- Display loading indicator during navigation -->
{#if $navigating}
<div class="loading-bar" />
{/if}
<!-- Access current URL params -->
<p>Current path: {$page.url.pathname}</p>
Step 7: Best Practices
Project organisation at scale
As projects grow, a flat lib/ directory becomes hard to navigate. Structure it by domain:
src/lib/
├── components/
│ ├── ui/ # Generic UI (Button, Input, Modal)
│ └── features/ # Feature-specific components
├── server/ # Server-only code (DB clients, services)
│ ├── db.ts
│ └── auth.ts
├── stores/ # Svelte stores
├── utils/ # Pure utility functions
└── types/ # Shared TypeScript types
Files in src/lib/server/ are automatically blocked from browser imports, similar to private environment variables.
Performance optimisation
Lazy-load heavy components using dynamic imports:
<script lang="ts">
import { onMount } from 'svelte';
let HeavyChart: typeof import('$lib/components/HeavyChart.svelte').default;
onMount(async () => {
const module = await import('$lib/components/HeavyChart.svelte');
HeavyChart = module.default;
});
</script>
{#if HeavyChart}
<svelte:component this={HeavyChart} />
{/if}
Preload links to fetch page data before the user clicks:
<a href="/blog" data-sveltekit-preload-data="hover">Blog</a>
Enable precompression in adapter-node configuration to serve gzip and brotli assets without runtime compression overhead.
SEO and metadata
Use SvelteKit's <svelte:head> for page-specific meta tags:
<!-- src/routes/blog/[slug]/+page.svelte -->
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
</script>
<svelte:head>
<title>{data.post.title} | My Blog</title>
<meta name="description" content={data.post.excerpt} />
<meta property="og:title" content={data.post.title} />
<meta property="og:image" content={data.post.coverImage} />
<link rel="canonical" href="https://example.com/blog/{data.post.slug}" />
</svelte:head>
Testing strategy
Unit test utility functions and stores with Vitest:
// src/lib/utils/date.test.ts
import { describe, it, expect } from 'vitest';
import { formatDate } from './date';
describe('formatDate', () => {
it('formats a date string as DD MMM YYYY', () => {
expect(formatDate('2024-03-15')).toBe('15 Mar 2024');
});
});
End-to-end test critical user flows with Playwright:
// tests/blog.test.ts
import { test, expect } from '@playwright/test';
test('blog post page renders correctly', async ({ page }) => {
await page.goto('/blog/my-first-post');
await expect(page.locator('h1')).toContainText('My First Post');
});
Step 8: Deploy with DeployHQ
Deploying a SvelteKit application with DeployHQ is straightforward when using adapter-node. This section walks through the complete setup for deploying to a VPS or dedicated server.
Prepare your application for deployment
First, confirm you are using adapter-node in svelte.config.js:
import adapter from '@sveltejs/adapter-node';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
const config = {
preprocess: vitePreprocess(),
kit: {
adapter: adapter({
out: 'build',
precompress: true
})
}
};
export default config;
Your build output will land in the build/ directory. The entry point is build/index.js. The server reads its port from the PORT environment variable, defaulting to 3000.
Configure the build command in DeployHQ
In your DeployHQ project settings, set the build command:
npm ci && npm run build
If you use pnpm or Yarn, substitute accordingly:
# pnpm
pnpm install --frozen-lockfile && pnpm run build
# Yarn
yarn install --frozen-lockfile && yarn build
Set the deployment path and post-deployment commands
In DeployHQ, configure the following:
- Deployment path: The directory on your server where files are deployed, e.g.,
/var/www/my-app
Post-deployment command:
cd /var/www/my-app && npm ci --omit=dev && pm2 restart my-app
Configure environment variables
Set your application's environment variables on the server in a .env file at /var/www/my-app/.env:
PORT=3000
NODE_ENV=production
DATABASE_URL=postgresql://user:password@localhost:5432/myapp
SECRET_API_KEY=your-production-secret
Never commit .env files containing secrets to your repository.
Start and manage the server with PM2
On your server, install PM2 to manage the Node.js process:
npm install -g pm2
Create a PM2 ecosystem file:
// ecosystem.config.cjs
module.exports = {
apps: [
{
name: 'my-sveltekit-app',
script: './build/index.js',
instances: 'max',
exec_mode: 'cluster',
env_production: {
NODE_ENV: 'production',
PORT: 3000
},
max_memory_restart: '512M'
}
]
};
Start the application on first deployment:
cd /var/www/my-app
pm2 start ecosystem.config.cjs --env production
pm2 save
pm2 startup
Your post-deployment command in DeployHQ then becomes:
cd /var/www/my-app && npm ci --omit=dev && pm2 reload ecosystem.config.cjs --env production
Branch-based environments
DeployHQ supports deploying different branches to different environments:
| Branch | Environment | Server | Port |
|---|---|---|---|
main |
Production | prod-server | 3000 |
staging |
Staging | staging-server | 3001 |
develop |
Development | dev-server | 3002 |
Serving behind a reverse proxy
In production, place Nginx in front of your Node.js process to handle SSL termination and static file serving:
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
location /_app/ {
alias /var/www/my-app/build/client/_app/;
expires 1y;
add_header Cache-Control "public, immutable";
}
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Step 9: Troubleshooting
"Cannot find module" errors after deployment
Symptom: The application starts locally but fails on the server with module resolution errors.
Fix: Ensure your DeployHQ deployment includes package.json and package-lock.json, and that the post-deployment command runs npm ci --omit=dev in the deployment directory.
Environment variables not available at runtime
Symptom: Variables that work in development return undefined in production.
Fix: Use $env/dynamic/private for values that change between environments:
import { env } from '$env/dynamic/private';
const dbUrl = env.DATABASE_URL; // Read at runtime
Port conflicts on the server
Fix: Check what is listening on the port:
ss -tulnp | grep :3000
If it is an old PM2 process, delete it:
pm2 delete my-sveltekit-app
pm2 start ecosystem.config.cjs --env production
Prerendered pages not updating after content changes
Fix: Prerendered pages are HTML files generated at build time. Either rebuild and redeploy, or move frequently-changing content to SSR by removing export const prerender = true from that route.
Build fails with "adapter-node not found"
Fix: Ensure the adapter is installed:
npm install -D @sveltejs/adapter-node
Verify svelte.config.js imports from @sveltejs/adapter-node and not the default @sveltejs/adapter-auto.
Conclusion
SvelteKit combines Svelte's compiler-based performance advantages with the full-stack capabilities modern applications need: file-based routing, server-side rendering, form actions, API routes, and a flexible adapter system for deploying anywhere. Its approach of compiling components to vanilla JavaScript rather than shipping a runtime means your users receive smaller bundles and faster page loads — without you having to do anything special to achieve it.
For teams deploying to their own infrastructure, the adapter-node output is clean and predictable: a build/ directory containing a Node.js server you start with node build/index.js. With DeployHQ, you configure your build command (npm ci && npm run build), point it at your server, and every push to your production branch triggers a deployment automatically. Add branch-based environments for staging and development, wire up PM2 for zero-downtime reloads, and you have a professional deployment pipeline without the complexity of a managed platform.
Ready to streamline your workflow? Sign up for DeployHQ and connect your first SvelteKit project in minutes.
If you have questions about deploying your SvelteKit projects, reach out to us at support@deployhq.com or find us on Twitter/X.