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.

> **New in 2026 — managed Static Hosting**: DeployHQ now offers [Static Hosting](https://www.deployhq.com/hosting/static) — fully-managed deployment to Cloudflare's edge. For prerendered SvelteKit builds with `adapter-static`, this is the simplest path. The walkthrough below covers BYO-server deployment with `adapter-node` for SSR, which remains fully supported.

## 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:

```bash
node --version
```

If you need to manage multiple Node.js versions, use a version manager like `nvm` or `fnm`:

```bash
# 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:

```bash
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:

```bash
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 a `load` function for fetching page data
- `+page.server.ts` — Runs on the server only; exports `load` and `actions` functions
- `+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:

```typescript
// 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:

```bash
npm install -D @sveltejs/adapter-node
```

**adapter-static** — Generates a fully static site with no server component. Use this for JAMstack deployments:

```bash
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`:

```javascript
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`:

```typescript
// 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`:

```javascript
// 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

```bash
# .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:

```typescript
// 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:

```svelte
<!-- 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:

```typescript
// 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:

```svelte
<!-- 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:

```typescript
// 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');
    }
};
```

```svelte
<!-- 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:

```typescript
// 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](https://www.deployhq.com/signup) connects your Git repo to any server and deploys automatically when you push — SFTP, SSH, or cloud. [Try it free](https://www.deployhq.com/signup).*

---

## 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:

```typescript
// 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:

```typescript
// 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:

```typescript
// 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:

```svelte
<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:

```svelte
<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:

```svelte
<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:

```svelte
<!-- 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:

```typescript
// 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:

```typescript
// 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`:

```javascript
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:

```bash
npm ci && npm run build
```

If you use pnpm or Yarn, substitute accordingly:

```bash
# 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:**

```bash
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`:

```bash
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:

```bash
npm install -g pm2
```

Create a PM2 ecosystem file:

```javascript
// 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:

```bash
cd /var/www/my-app
pm2 start ecosystem.config.cjs --env production
pm2 save
pm2 startup
```

Your post-deployment command in DeployHQ then becomes:

```bash
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:

```nginx
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:

```typescript
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:

```bash
ss -tulnp | grep :3000
```

If it is an old PM2 process, delete it:

```bash
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:

```bash
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](https://www.deployhq.com/signup) 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](mailto:support@deployhq.com) or find us on [Twitter/X](https://x.com/deployhq).
