Web Development

SvelteKit: Full-Stack Svelte

Tutorial lengkap SvelteKit dari nol β€” file-based routing, load functions, form actions, server routes, adapters, dan deployment dengan contoh kode praktis

1. Pengenalan SvelteKit

SvelteKit adalah framework full-stack resmi untuk Svelte. Dikembangkan oleh Rich Harris (pembuat Svelte dan sekarang bekerja di Vercel), SvelteKit menyediakan semua yang Anda butuhkan untuk membangun aplikasi web production-ready: routing, SSR, SSG, API routes, dan deployment ke berbagai platform.

Yang membuat SvelteKit istimewa adalah filosofinya yang berbeda dari React atau Vue: Svelte memindahkan kompilasi ke build time, sehingga bundle yang dikirim ke browser sangat kecil dan tidak memerlukan virtual DOM runtime.

Mengapa Memilih SvelteKit?

Keunggulan Penjelasan
Zero Bundle SizeTidak ada virtual DOM runtime β€” komponen dikompilasi menjadi vanilla JS
Sintaks SederhanaSvelte menggunakan sintaks yang mendekati HTML/CSS/JS biasa
Reactive by DefaultCukup gunakan $: untuk reactivity tanpa useState/ref
Form ActionsBuilt-in server-side form handling β€” bahkan berfungsi tanpa JavaScript!
Flexible RenderingSSR, SSG, SPA, atau hybrid β€” bisa diatur per-route
Adapter SystemDeploy ke Vercel, Netlify, Cloudflare, Node.js, atau static

SvelteKit vs Alternatif Lain

Aspek SvelteKit Next.js Nuxt.js
UI LibrarySvelteReactVue.js
Runtime Size~2 KB (minimal)~42 KB~33 KB
ReactivityCompile-timeRuntime (hooks)Runtime (proxy)
Form Handlingβœ… Built-in actions❌ Manual (Server Actions)❌ Manual
Learning Curve🟒 Mudah🟑 Sedang🟒 Mudah
Compile ApproachAhead-of-TimeJust-in-TimeJust-in-Time
Diagram: Arsitektur SvelteKit
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                  SVELTEKIT APPLICATION                   β”‚
β”‚                                                         β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚                 Svelte Components                  β”‚  β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚  β”‚
β”‚  β”‚  β”‚ +page   β”‚  β”‚ +layout  β”‚  β”‚ +error          β”‚  β”‚  β”‚
β”‚  β”‚  β”‚ .svelte β”‚  β”‚ .svelte  β”‚  β”‚ .svelte         β”‚  β”‚  β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚          β”‚                                               β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚              SvelteKit Core (Vite)                β”‚  β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚  β”‚
β”‚  β”‚  β”‚  Vite    β”‚  β”‚  Routing  β”‚  β”‚   Adapters    β”‚  β”‚  β”‚
β”‚  β”‚  β”‚ (bundler)β”‚  β”‚ (file-    β”‚  β”‚  (deploy to   β”‚  β”‚  β”‚
β”‚  β”‚  β”‚          β”‚  β”‚  based)   β”‚  β”‚   platform)   β”‚  β”‚  β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚                                                         β”‚
β”‚  Render:  SSR (server)  β”‚  SSG (prerender)  β”‚  SPA      β”‚
β”‚  Deploy:  Node β”‚ Vercel β”‚ Netlify β”‚ Cloudflare β”‚ Static  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

2. Instalasi dan Setup

Membuat Proyek Baru

Bash
# Membuat proyek SvelteKit baru
npm create svelte@latest my-svelte-app

# Wizard akan menanyakan:
# 1. Template: Skeleton project / Demo app / Library
# 2. TypeScript: Yes / No
# 3. ESLint: Yes / No
# 4. Prettier: Yes / No
# 5. Playwright (testing): Yes / No

# Masuk ke direktori
cd my-svelte-app

# Install dependencies
npm install

# Jalankan development server
npm run dev

# Output:
#   VITE v5.x  ready in 300 ms
#   ➜  Local:   http://localhost:5173/

Struktur Proyek SvelteKit

