Building Full-Stack Apps with SvelteKit: The Complete Guide

Learn how to build production-ready full-stack applications with SvelteKit, covering routing, server-side rendering, API routes, authentication, and deployment.

Hero image for Building Full-Stack Apps with SvelteKit: The Complete Guide

SvelteKit is the official application framework for Svelte. While Svelte is the component framework, SvelteKit provides the full-stack layer: routing, server-side rendering, API routes, and deployment adapters. Together, they offer an incredibly productive development experience.

This guide covers everything you need to build production-ready applications with SvelteKit.

Why SvelteKit?

SvelteKit brings together the best of modern web development:

  • Svelte’s reactivity — No virtual DOM, compiles to vanilla JavaScript
  • File-based routing — Your folder structure is your routes
  • Server-Side Rendering (SSR) — Great SEO and initial load performance
  • API Routes — Backend and frontend in one project
  • TypeScript — First-class support throughout

Project Setup

npm create svelte@latest my-app
cd my-app
npm install
npm run dev

During setup, choose Skeleton project for a clean start, and enable TypeScript and ESLint.

File-Based Routing

SvelteKit’s routing is based on the filesystem:

src/routes/
├── +page.svelte          → /
├── about/
│   └── +page.svelte      → /about
├── blog/
│   ├── +page.svelte      → /blog
│   └── [slug]/
│       └── +page.svelte  → /blog/[slug]
└── api/
    └── users/
        └── +server.ts    → /api/users (API endpoint)

Special Route Files

FilePurpose
+page.sveltePage component
+page.tsPage load function (runs on server + client)
+page.server.tsServer-only load function
+layout.svelteShared layout wrapper
+layout.tsLayout load function
+server.tsAPI endpoint (GET, POST, etc.)
+error.svelteError page

Loading Data

The +page.ts (or +page.server.ts) file exports a load function that fetches data for the page:

// src/routes/blog/+page.ts
import type { PageLoad } from "./$types";

export const load: PageLoad = async ({ fetch }) => {
  const response = await fetch("/api/posts");
  const posts = await response.json();

  return {
    posts, // Available as `data.posts` in the page
  };
};

In the page component, use the data prop:

<!-- src/routes/blog/+page.svelte -->
<script lang="ts">
  import type { PageData } from './$types';

  let { data }: { data: PageData } = $props();
</script>

{#each data.posts as post}
  <article>
    <h2>{post.title}</h2>
    <p>{post.description}</p>
  </article>
{/each}

Server-Only Load Functions

Use +page.server.ts for database queries and sensitive operations:

// src/routes/dashboard/+page.server.ts
import { db } from "$lib/db";
import { redirect } from "@sveltejs/kit";
import type { PageServerLoad } from "./$types";

export const load: PageServerLoad = async ({ locals }) => {
  // Access session from locals (set by auth hook)
  if (!locals.user) {
    redirect(302, "/login");
  }

  const stats = await db.query.analytics.findMany({
    where: { userId: locals.user.id },
    orderBy: { date: "desc" },
    limit: 30,
  });

  return { stats };
};

API Routes

SvelteKit lets you create API endpoints using +server.ts files:

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

export const GET: RequestHandler = async ({ url }) => {
  const page = Number(url.searchParams.get("page") ?? "1");
  const limit = 10;

  const posts = await db.query.posts.findMany({
    where: { published: true },
    orderBy: { publishedAt: "desc" },
    limit,
    offset: (page - 1) * limit,
  });

  return json(posts);
};

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

  const body = await request.json();

  const post = await db
    .insert(posts)
    .values({
      title: body.title,
      content: body.content,
      authorId: locals.user.id,
      publishedAt: new Date(),
    })
    .returning();

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

Forms and Actions

SvelteKit’s Form Actions provide a progressive enhancement-friendly way to handle form submissions:

// src/routes/contact/+page.server.ts
import { fail } from "@sveltejs/kit";
import type { Actions } from "./$types";
import { sendEmail } from "$lib/email";

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?.includes("@")) {
      return fail(400, {
        email,
        message,
        error: "Please provide a valid email address",
      });
    }

    await sendEmail({ to: "hello@kingtechnologies.dev", from: email, message });

    return { success: true };
  },
};

In the page, the form works without JavaScript (progressive enhancement):

<!-- src/routes/contact/+page.svelte -->
<script lang="ts">
  import { enhance } from '$app/forms';
  import type { ActionData } from './$types';

  let { form }: { form: ActionData } = $props();
</script>

