Frontend

SvelteKit: Building Full-Stack Svelte Apps

Mayur Dabhi
Mayur Dabhi
June 5, 2026
14 min read

SvelteKit is Svelte's official full-stack framework — it takes Svelte's compile-time reactivity and layers on everything a production application needs: file-based routing, server-side rendering, server-only data loading, API endpoints, and a flexible adapter system that deploys anywhere from Vercel to a bare Node.js server. Think of it as Next.js for React or Nuxt for Vue, but built on a fundamentally different philosophy.

Where React and Vue carry a runtime library into the browser, Svelte compiles your components away at build time. The result is smaller bundles, faster Time to Interactive, and components that read closer to plain HTML and JavaScript. SvelteKit adds the routing and server layers on top of that, using Vite as the build tool for near-instant hot module replacement during development.

Released as stable in December 2022, SvelteKit has been battle-tested in production by teams who prioritize performance and developer experience. In this guide you will build a solid understanding of how SvelteKit works — from file-based routing and load functions through to API routes, form actions, and deployment adapters.

Why SvelteKit Performs So Well

Svelte compiles components to optimized vanilla JavaScript — no virtual DOM reconciliation at runtime. SvelteKit pages typically ship 30–50% less JavaScript than equivalent Next.js pages. Server-side rendering is built in by default, so users see content before any JavaScript executes, and navigation between pages uses client-side routing with prefetching for near-instant transitions.

What is SvelteKit?

SvelteKit is the result of layering a full web framework on top of the Svelte compiler. Svelte alone gives you reactive components; SvelteKit gives you the rest of what you need to ship an application:

Feature SvelteKit Next.js Nuxt
Base framework Svelte React Vue 3
Build tool Vite Turbopack / Webpack Vite / Nitro
Bundle approach Compiled (no runtime) Runtime (React ~45 KB) Runtime (Vue ~35 KB)
SSR default Yes Yes (App Router) Yes
Data loading +page.server.js Server Components useFetch / useAsyncData
API routes +server.js app/api/route.js server/api/
TypeScript support First-class First-class First-class

Installation and Project Setup

SvelteKit projects are scaffolded with npm create svelte@latest, which walks you through a small interactive setup wizard. You only need Node.js 18 or later.

1

Scaffold a new project

Run the create command, choose a template, and select TypeScript support and any optional tooling (ESLint, Prettier, Playwright, Vitest).

Terminal
npm create svelte@latest my-sveltekit-app

# Interactive prompts:
# ✔ Which Svelte app template? › Skeleton project
# ✔ Add type checking with TypeScript? › Yes, using TypeScript syntax
# ✔ Select additional options: › ESLint, Prettier, Vitest

cd my-sveltekit-app
npm install
npm run dev
2

Explore the project structure

All routing lives in src/routes. Shared utilities and components go in src/lib and are importable via the $lib alias.

Project Structure
my-sveltekit-app/
├── src/
│   ├── routes/
│   │   ├── +layout.svelte      # Root layout (nav, footer)
│   │   ├── +page.svelte        # Home page  → /
│   │   ├── blog/
│   │   │   ├── +page.svelte    # Blog list  → /blog
│   │   │   └── [slug]/
│   │   │       ├── +page.svelte        # Blog post → /blog/:slug
│   │   │       └── +page.server.ts     # Server-side data loading
│   │   └── api/
│   │       └── posts/
│   │           └── +server.ts          # API route → /api/posts
│   ├── lib/
│   │   ├── components/         # Shared Svelte components
│   │   └── server/             # Server-only utilities (DB, etc.)
│   └── app.html                # HTML shell template
├── static/                     # Static assets (images, fonts)
├── svelte.config.js            # SvelteKit configuration
└── vite.config.ts              # Vite configuration
3

Configure your environment

Create a .env file for secrets. Variables prefixed with PUBLIC_ are exposed to the browser; all others remain server-only and are accessible via $env/static/private.

.env
DATABASE_URL="postgresql://user:password@localhost:5432/mydb"
JWT_SECRET="your-secret-key-never-expose-this"
PUBLIC_API_BASE="https://api.myapp.com"