File Structure
my-svelte-app/
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ lib/                    ← Shared library (auto-imported $lib)
β”‚   β”‚   β”œβ”€β”€ components/         ← Reusable components
β”‚   β”‚   β”‚   β”œβ”€β”€ Header.svelte
β”‚   β”‚   β”‚   β”œβ”€β”€ Footer.svelte
β”‚   β”‚   β”‚   └── Card.svelte
β”‚   β”‚   β”œβ”€β”€ server/             ← Server-only code ($lib/server)
β”‚   β”‚   β”‚   └── db.js
β”‚   β”‚   └── utils/              ← Utility functions
β”‚   β”‚       └── format.js
β”‚   β”œβ”€β”€ routes/                 ← File-based routing
β”‚   β”‚   β”œβ”€β”€ +page.svelte        ← / (home page)
β”‚   β”‚   β”œβ”€β”€ +layout.svelte      ← Root layout
β”‚   β”‚   β”œβ”€β”€ about/
β”‚   β”‚   β”‚   └── +page.svelte    ← /about
β”‚   β”‚   └── blog/
β”‚   β”‚       β”œβ”€β”€ +page.svelte    ← /blog
β”‚   β”‚       └── [slug]/
β”‚   β”‚           β”œβ”€β”€ +page.svelte         ← /blog/:slug
β”‚   β”‚           β”œβ”€β”€ +page.server.js      ← Server load + actions
β”‚   β”‚           └── +error.svelte        ← Error page
β”‚   β”œβ”€β”€ app.html                ← HTML template
β”‚   └── app.d.ts                ← TypeScript declarations
β”œβ”€β”€ static/                     ← Static files (tidak diproses)
β”‚   └── favicon.png
β”œβ”€β”€ svelte.config.js            ← Konfigurasi SvelteKit
β”œβ”€β”€ vite.config.js              ← Konfigurasi Vite
β”œβ”€β”€ package.json
└── tsconfig.json

Konfigurasi SvelteKit

svelte.config.js
// svelte.config.js
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';

/** @type {import('@sveltejs/kit').Config} */
const config = {
  // Preprocess β€” support untuk TypeScript, PostCSS, dll
  preprocess: vitePreprocess(),

  kit: {
    // Adapter menentukan target deployment
    // adapter-auto mendeteksi platform otomatis
    adapter: adapter(),

    // Alias untuk import paths
    alias: {
      '$components': 'src/lib/components',
      '$utils': 'src/lib/utils'
    }
  }
};

export default config;
πŸ’‘ Tips

Folder src/lib/ secara otomatis bisa diakses dengan alias $lib di seluruh proyek. Ini sangat nyaman untuk berbagi komponen dan utility functions antar halaman.

3. File-Based Routing

SvelteKit menggunakan sistem routing berbasis file yang intuitif. Setiap folder di src/routes/ menjadi route, dan file dengan nama khusus (+page.svelte, +page.server.js, dll.) menentukan perilakunya.

Konvensi Penamaan File

File Fungsi
+page.svelteKomponen UI yang dirender untuk route tersebut
+page.jsLoad function (universal β€” server + client)
+page.server.jsLoad function + form actions (server only)
+layout.svelteLayout wrapper untuk halaman dan child routes
+layout.server.jsLayout load function (server only)
+error.svelteHalaman error untuk route tersebut
+server.jsAPI endpoint (server only, no UI)

Struktur Routing