<form method="POST" use:enhance>
  {#if form?.error}
    <p class="error">{form.error}</p>
  {/if}

  {#if form?.success}
    <p class="success">Message sent!</p>
  {/if}

  <label>
    Email
    <input type="email" name="email" value={form?.email ?? ''} required />
  </label>

  <label>
    Message
    <textarea name="message" required>{form?.message ?? ''}</textarea>
  </label>

  <button type="submit">Send</button>
</form>

Layouts and Nested Routing

Layouts wrap pages and persist across navigation:

<!-- src/routes/+layout.svelte -->
<script lang="ts">
  import Header from '$lib/components/Header.svelte';
  import Footer from '$lib/components/Footer.svelte';

  let { children } = $props();
</script>

<Header />
<main>
  {@render children()}
</main>
<Footer />

Nested Layouts

src/routes/
├── +layout.svelte        # Root layout (header + footer)
├── blog/
│   ├── +layout.svelte    # Blog layout (sidebar + main)
│   ├── +page.svelte      # /blog — wrapped by BOTH layouts
│   └── [slug]/
│       └── +page.svelte  # /blog/[slug] — also wrapped by both
└── dashboard/
    ├── +layout.svelte    # Dashboard layout (sidebar nav)
    └── +page.svelte      # /dashboard

Hooks: The Middleware Layer

hooks.server.ts runs on every server request — perfect for authentication:

// src/hooks.server.ts
import type { Handle } from "@sveltejs/kit";
import { verifyToken } from "$lib/auth";

export const handle: Handle = async ({ event, resolve }) => {
  const token = event.cookies.get("auth-token");

  if (token) {
    const user = await verifyToken(token);
    event.locals.user = user ?? null;
  }

  const response = await resolve(event);

  // Add security headers
  response.headers.set("X-Frame-Options", "DENY");
  response.headers.set("X-Content-Type-Options", "nosniff");

  return response;
};

Stores and State Management

Svelte’s reactive stores work naturally with SvelteKit:

// src/lib/stores/cart.ts
import { writable, derived } from "svelte/store";

export interface CartItem {
  id: string;
  name: string;
  price: number;
  quantity: number;
}

const createCartStore = () => {
  const { subscribe, update, set } = writable<CartItem[]>([]);

  return {
    subscribe,
    addItem: (item: Omit<CartItem, "quantity">) =>
      update((items) => {
        const existing = items.find((i) => i.id === item.id);
        if (existing) {
          return items.map((i) =>
            i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i,
          );
        }
        return [...items, { ...item, quantity: 1 }];
      }),
    removeItem: (id: string) =>
      update((items) => items.filter((i) => i.id !== id)),
    clear: () => set([]),
  };
};

export const cart = createCartStore();

export const cartTotal = derived(cart, ($cart) =>
  $cart.reduce((total, item) => total + item.price * item.quantity, 0),
);

export const cartCount = derived(cart, ($cart) =>
  $cart.reduce((count, item) => count + item.quantity, 0),
);

Error Handling

SvelteKit has excellent built-in error handling:

// For expected errors (shown to user)
import { error } from "@sveltejs/kit";
error(404, "Post not found");

// For unexpected errors (generic 500 page)
throw new Error("Database connection failed");

Create a custom error page:

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

<h1>{$page.status}: {$page.error?.message}</h1>

{#if $page.status === 404}
  <p>The page you're looking for doesn't exist.</p>
  <a href="/">Go home</a>
{/if}

Deployment

SvelteKit adapts to your deployment target:

# Node.js server
npm install @sveltejs/adapter-node

# Vercel (auto-detected)
npm install @sveltejs/adapter-vercel

# Static site generation
npm install @sveltejs/adapter-static

# Cloudflare Pages/Workers
npm install @sveltejs/adapter-cloudflare
// svelte.config.js
import adapter from "@sveltejs/adapter-vercel";

export default {
  kit: {
    adapter: adapter({
      runtime: "nodejs22.x",
    }),
  },
};

Performance Tips

  1. Preload routes with data-sveltekit-preload-data:
<a href="/blog" data-sveltekit-preload-data="hover">Blog</a>
  1. Stream slow data to avoid blocking the initial render:
// +page.server.ts
export const load: PageServerLoad = async () => {
  return {
    // Fast data loads immediately
    user: await getUser(),
    // Slow data streams in — page renders before this resolves
    analytics: getAnalytics(), // No await!
  };
};
  1. Use $app/stores for accessing page info without prop drilling:
<script>
  import { page } from '$app/stores';
</script>

<!-- Active link based on current URL -->
<a href="/blog" class:active={$page.url.pathname.startsWith('/blog')}>Blog</a>

Conclusion

SvelteKit is a complete framework for building modern web applications. Its file-based routing, built-in SSR, form actions, and TypeScript support make it incredibly productive once you understand the conventions.

The framework scales from simple static sites to complex full-stack applications with databases, authentication, and real-time features. Combined with Svelte’s minimal JavaScript output, the result is applications that are fast by default.

Start with a simple project, learn the routing and data loading patterns, then gradually add server-side features as needed. SvelteKit’s progressive nature means you only add complexity when you actually need it.