File-Based Routing

SvelteKit's router is entirely driven by the file system. Every directory inside src/routes is a route segment, and the contents of that directory determine what happens when a user visits that URL. The naming conventions use a + prefix to distinguish route files from regular components:

File name Purpose Runs on
+page.svelte Page UI component Client + server (SSR)
+layout.svelte Shared wrapper around child routes Client + server (SSR)
+page.server.ts Server-only load function & form actions Server only
+page.ts Universal load function Server (SSR) + client (navigation)
+server.ts REST API endpoint Server only
+error.svelte Custom error page Client + server (SSR)

Dynamic Routes and Route Groups

Wrap a segment in square brackets to capture a dynamic value: src/routes/blog/[slug] matches /blog/any-slug-here. Use spread syntax for catch-all routes: [...path]. Wrap a segment in parentheses to create a route group that shares a layout without affecting the URL: (marketing), (app).

SvelteKit File-Based Routing src/routes/ +layout.svelte wraps everything +page.svelte → / blog/+page.svelte → /blog api/posts/+server.ts → /api/posts [slug]/+page.svelte → /blog/:slug [slug]/+page.server.ts server-only load() (group) folders share layouts without affecting URLs

SvelteKit routes map directly to the file system in src/routes/

Here is a root layout that wraps every page with navigation, and a simple home page that uses the data prop passed from a load function:

src/routes/+layout.svelte
<script lang="ts">
    import { page } from '$app/stores';
</script>

<nav>
    <a href="/" class:active={$page.url.pathname === '/'}>Home</a>
    <a href="/blog" class:active={$page.url.pathname.startsWith('/blog')}>Blog</a>
    <a href="/about" class:active={$page.url.pathname === '/about'}>About</a>
</nav>

<main>
    <slot />
</main>

<footer>
    <p>© 2026 My SvelteKit App</p>
</footer>