File Structure β†’ Routes
src/routes/
β”œβ”€β”€ +page.svelte                    β†’ /
β”œβ”€β”€ +layout.svelte                  β†’ (root layout untuk semua halaman)
β”‚
β”œβ”€β”€ about/
β”‚   └── +page.svelte                β†’ /about
β”‚
β”œβ”€β”€ blog/
β”‚   β”œβ”€β”€ +page.svelte                β†’ /blog
β”‚   β”œβ”€β”€ +page.server.js             β†’ (load data untuk /blog)
β”‚   └── [slug]/
β”‚       β”œβ”€β”€ +page.svelte            β†’ /blog/my-post
β”‚       β”œβ”€β”€ +page.server.js         β†’ (load data per slug)
β”‚       └── +error.svelte           β†’ (error handler)
β”‚
β”œβ”€β”€ products/
β”‚   β”œβ”€β”€ +page.svelte                β†’ /products
β”‚   └── [[category]]/
β”‚       └── +page.svelte            β†’ /products atau /products/shoes
β”‚                                     ([[ ]] = opsional parameter)
β”‚
β”œβ”€β”€ [...catchall]/
β”‚   └── +page.svelte                β†’ /* (catch-all route)
β”‚
└── api/
    β”œβ”€β”€ users/
    β”‚   └── +server.js              β†’ GET/POST /api/users
    └── auth/
        └── login/
            └── +server.js          β†’ POST /api/auth/login

Dynamic Routes

src/routes/blog/[slug]/+page.svelte
<!-- Dynamic route: /blog/:slug -->
<script>
  // data di-pass dari +page.server.js load function
  export let data;
</script>

<svelte:head>
  <title>{data.post.title} | BeebaneLabs</title>
  <meta name="description" content={data.post.excerpt} />
</svelte:head>

<article class="blog-post">
  <h1>{data.post.title}</h1>
  <p class="meta">
    Ditulis oleh {data.post.author} Β· {data.post.date}
  </p>
  <div class="content">
    {@html data.post.content}
  </div>
</article>

<style>
  .blog-post {
    max-width: 800px;
    margin: 0 auto;
    padding: 2rem;
  }

  .meta {
    color: #888;
    font-size: 0.9rem;
    margin-bottom: 2rem;
  }

  .content :global(h2) {
    margin-top: 2rem;
    color: var(--heading-color);
  }

  .content :global(p) {
    line-height: 1.8;
  }
</style>

Navigasi di Svelte

src/lib/components/Nav.svelte
<script>
  import { page } from '$app/stores';
</script>

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

  <!-- Link dengan prefetching -->
  <a href="/products" data-sveltekit-preload-data>
    Produk
  </a>
</nav>

<style>
  nav {
    display: flex;
    gap: 1.5rem;
    padding: 1rem;
  }

  a {
    color: var(--text-color);
    text-decoration: none;
    padding: 0.5rem 1rem;
    border-radius: 8px;
    transition: all 0.2s;
  }

  a:hover {
    background: var(--hover-bg);
  }

  a.active {
    background: var(--primary-color);
    color: white;
  }
</style>

Programmatic Navigation

Svelte β€” goto()
<script>
  import { goto, invalidate, invalidateAll } from '$app/navigation';

  async function handleLogin() {
    const response = await fetch('/api/auth/login', {
      method: 'POST',
      body: JSON.stringify({ email, password })
    });

    if (response.ok) {
      // Redirect ke dashboard
      await goto('/dashboard');
    }
  }

  async function handleLogout() {
    await fetch('/api/auth/logout', { method: 'POST' });

    // Invalidate semua load data β€” trigger re-fetch
    await invalidateAll();

    // Redirect ke home
    await goto('/');
  }

  async function refreshData() {
    // Invalidate specific URL β€” trigger re-fetch hanya untuk URL tersebut
    await invalidate('/api/products');
  }
</script>

<button on:click={handleLogin}>Login</button>
<button on:click={handleLogout}>Logout</button>
<button on:click={refreshData}>Refresh Data</button>

4. Load Functions

Load functions adalah cara SvelteKit untuk mengambil data sebelum halaman dirender. Ada dua jenis: universal load (+page.js) yang berjalan di server dan client, dan server load (+page.server.js) yang hanya berjalan di server.

Server Load Function

src/routes/blog/[slug]/+page.server.js
// src/routes/blog/[slug]/+page.server.js
// Hanya berjalan di server β€” aman untuk database, API keys, dll

import { error } from '@sveltejs/kit';
import { db } from '$lib/server/db.js';

/** @type {import('./$types').PageServerLoad} */
export async function load({ params, setHeaders }) {
  // params.slug = nilai dari URL /blog/[slug]
  const { slug } = params;

  // Fetch dari database
  const post = await db.posts.findUnique({
    where: { slug },
    include: { author: true, tags: true }
  });

  // Jika post tidak ditemukan, throw error 404
  if (!post) {
    throw error(404, {
      message: 'Artikel tidak ditemukan'
    });
  }

  // Set cache headers
  setHeaders({
    'Cache-Control': 'public, max-age=3600'
  });

  // Return data β€” tersedia di +page.svelte sebagai `data`
  return {
    post: {
      title: post.title,
      content: post.content,
      excerpt: post.excerpt,
      author: post.author.name,
      date: post.createdAt.toLocaleDateString('id-ID'),
      tags: post.tags.map(t => t.name)
    }
  };
}

Universal Load Function

src/routes/products/+page.js
// src/routes/products/+page.js
// Berjalan di server (SSR) DAN client (navigasi SPA)

/** @type {import('./$types').PageLoad} */
export async function load({ fetch, url }) {
  // fetch() β€” SvelteKit's enhanced fetch
  // Otomatis handle cookies dan relative URLs saat SSR

  const category = url.searchParams.get('category') || 'all';
  const page = url.searchParams.get('page') || '1';

  const response = await fetch(
    `/api/products?category=${category}&page=${page}`
  );
  const products = await response.json();

  return {
    products: products.items,
    pagination: products.pagination,
    currentCategory: category
  };
}

Menggunakan Data di Komponen

src/routes/blog/[slug]/+page.svelte
<script>
  // data otomatis di-pass dari load function
  /** @type {import('./$types').PageData} */
  export let data;
</script>

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

