The fastest way to build web apps. File-based routing, server-side rendering, and progressive enhancement by default.


๐Ÿš€ Project Structure

my-app/
โ”œโ”€โ”€ src/
โ”‚   โ”œโ”€โ”€ lib/
โ”‚   โ”‚   โ”œโ”€โ”€ components/     # Reusable components
โ”‚   โ”‚   โ”œโ”€โ”€ server/         # Server-only modules
โ”‚   โ”‚   โ”œโ”€โ”€ stores/         # State management
โ”‚   โ”‚   โ””โ”€โ”€ utils/          # Helper functions
โ”‚   โ”œโ”€โ”€ routes/
โ”‚   โ”‚   โ”œโ”€โ”€ +page.svelte    # Home page
โ”‚   โ”‚   โ”œโ”€โ”€ +page.ts        # Page load function
โ”‚   โ”‚   โ”œโ”€โ”€ +layout.svelte  # Root layout
โ”‚   โ”‚   โ”œโ”€โ”€ +layout.ts      # Layout load
โ”‚   โ”‚   โ”œโ”€โ”€ +error.svelte   # Error boundary
โ”‚   โ”‚   โ”œโ”€โ”€ api/
โ”‚   โ”‚   โ”‚   โ””โ”€โ”€ users/
โ”‚   โ”‚   โ”‚       โ””โ”€โ”€ +server.ts  # API endpoint
โ”‚   โ”‚   โ””โ”€โ”€ (group)/
โ”‚   โ”‚       โ””โ”€โ”€ +layout.svelte  # Route group
โ”‚   โ”œโ”€โ”€ app.html            # HTML template
โ”‚   โ””โ”€โ”€ app.d.ts            # Type declarations
โ”œโ”€โ”€ static/                 # Static assets
โ”œโ”€โ”€ tests/
โ”œโ”€โ”€ svelte.config.js
โ”œโ”€โ”€ vite.config.ts
โ””โ”€โ”€ tsconfig.json

๐Ÿ“ File Naming Convention

FilePurpose
+page.sveltePage component
+page.tsUniversal load function
+page.server.tsServer-only load
+layout.svelteLayout wrapper
+layout.tsLayout load data
+layout.server.tsServer layout load
+error.svelteError UI
+server.tsAPI endpoint
+hooks.tsClient hooks
+hooks.server.tsServer hooks

๐Ÿ”„ Data Loading

Universal Load (+page.ts)

Runs on server first, then client for SPA navigation.

// src/routes/blog/[slug]/+page.ts
import type { PageLoad } from "./$types"
import { error } from "@sveltejs/kit"
 
export const load: PageLoad = async ({ params, fetch }) => {
  const response = await fetch(`/api/posts/${params.slug}`)
 
  if (!response.ok) {
    throw error(response.status, "Post not found")
  }
 
  return {
    post: await response.json(),
    streamed: {
      comments: fetch(`/api/comments/${params.slug}`).then((r) => r.json()),
    },
  }
}
<!-- +page.svelte -->
<script>
  let { data } = $props()
</script>
 
<h1>{data.post.title}</h1>
 