<style>
    nav a.active { color: #FF3E00; font-weight: 600; }
</style>
src/routes/+page.svelte
<script lang="ts">
    import type { PageData } from './$types';

    export let data: PageData;

    let count = 0;
    const increment = () => count++;
</script>

<svelte:head>
    <title>{data.siteTitle}</title>
</svelte:head>

<h1>Welcome to {data.siteTitle}</h1>
<p>Posts published: {data.postCount}</p>

<button on:click={increment}>
    Clicked {count} {count === 1 ? 'time' : 'times'}
</button>

Loading Data with Load Functions

SvelteKit separates data fetching from rendering through load functions. A load function runs before its page renders, fetches whatever the page needs, and returns data that becomes the data prop on +page.svelte. There are two variants:

src/routes/blog/[slug]/+page.server.ts
import { error } from '@sveltejs/kit';
import { db } from '$lib/server/database';
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async ({ params }) => {
    const post = await db.post.findUnique({
        where: { slug: params.slug },
        include: { author: true, tags: true }
    });

    if (!post) {
        // SvelteKit renders +error.svelte with this message
        throw error(404, `Post "${params.slug}" not found`);
    }

    return {
        post,
        // Only serializable data reaches the client
        meta: {
            title: post.title,
            description: post.excerpt
        }
    };
};
src/routes/blog/[slug]/+page.svelte
<script lang="ts">
    import type { PageData } from './$types';
    export let data: PageData;

    const { post, meta } = data;
</script>

<svelte:head>
    <title>{meta.title}</title>
    <meta name="description" content={meta.description}>
</svelte:head>

<article>
    <h1>{post.title}</h1>
    <p class="byline">
        By {post.author.name} •
        {new Date(post.publishedAt).toLocaleDateString()}
    </p>
    <div class="content">{@html post.htmlContent}</div>

    <ul class="tags">
        {#each post.tags as tag}
            <li><a href="/blog?tag={tag.slug}">{tag.name}</a></li>
        {/each}
    </ul>
</article>

Caching and Revalidation

Load functions can set HTTP cache headers so that CDNs cache rendered pages. Return a setHeaders call from your load function to control the Cache-Control header, or use SvelteKit's built-in invalidate() and invalidateAll() on the client to force a reload when data changes.

src/routes/blog/+page.server.ts — with caching
import type { PageServerLoad } from './$types';
import { db } from '$lib/server/database';

export const load: PageServerLoad = async ({ setHeaders }) => {
    const posts = await db.post.findMany({
        where: { published: true },
        orderBy: { publishedAt: 'desc' },
        select: { id: true, title: true, slug: true, excerpt: true, publishedAt: true }
    });

    // Cache the page at the CDN for 60 seconds, allow stale for 300 s
    setHeaders({
        'Cache-Control': 'public, max-age=60, stale-while-revalidate=300'
    });

    return { posts };
};
Avoid Browser APIs in Server Load Functions

Because +page.server.ts load functions run exclusively on the server, window, document, localStorage, and sessionStorage do not exist. For universal load functions (+page.ts) that run on both, guard browser-only calls with the browser boolean from $app/environment: if (browser) { ... }.

API Routes and Form Actions

SvelteKit gives you two ways to handle server-side mutations: API routes with +server.ts files that respond to HTTP requests programmatically, and form actions in +page.server.ts that handle HTML form submissions with progressive enhancement.

API Routes with +server.ts

Export named functions matching HTTP verbs. SvelteKit's json() helper sets the correct Content-Type and serializes the response:

src/routes/api/posts/+server.ts
import { json, error } from '@sveltejs/kit';
import { db } from '$lib/server/database';
import type { RequestHandler } from './$types';

// GET /api/posts?page=1&limit=10
export const GET: RequestHandler = async ({ url }) => {
    const page  = Number(url.searchParams.get('page')  ?? '1');
    const limit = Number(url.searchParams.get('limit') ?? '10');

    const [posts, total] = await Promise.all([
        db.post.findMany({
            where: { published: true },
            skip:  (page - 1) * limit,
            take:  limit,
            orderBy: { publishedAt: 'desc' }
        }),
        db.post.count({ where: { published: true } })
    ]);

    return json({ posts, total, page, pages: Math.ceil(total / limit) });
};

// POST /api/posts
export const POST: RequestHandler = async ({ request, locals }) => {
    if (!locals.user?.isAdmin) throw error(403, 'Forbidden');

    const body = await request.json();
    if (!body.title || !body.content) {
        throw error(400, 'title and content are required');
    }

    const post = await db.post.create({
        data: {
            title:     body.title,
            content:   body.content,
            slug:      body.title.toLowerCase().replace(/\s+/g, '-'),
            authorId:  locals.user.id
        }
    });

    return json(post, { status: 201 });
};

Form Actions — Progressive Enhancement

Form actions are a more ergonomic alternative for operations triggered by forms. They work without JavaScript (full page reload) and upgrade transparently to client-side submissions with SvelteKit's enhance action:

import { fail, redirect } from '@sveltejs/kit';
import { db } from '$lib/server/database';
import type { Actions } from './$types';

export const actions: Actions = {
    // matches action="?/create" on the form
    create: async ({ request, locals }) => {
        if (!locals.user) throw redirect(303, '/login');

        const data    = await request.formData();
        const title   = data.get('title')?.toString().trim();
        const content = data.get('content')?.toString().trim();

        if (!title || title.length < 3) {
            return fail(400, {
                error: 'Title must be at least 3 characters',
                fields: { title, content }
            });
        }

        await db.post.create({
            data: { title, content, authorId: locals.user.id }
        });

        throw redirect(303, '/blog');
    }
};
<script lang="ts">
    import { enhance } from '$app/forms';
    import type { ActionData } from './$types';

    export let form: ActionData;
</script>

<!-- use:enhance upgrades to fetch-based submission -->
<form method="POST" action="?/create" use:enhance>
    {#if form?.error}
        <p class="error">{form.error}</p>
    {/if}

    <label>
        Title
        <input
            name="title"
            value={form?.fields?.title ?? ''}
            required minlength="3"
        >
    </label>

    <label>
        Content
        <textarea name="content">{form?.fields?.content ?? ''}</textarea>
    </label>

    <button type="submit">Publish</button>
</form>
SvelteKit Request Lifecycle (SSR) Browser GET /blog/post Route Match blog/[slug] load() +page.server.ts Database Prisma / SQL Render +page.svelte HTML streamed to browser → client-side hydration

A page request: route matched, server load called, page rendered, HTML returned, then hydrated in the browser

Deploying SvelteKit with Adapters

SvelteKit's adapter system is what makes the same codebase deployable to radically different environments. An adapter transforms the SvelteKit build output into whatever format the target platform expects. Install the adapter and configure it in svelte.config.js.

adapter-auto detects your deployment platform automatically (Vercel, Netlify, Cloudflare, etc.) and applies the correct adapter. Best for zero-config deployments.

npm install -D @sveltejs/adapter-auto

// svelte.config.js
import adapter from '@sveltejs/adapter-auto';

export default {
    kit: { adapter: adapter() }
};

adapter-node produces a self-contained Node.js server. Great for Docker containers, VPS deployments, and anywhere you run your own infrastructure.

npm install -D @sveltejs/adapter-node

// svelte.config.js
import adapter from '@sveltejs/adapter-node';

export default {
    kit: {
        adapter: adapter({
            out: 'build',       // output directory
            precompress: true   // gzip + brotli static assets
        })
    }
};

// Build and run:
// npm run build
// node build/index.js

adapter-vercel deploys pages as serverless or edge functions. Each route can opt into the edge runtime for global low-latency execution.

npm install -D @sveltejs/adapter-vercel

// svelte.config.js
import adapter from '@sveltejs/adapter-vercel';

export default {
    kit: {
        adapter: adapter({
            runtime: 'edge'   // or 'nodejs20.x'
        })
    }
};

// Per-route override in +page.server.ts:
export const config = { runtime: 'nodejs20.x' };

adapter-static pre-renders your entire site to static HTML at build time. Perfect for blogs, documentation, and marketing sites with no runtime server needed.

npm install -D @sveltejs/adapter-static

// svelte.config.js
import adapter from '@sveltejs/adapter-static';

export default {
    kit: {
        adapter: adapter({
            pages: 'build',
            assets: 'build',
            fallback: 'index.html' // for SPAs with client-side routing
        }),
        prerender: {
            entries: ['*'] // pre-render all discovered pages
        }
    }
};

Building and Previewing

Build commands
# Development server with HMR
npm run dev

# Type-check without building
npm run check

# Production build
npm run build

# Preview the production build locally (uses adapter-node or adapter-auto)
npm run preview

Key Takeaways and Next Steps

SvelteKit gives you a coherent full-stack framework without the framework fatigue that comes with assembling a React stack from scratch. The file-system conventions are opinionated enough to eliminate decision paralysis, but flexible enough to handle real-world complexity. Here is a summary of the core ideas:

What to Explore Next

  • Svelte Stores: writable, readable, and derived stores for shared reactive state across components
  • Hooks: src/hooks.server.ts for authentication middleware, database client injection into locals, and request logging
  • Authentication: Lucia Auth or Auth.js (NextAuth for SvelteKit) for production-grade session management
  • Database with Prisma: Pair SvelteKit with Prisma ORM for type-safe database queries that complement SvelteKit's generated types
  • Testing: Vitest for unit tests, Playwright for end-to-end tests — both configured automatically by the project wizard
  • Streaming: SvelteKit supports streaming SSR, letting you send a shell immediately and stream slow data as it resolves
"SvelteKit is what happens when you design a framework for the web platform rather than around a runtime abstraction. Less framework, more web."
— Rich Harris, Creator of Svelte

SvelteKit's strength is in how little it asks you to learn that isn't already the web. HTTP verbs, form submissions, URL search params, cache headers — these are all first-class concepts in SvelteKit rather than things you work around. That alignment with the platform is why SvelteKit apps tend to be faster, smaller, and easier to reason about than their peers.

SvelteKit Svelte Full-Stack JavaScript SSR Vite Frontend
Mayur Dabhi

Mayur Dabhi

Full Stack Developer with 5+ years of experience building scalable web applications with Laravel, React, and Node.js.