<article>
  <h1>{data.post.title}</h1>
  <p>Oleh {data.post.author} Β· {data.post.date}</p>

  <div class="tags">
    {#each data.post.tags as tag}
      <span class="tag">{tag}</span>
    {/each}
  </div>

  <div class="content">
    {@html data.post.content}
  </div>
</article>

Parent Load Data

src/routes/blog/[slug]/+page.server.js β€” Parent Data
// Akses data dari parent layout load function
export async function load({ params, parent }) {
  // Ambil data dari +layout.server.js di atas
  const parentData = await parent();
  const currentUser = parentData.user;

  const post = await db.posts.findUnique({
    where: { slug: params.slug }
  });

  // Cek apakah user bisa edit post ini
  const canEdit = currentUser?.id === post.authorId ||
                  currentUser?.role === 'admin';

  return {
    post,
    canEdit
  };
}
⚠️ Bedakan Universal dan Server Load

Universal load (+page.js) berjalan di server DAN client β€” jangan letakkan kode sensitif di sini! Server load (+page.server.js) hanya berjalan di server β€” aman untuk database queries, API keys, dan operasi yang membutuhkan autentikasi. Server load data secara otomatis di-strip dari data yang dikirim ke client (tidak bisa diakses hacker).

5. Form Actions

Form Actions adalah salah satu fitur terbaik SvelteKit. Ini memungkinkan Anda menangani form submissions di server tanpa perlu menulis JavaScript client-side β€” bahkan tetap berfungsi jika JavaScript dimatikan di browser!

Membuat Form Action

src/routes/contact/+page.server.js
// src/routes/contact/+page.server.js

import { fail } from '@sveltejs/kit';
import { sendEmail } from '$lib/server/email.js';

/** @type {import('./$types').Actions} */
export const actions = {
  // Default action β€” dipanggil saat form action="?/contact"
  contact: async ({ request }) => {
    // Ambil data dari form submission
    const formData = await request.formData();
    const name = formData.get('name');
    const email = formData.get('email');
    const message = formData.get('message');

    // Validasi server-side
    const errors = {};

    if (!name || name.length < 2) {
      errors.name = 'Nama harus minimal 2 karakter';
    }

    if (!email || !email.includes('@')) {
      errors.email = 'Email tidak valid';
    }

    if (!message || message.length < 10) {
      errors.message = 'Pesan harus minimal 10 karakter';
    }

    // Jika ada error, return data dengan fail()
    if (Object.keys(errors).length > 0) {
      return fail(400, {
        errors,
        // Preserve form values agar user tidak perlu mengetik ulang
        values: { name, email, message }
      });
    }

    // Kirim email
    try {
      await sendEmail({ name, email, message });
    } catch (err) {
      return fail(500, {
        error: 'Gagal mengirim email. Coba lagi nanti.',
        values: { name, email, message }
      });
    }

    // Return success
    return { success: true };
  }
};

Form di Svelte Component

src/routes/contact/+page.svelte
<script>
  import { enhance } from '$app/forms';

  /** @type {import('./$types').PageData} */
  export let data;

  /** @type {import('./$types').ActionData} */
  export let form;

  let loading = false;
</script>

<svelte:head>
  <title>Hubungi Kami</title>
</svelte:head>

<h1>Hubungi Kami</h1>

{#if form?.success}
  <div class="alert success">
    βœ… Pesan berhasil dikirim! Kami akan segera membalas.
  </div>
{/if}

{#if form?.error}
  <div class="alert error">
    ❌ {form.error}
  </div>
{/if}

<!--
  use:enhance = progressive enhancement
  Form tetap berfungsi tanpa JS, tapi dengan JS
  mendapatkan UX yang lebih baik (no full page reload)
-->
<form method="POST" action="?/contact" use:enhance={() => {
  loading = true;

  return async ({ result, update }) => {
    loading = false;
    await update();
  };
}}>
  <label>
    Nama
    <input
      type="text"
      name="name"
      value={form?.values?.name ?? ''}
      class:error={form?.errors?.name}
      required
    />
    {#if form?.errors?.name}
      <span class="error-msg">{form.errors.name}</span>
    {/if}
  </label>

  <label>
    Email
    <input
      type="email"
      name="email"
      value={form?.values?.email ?? ''}
      class:error={form?.errors?.email}
      required
    />
    {#if form?.errors?.email}
      <span class="error-msg">{form.errors.email}</span>
    {/if}
  </label>

  <label>
    Pesan
    <textarea
      name="message"
      rows="5"
      class:error={form?.errors?.message}
      required
    >{form?.values?.message ?? ''}</textarea>
    {#if form?.errors?.message}
      <span class="error-msg">{form.errors.message}</span>
    {/if}
  </label>

  <button type="submit" disabled={loading}>
    {loading ? 'Mengirim...' : 'Kirim Pesan'}
  </button>
</form>

<style>
  .error-msg {
    color: #ef4444;
    font-size: 0.85rem;
    margin-top: 4px;
  }

  input.error, textarea.error {
    border-color: #ef4444;
  }

  .alert.success {
    background: #065f46;
    color: #a7f3d0;
    padding: 1rem;
    border-radius: 8px;
    margin-bottom: 1rem;
  }

  .alert.error {
    background: #7f1d1d;
    color: #fca5a5;
    padding: 1rem;
    border-radius: 8px;
    margin-bottom: 1rem;
  }
</style>
πŸ’‘ Tips

Form actions di SvelteKit mendukung progressive enhancement. Tanpa JavaScript, form bekerja seperti form HTML biasa (full page reload). Dengan use:enhance, form mendapatkan SPA experience tanpa reload. Ini berarti aplikasi Anda tetap berfungsi untuk user dengan JavaScript yang dimatikan!

6. Server Routes (API)

File +server.js di folder routes memungkinkan Anda membuat API endpoints. Berbeda dengan +page.server.js yang terikat pada halaman, +server.js adalah standalone API routes.

Membuat API Endpoint

src/routes/api/products/+server.js
// src/routes/api/products/+server.js
// GET /api/products?category=shoes&page=1

import { json, error } from '@sveltejs/kit';
import { db } from '$lib/server/db.js';

/** @type {import('./$types').RequestHandler} */
export async function GET({ url }) {
  const category = url.searchParams.get('category');
  const page = parseInt(url.searchParams.get('page') || '1');
  const limit = parseInt(url.searchParams.get('limit') || '20');

  const where = category ? { category } : {};

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

  return json({
    items: products,
    pagination: {
      page,
      limit,
      total,
      totalPages: Math.ceil(total / limit)
    }
  });
}

/** @type {import('./$types').RequestHandler} */
export async function POST({ request }) {
  const body = await request.json();

  // Validasi
  if (!body.name || !body.price) {
    throw error(400, 'Name dan price wajib diisi');
  }

  const product = await db.products.create({
    data: {
      name: body.name,
      price: parseFloat(body.price),
      category: body.category || 'uncategorized',
      description: body.description || ''
    }
  });

  return json({ success: true, product }, { status: 201 });
}

Endpoint dengan Parameter

src/routes/api/products/[id]/+server.js
// src/routes/api/products/[id]/+server.js
// GET /api/products/123
// PUT /api/products/123
// DELETE /api/products/123

import { json, error } from '@sveltejs/kit';

export async function GET({ params }) {
  const product = await db.products.findUnique({
    where: { id: params.id }
  });

  if (!product) {
    throw error(404, 'Produk tidak ditemukan');
  }

  return json(product);
}

export async function PUT({ params, request }) {
  const body = await request.json();

  const product = await db.products.update({
    where: { id: params.id },
    data: body
  });

  return json({ success: true, product });
}

export async function DELETE({ params }) {
  await db.products.delete({
    where: { id: params.id }
  });

  return json({ success: true });
}

Server-Only Code ($lib/server)

src/lib/server/db.js
// src/lib/server/db.js
// File di $lib/server/ TIDAK BISA di-import dari client code
// Ini sangat bagus untuk menyimpan kode sensitif

import { PrismaClient } from '@prisma/client';
import { DATABASE_URL } from '$env/static/private';

const prisma = new PrismaClient({
  datasources: {
    db: { url: DATABASE_URL }
  }
});

export { prisma as db };

// Contoh utility functions
export async function getUserFromSession(cookies) {
  const sessionId = cookies.get('session_id');
  if (!sessionId) return null;

  const session = await prisma.session.findUnique({
    where: { id: sessionId },
    include: { user: true }
  });

  return session?.user || null;
}

7. Komponen Svelte

Svelte menggunakan pendekatan yang sangat elegan: komponen adalah file .svelte yang menggabungkan HTML, CSS, dan JavaScript dalam satu file. Tidak perlu className, tidak perlu useEffect β€” sintaksnya mendekati vanilla web.

Dasar Komponen Svelte

src/lib/components/UserCard.svelte
<script>
  // Props β€” diterima dari parent
  export let name;
  export let email;
  export let role = 'member'; // default value

  // Reactive state β€” langsung reactive tanpa ref() atau useState()!
  let isExpanded = false;

  // Reactive declarations β€” otomatis update saat dependencies berubah
  $: initials = name
    .split(' ')
    .map(n => n[0])
    .join('')
    .toUpperCase();

  $: badgeClass = role === 'admin' ? 'badge-admin' : 'badge-member';

  // Functions
  function toggleExpand() {
    isExpanded = !isExpanded;
  }
</script>

<div class="user-card" on:click={toggleExpand}>
  <div class="avatar">{initials}</div>

  <div class="info">
    <h3>{name}</h3>
    <p>{email}</p>
    <span class="badge {badgeClass}">{role}</span>
  </div>

  {#if isExpanded}
    <div class="details">
      <slot />
    </div>
  {/if}
</div>

<style>
  /* CSS di sini otomatis scoped ke komponen ini! */
  .user-card {
    padding: 1.5rem;
    border-radius: 12px;
    background: var(--card-bg);
    border: 1px solid var(--border-color);
    cursor: pointer;
    transition: transform 0.2s;
  }

  .user-card:hover {
    transform: translateY(-2px);
  }

  .avatar {
    width: 48px;
    height: 48px;
    border-radius: 50%;
    background: var(--primary-color);
    display: flex;
    align-items: center;
    justify-content: center;
    font-weight: 700;
    color: white;
  }

  .badge {
    padding: 4px 12px;
    border-radius: 999px;
    font-size: 0.75rem;
  }

  .badge-admin { background: #ef4444; color: white; }
  .badge-member { background: #3b82f6; color: white; }

  .details {
    margin-top: 1rem;
    padding-top: 1rem;
    border-top: 1px solid var(--border-color);
  }
</style>

Reactivity di Svelte

Reactivity Demo
<script>
  // Simple reactive variable β€” cukup deklarasi, langsung reactive!
  let count = 0;

  // Reactive declaration β€” update otomatis saat count berubah
  $: doubled = count * 2;
  $: quadrupled = doubled * 2;

  // Reactive statement (side effect)
  $: {
    console.log(`Count berubah: ${count}`);
    console.log(`Doubled: ${doubled}`);
  }

  // Reactive if statement
  $: if (count > 10) {
    alert('Count sudah lebih dari 10!');
  }

  // Array reactivity β€” gunakan assignment (=) untuk trigger update
  let items = ['apel', 'pisang'];

  function addItem(fruit) {
    // ❌ items.push(fruit) β€” TIDAK reactive!
    // βœ… items = [...items, fruit] β€” reactive!
    items = [...items, fruit];
  }

  function removeItem(index) {
    // βœ… Filter juga membuat array baru (reactive)
    items = items.filter((_, i) => i !== index);
  }
</script>

<!-- Template -->
<button on:click={() => count++}>
  Klik: {count}
</button>
<p>Doubled: {doubled}</p>
<p>Quadrupled: {quadrupled}</p>

<ul>
  {#each items as item, i}
    <li>
      {item}
      <button on:click={() => removeItem(i)}>βœ•</button>
    </li>
  {/each}
</ul>

Loops dan Conditional Rendering

Svelte β€” Template Syntax
<script>
  let products = [
    { id: 1, name: 'Laptop', price: 15000000, inStock: true },
    { id: 2, name: 'Mouse', price: 150000, inStock: false },
    { id: 3, name: 'Keyboard', price: 500000, inStock: true }
  ];

  let showProducts = true;
</script>

<!-- Conditional rendering -->
{#if showProducts}
  <h2>Daftar Produk</h2>

  <!-- Loop dengan each -->
  {#each products as product (product.id)}
    <div class="product">
      <h3>{product.name}</h3>
      <p>Rp {product.price.toLocaleString('id-ID')}</p>

      <!-- Nested conditional -->
      {#if product.inStock}
        <span class="stock available">Tersedia</span>
      {:else}
        <span class="stock unavailable">Habis</span>
      {/if}
    </div>

  {:else}
    <p>Tidak ada produk ditemukan.</p>
    <!-- {:else} di each = jika array kosong -->
  {/each}

{:else}
  <p>Produk disembunyikan.</p>
{/if}

<!-- Await blocks β€” handle promises langsung di template -->
{#await fetchProducts()}
  <p>Loading produk...</p>
{:then products}
  {#each products as product}
    <p>{product.name}</p>
  {/each}
{:catch error}
  <p>Error: {error.message}</p>
{/await}

8. Svelte Stores

Stores di Svelte adalah reactive data containers yang bisa dibagikan antar komponen. Berbeda dengan state management library di React atau Vue, Svelte stores sangat ringan dan terintegrasi langsung ke bahasa.

Jenis-jenis Store

src/lib/stores.js
// src/lib/stores.js
import { writable, derived, readable } from 'svelte/store';

// 1. WRITABLE STORE β€” bisa dibaca dan ditulis
export const count = writable(0);
export const user = writable(null);
export const cart = writable([]);

// 2. READABLE STORE β€” hanya bisa dibaca (nilai dikontrol oleh store)
export const time = readable(new Date(), function start(set) {
  const interval = setInterval(() => {
    set(new Date());
  }, 1000);

  // Cleanup function β€” dipanggil saat tidak ada subscriber
  return function stop() {
    clearInterval(interval);
  };
});

// 3. DERIVED STORE β€” dihitung dari store lain
export const formattedTime = derived(
  time,
  ($time) => $time.toLocaleTimeString('id-ID')
);

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

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

Menggunakan Store di Komponen

src/lib/components/Cart.svelte
<script>
  import { cart, cartTotal, cartCount } from '$lib/stores.js';

  function addToCart(product) {
    // $cart = shorthand untuk subscribe dan set
    $cart = [...$cart, { ...product, quantity: 1 }];
  }

  function removeFromCart(productId) {
    $cart = $cart.filter(item => item.id !== productId);
  }

  function updateQuantity(productId, newQty) {
    $cart = $cart.map(item =>
      item.id === productId
        ? { ...item, quantity: Math.max(0, newQty) }
        : item
    ).filter(item => item.quantity > 0);
  }

  // $ prefix di template = auto-subscribe/unsubscribe
  // $cart = store value, $cartTotal = derived value
</script>

<div class="cart">
  <h2>πŸ›’ Keranjang ({$cartCount} item)</h2>

  {#if $cart.length === 0}
    <p>Keranjang kosong</p>
  {:else}
    {#each $cart as item (item.id)}
      <div class="cart-item">
        <span>{item.name}</span>
        <div class="qty-controls">
          <button on:click={() => updateQuantity(item.id, item.quantity - 1)}>-</button>
          <span>{item.quantity}</span>
          <button on:click={() => updateQuantity(item.id, item.quantity + 1)}>+</button>
        </div>
        <span>Rp {(item.price * item.quantity).toLocaleString('id-ID')}</span>
        <button on:click={() => removeFromCart(item.id)}>βœ•</button>
      </div>
    {/each}

    <div class="cart-total">
      <strong>Total: Rp {$cartTotal.toLocaleString('id-ID')}</strong>
    </div>
  {/if}
</div>

9. Layouts dan Error Handling

Root Layout

src/routes/+layout.svelte
<script>
  import Header from '$lib/components/Header.svelte';
  import Footer from '$lib/components/Footer.svelte';

  /** @type {import('./$types').LayoutData} */
  export let data;
</script>

<div class="app">
  <Header user={data.user} />

  <main>
    <!-- <slot /> merender child routes -->
    <slot />
  </main>

  <Footer />
</div>

<style>
  .app {
    display: flex;
    flex-direction: column;
    min-height: 100vh;
  }

  main {
    flex: 1;
    padding: 2rem 1rem;
    max-width: 1200px;
    margin: 0 auto;
    width: 100%;
  }
</style>

Nested Layout

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

  const navItems = [
    { href: '/dashboard', label: 'Overview', icon: 'πŸ“Š' },
    { href: '/dashboard/products', label: 'Produk', icon: 'πŸ“¦' },
    { href: '/dashboard/orders', label: 'Pesanan', icon: 'πŸ›’' },
    { href: '/dashboard/settings', label: 'Settings', icon: 'βš™οΈ' }
  ];
</script>

<div class="dashboard-layout">
  <aside class="sidebar">
    <h2>Dashboard</h2>
    <nav>
      {#each navItems as item}
        <a
          href={item.href}
          class:active={$page.url.pathname === item.href}
        >
          <span>{item.icon}</span>
          {item.label}
        </a>
      {/each}
    </nav>
  </aside>

  <div class="dashboard-content">
    <slot />
  </div>
</div>

<style>
  .dashboard-layout {
    display: grid;
    grid-template-columns: 250px 1fr;
    gap: 2rem;
    min-height: 70vh;
  }

  .sidebar {
    background: var(--sidebar-bg);
    padding: 1.5rem;
    border-radius: 12px;
  }

  .sidebar nav {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    margin-top: 1.5rem;
  }

  .sidebar nav a {
    padding: 0.75rem 1rem;
    border-radius: 8px;
    color: var(--text-color);
    text-decoration: none;
    transition: background 0.2s;
  }

  .sidebar nav a:hover { background: var(--hover-bg); }
  .sidebar nav a.active { background: var(--primary-color); color: white; }
</style>

Error Handling

src/routes/+error.svelte
<script>
  import { page } from '$app/stores';
</script>

<div class="error-page">
  <h1>{$page.status}</h1>

  {#if $page.status === 404}
    <h2>Halaman Tidak Ditemukan</h2>
    <p>Maaf, halaman yang Anda cari tidak tersedia.</p>
    <a href="/" class="btn">Kembali ke Beranda</a>
  {:else}
    <h2>Terjadi Kesalahan</h2>
    <p>{$page.error?.message || 'Unknown error'}</p>
    <a href="/" class="btn">Kembali ke Beranda</a>
  {/if}
</div>

<style>
  .error-page {
    text-align: center;
    padding: 4rem 2rem;
  }

  .error-page h1 {
    font-size: 6rem;
    font-weight: 800;
    color: var(--primary-color);
    margin: 0;
  }

  .btn {
    display: inline-block;
    margin-top: 2rem;
    padding: 0.75rem 2rem;
    background: var(--primary-color);
    color: white;
    text-decoration: none;
    border-radius: 8px;
  }
</style>

10. Adapters dan Deployment

Adapters mengubah build output SvelteKit agar cocok dengan platform deployment target. Ini salah satu fitur paling fleksibel dari SvelteKit β€” satu kode bisa di-deploy ke berbagai platform.

Adapter yang Tersedia

Adapter Platform Install
@sveltejs/adapter-autoAuto-detect (default)npm i @sveltejs/adapter-auto
@sveltejs/adapter-nodeNode.js servernpm i @sveltejs/adapter-node
@sveltejs/adapter-staticStatic files (SSG)npm i @sveltejs/adapter-static
@sveltejs/adapter-vercelVercel (serverless)npm i @sveltejs/adapter-vercel
@sveltejs/adapter-netlifyNetlify (serverless)npm i @sveltejs/adapter-netlify
@sveltejs/adapter-cloudflareCloudflare Pages/Workersnpm i @sveltejs/adapter-cloudflare

Static Adapter (SSG)

svelte.config.js β€” Static
// svelte.config.js β€” untuk static site generation
import adapter from '@sveltejs/adapter-static';

export default {
  kit: {
    adapter: adapter({
      // Default output directory
      pages: 'build',
      assets: 'build',
      // Fallback page untuk SPA routing
      fallback: '404.html',
      // Precompress files dengan gzip/brotli
      precompress: false,
      // Strict mode β€” error jika ada prerender yang gagal
      strict: true
    })
  }
};

// Di +layout.js atau +page.js, tentukan prerender:
// export const prerender = true;

Node.js Adapter

svelte.config.js β€” Node
// svelte.config.js β€” untuk Node.js server
import adapter from '@sveltejs/adapter-node';

export default {
  kit: {
    adapter: adapter({
      // Output directory
      out: 'build',
      // Precompress dengan gzip
      precompress: true,
      // Environment prefix untuk env vars
      envPrefix: ''
    })
  }
};

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

// Environment variables:
// PORT=3000
// ORIGIN=https://example.com
Diagram: Adapter System
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              SVELTEKIT ADAPTER SYSTEM                β”‚
β”‚                                                     β”‚
β”‚              SvelteKit Application                  β”‚
β”‚                       β”‚                             β”‚
β”‚              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”                    β”‚
β”‚              β”‚  svelte.config  β”‚                    β”‚
β”‚              β”‚  .js (adapter)  β”‚                    β”‚
β”‚              β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜                    β”‚
β”‚                       β”‚                             β”‚
β”‚    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”          β”‚
β”‚    β”‚          β”‚       β”‚       β”‚          β”‚          β”‚
β”‚  β”Œβ”€β–Όβ”€β”€β”  β”Œβ”€β”€β”€β–Όβ”€β”€β” β”Œβ”€β”€β–Όβ”€β”€β” β”Œβ”€β–Όβ”€β”€β”€β”  β”Œβ”€β”€β–Όβ”€β”€β”€β”€β”     β”‚
β”‚  β”‚Nodeβ”‚  β”‚Vercelβ”‚ β”‚CF   β”‚ β”‚Net. β”‚  β”‚Static β”‚     β”‚
β”‚  β”‚.js β”‚  β”‚      β”‚ β”‚Pagesβ”‚ β”‚lify β”‚  β”‚  SSG  β”‚     β”‚
β”‚  β””β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”˜     β”‚
β”‚                                                     β”‚
β”‚  Build β†’ build/     Build β†’ .vercel/    Build β†’ build/  β”‚
β”‚  Run: node build/   Deploy: vercel      Serve: any CDN  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

11. Quiz Pemahaman

Uji pemahaman Anda tentang SvelteKit dengan quiz interaktif berikut!

1. Apa kepanjangan dari file +page.server.js di SvelteKit?

2. Bagaimana cara membuat reactive variable di Svelte?

3. Apa fungsi dari use:enhance di form SvelteKit?

4. Apa perbedaan utama Svelte dibanding React/Vue?

5. Apa fungsi adapter di SvelteKit?

πŸ“ Ringkasan

Dalam tutorial ini, Anda telah mempelajari:

  • Pengenalan SvelteKit dan keunggulan compile-time approach
  • Instalasi, setup, dan struktur proyek SvelteKit
  • File-based routing dengan konvensi penamaan file
  • Load functions (server load dan universal load)
  • Form actions dengan progressive enhancement
  • Server routes untuk API endpoints
  • Komponen Svelte dengan reactivity dan scoped CSS
  • Svelte stores (writable, readable, derived)
  • Layouts dan error handling
  • Adapters dan deployment ke berbagai platform