{#await data.streamed.comments}
  <p>Loading comments...</p>
{:then comments}
  <CommentList {comments} />
{/await}

Server Load (+page.server.ts)

Server-only, can use private env vars, database directly.

// src/routes/dashboard/+page.server.ts
import type { PageServerLoad } from "./$types"
import { db } from "$lib/server/db"
import { redirect } from "@sveltejs/kit"
 
export const load: PageServerLoad = async ({ locals, url }) => {
  // Check auth
  if (!locals.user) {
    throw redirect(302, `/login?redirectTo=${url.pathname}`)
  }
 
  // Direct DB access (no API call needed)
  const projects = await db.query("SELECT * FROM projects WHERE user_id = $1", [locals.user.id])
 
  return {
    projects,
    user: locals.user,
  }
}

Layout Data

// src/routes/+layout.ts
import type { LayoutLoad } from "./$types"
 
export const load: LayoutLoad = async ({ fetch }) => {
  const settings = await fetch("/api/settings").then((r) => r.json())
 
  return {
    settings,
    // Available to all child routes
  }
}
<!-- +layout.svelte -->
<script>
  let { data, children } = $props()
</script>
 
<Nav theme={data.settings.theme} />
{@render children()}
<Footer />

๐Ÿ”Œ API Endpoints

REST API (+server.ts)

// src/routes/api/users/+server.ts
import type { RequestHandler } from "./$types"
import { json, error } from "@sveltejs/kit"
import { db } from "$lib/server/db"
 
// GET /api/users
export const GET: RequestHandler = async ({ url }) => {
  const page = Number(url.searchParams.get("page")) || 1
  const limit = Math.min(Number(url.searchParams.get("limit")) || 10, 100)
 
  const users = await db.users.findMany({
    take: limit,
    skip: (page - 1) * limit,
  })
 
  return json({ users, page, limit })
}
 
// POST /api/users
export const POST: RequestHandler = async ({ request }) => {
  const body = await request.json()
 
  // Validation
  if (!body.email?.includes("@")) {
    throw error(400, "Valid email required")
  }
 
  const user = await db.users.create({ data: body })
 
  return json(user, { status: 201 })
}
 
// PATCH /api/users/:id
// DELETE /api/users/:id

Form Actions (+page.server.ts)

// src/routes/login/+page.server.ts
import type { Actions } from "./$types"
import { fail, redirect } from "@sveltejs/kit"
import { auth } from "$lib/server/auth"
 
export const actions: Actions = {
  default: async ({ request, cookies }) => {
    const data = await request.formData()
    const email = data.get("email")
    const password = data.get("password")
 
    // Validation
    if (!email || !password) {
      return fail(400, {
        email: email?.toString() || "",
        error: "Email and password required",
      })
    }
 
    // Auth
    const user = await auth.verify(email.toString(), password.toString())
    if (!user) {
      return fail(400, {
        email: email.toString(),
        error: "Invalid credentials",
      })
    }
 
    // Set session
    const session = await auth.createSession(user.id)
    cookies.set("sessionid", session.id, {
      path: "/",
      httpOnly: true,
      sameSite: "strict",
      secure: process.env.NODE_ENV === "production",
      maxAge: 60 * 60 * 24 * 7, // 1 week
    })
 
    throw redirect(302, "/dashboard")
  },
}
<!-- +page.svelte -->
<script>
  let { form } = $props()
</script>
 
<form method="POST">
  <input name="email" type="email" value={form?.email ?? ''} />
  <input name="password" type="password" />
  <button type="submit">Login</button>
 
  {#if form?.error}
    <p class="error">{form.error}</p>
  {/if}
</form>

Named Actions

// Multiple actions on same page
export const actions: Actions = {
  create: async ({ request }) => {
    /* ... */
  },
  update: async ({ request }) => {
    /* ... */
  },
  delete: async ({ request }) => {
    /* ... */
  },
}
<!-- Use ?/actionName -->
<form method="POST" action="?/create">
  <button>Create</button>
</form>
 
<form method="POST" action="?/delete">
  <button>Delete</button>
</form>

๐Ÿ” Authentication & Hooks

Server Hooks (hooks.server.ts)

// src/hooks.server.ts
import type { Handle } from "@sveltejs/kit"
import { auth } from "$lib/server/auth"
 
export const handle: Handle = async ({ event, resolve }) => {
  // Get session from cookie
  const sessionId = event.cookies.get("sessionid")
 
  if (sessionId) {
    const session = await auth.getSession(sessionId)
    if (session) {
      event.locals.user = session.user
      event.locals.session = session
    }
  }
 
  // Protected routes
  if (event.url.pathname.startsWith("/admin")) {
    if (!event.locals.user?.isAdmin) {
      return new Response("Unauthorized", { status: 403 })
    }
  }
 
  const response = await resolve(event)
  return response
}

Type-Safe Locals

// src/app.d.ts
declare global {
  namespace App {
    interface Locals {
      user?: {
        id: string
        email: string
        isAdmin: boolean
      }
      session?: {
        id: string
        expiresAt: Date
      }
    }
 
    interface PageData {
      // Automatic type from load functions
    }
 
    interface Error {
      message: string
      code?: string
    }
  }
}
 
export {}

๐ŸŒ Advanced Patterns

Streaming

// +page.server.ts
export const load: PageServerLoad = async () => {
  return {
    // Stream slow data
    slowData: new Promise((resolve) => {
      setTimeout(() => resolve("loaded!"), 2000)
    }),
  }
}
<!-- Automatic streaming -->
{#await data.slowData}
  <Skeleton />
{:then value}
  <Content {value} />
{/await}

Parallel Data Loading

// Fetch in parallel
export const load: PageLoad = async ({ fetch }) => {
  const [users, posts, settings] = await Promise.all([
    fetch("/api/users").then((r) => r.json()),
    fetch("/api/posts").then((r) => r.json()),
    fetch("/api/settings").then((r) => r.json()),
  ])
 
  return { users, posts, settings }
}

Route Groups

src/routes/
โ”œโ”€โ”€ (marketing)/           # No URL segment
โ”‚   โ”œโ”€โ”€ +layout.svelte     # Marketing layout
โ”‚   โ”œโ”€โ”€ +page.svelte       # /
โ”‚   โ”œโ”€โ”€ about/
โ”‚   โ”‚   โ””โ”€โ”€ +page.svelte   # /about
โ”‚   โ””โ”€โ”€ pricing/
โ”‚       โ””โ”€โ”€ +page.svelte   # /pricing
โ”œโ”€โ”€ (app)/
โ”‚   โ”œโ”€โ”€ +layout.svelte     # App layout (with sidebar)
โ”‚   โ”œโ”€โ”€ dashboard/
โ”‚   โ”‚   โ””โ”€โ”€ +page.svelte   # /dashboard
โ”‚   โ””โ”€โ”€ settings/
โ”‚       โ””โ”€โ”€ +page.svelte   # /settings
โ””โ”€โ”€ +layout.svelte         # Root layout

Dynamic Routes

// src/routes/blog/[slug]/+page.ts
export const load: PageLoad = async ({ params }) => {
  // params.slug available
  console.log(params.slug) // 'my-post'
}
// src/routes/blog/[...path]/+page.ts
// Matches: /blog/a, /blog/a/b, /blog/a/b/c
export const load: PageLoad = async ({ params }) => {
  console.log(params.path) // 'a/b/c'
}

Optional Parameters

src/routes/
โ””โ”€โ”€ lang/
    โ””โ”€โ”€ [[lang]]/
        โ””โ”€โ”€ +page.svelte   # Matches: /lang, /lang/en, /lang/de

โšก Configuration

Svelte Config

// svelte.config.js
import adapter from "@sveltejs/adapter-auto"
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"
 
/** @type {import('@sveltejs/kit').Config} */
const config = {
  preprocess: vitePreprocess(),
  kit: {
    adapter: adapter(),
 
    alias: {
      $lib: "./src/lib",
      "$lib/*": "./src/lib/*",
      $components: "./src/lib/components",
      $stores: "./src/lib/stores",
    },
 
    files: {
      assets: "static",
      lib: "src/lib",
      routes: "src/routes",
      appTemplate: "src/app.html",
    },
  },
}
 
export default config

Adapters

// Vercel
import adapter from "@sveltejs/adapter-vercel"
 
// Netlify
import adapter from "@sveltejs/adapter-netlify"
 
// Static (SPA mode)
import adapter from "@sveltejs/adapter-static"
 
// Node server
import adapter from "@sveltejs/adapter-node"

๐Ÿงช Testing

// src/routes/api/users/+server.test.ts
import { describe, it, expect } from "vitest"
import { GET } from "./+server"
 
describe("/api/users", () => {
  it("returns users list", async () => {
    const response = await GET({ url: new URL("http://localhost/api/users") })
    const data = await response.json()
 
    expect(data.users).toBeDefined()
    expect(Array.isArray(data.users)).toBe(true)
  })
})

๐ŸŽฏ Best Practices

  1. Use server loads for private data โ€” no API calls needed
  2. Stream slow data โ€” better perceived performance
  3. Validate forms server-side โ€” always, then enhance client-side
  4. Type your locals โ€” extends App.Locals in app.d.ts
  5. Use route groups โ€” share layouts without URL segments
  6. Handle errors gracefully โ€” +error.svelte boundaries
  7. Progressive enhancement โ€” forms work without JS

๐Ÿ”— Resources