Svelte #

Svelte adalah compiler, bukan framework runtime. Kode Svelte dikompilasi menjadi JavaScript vanilla yang sangat kecil dan cepat, tanpa virtual DOM. Svelte 5 membawa runes — system reactivity yang lebih eksplisit dan powerful. SvelteKit, meta-framework resmi, menambahkan routing, SSR/SSG, dan adapter untuk berbagai platform.

Untuk local development, SvelteKit + Vite memberikan HMR yang sangat cepat. Docker memastikan environment konsisten, dan output build (static atau Node) bisa di-serve dengan nginx atau Node server.

Artikel ini membahas setup Svelte 5 dan SvelteKit dengan Docker Compose, termasuk runes, stores, routing, dan strategi deployment.

Prasyarat #

Pastikan sudah terinstall:

  • Docker dan Docker Compose versi terbaru
  • Node.js 20+ (opsional)

Project SvelteKit (bootstrap dengan npx sv create):

my-svelte-app/
├── src/
│   ├── app.html
│   ├── app.d.ts
│   ├── lib/
│   │   ├── components/
│   │   ├── stores/
│   │   └── utils/
│   └── routes/
│       ├── +page.svelte
│       ├── +layout.svelte
│       └── api/
├── static/
├── package.json
├── svelte.config.js
├── vite.config.ts
├── Dockerfile
└── docker-compose.yml

Mengapa Svelte Berbeda #

flowchart LR
    A[Source .svelte] --> B[Svelte Compiler]
    B --> C[Vanilla JS]
    C --> D[Browser]
    
    style A fill:#ff3e00,color:#fff
    style B fill:#ff3e00,color:#fff
    style C fill:#40b3ff,color:#fff
    style D fill:#40b3ff,color:#fff

Svelte mengompilasi komponen menjadi JavaScript imperatif yang langsung memanipulasi DOM. Hasilnya:

Aspek Svelte React/Vue
Runtime ~5KB (runes) 40-130KB
Bundle size Sangat kecil Lebih besar
Reactivity Compiler-level Virtual DOM / Proxy
Learning curve Landai Lebih tinggi

Dockerfile Multi-Stage #

SvelteKit build dengan adapter menghasilkan output berbeda tergantung target:

  • adapter-static — HTML statis untuk nginx
  • adapter-node — Node server untuk SSR/ISR
  • adapter-auto — auto-detect platform (Vercel, Netlify, dll)
# syntax=docker/dockerfile:1.6

# ---- Stage 1: Install dependencies ----
FROM node:20-alpine AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app

COPY package.json package-lock.json* ./
RUN npm ci

# ---- Stage 2: Build SvelteKit ----
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

ENV NODE_ENV=production
RUN npm run build

# ---- Stage 3: Production dengan adapter-node ----
FROM node:20-alpine AS runner
WORKDIR /app

ENV NODE_ENV=production
ENV HOST=0.0.0.0
ENV PORT=3000

RUN addgroup --system --gid 1001 svelte
RUN adduser --system --uid 1001 svelte

# Copy output SvelteKit
COPY --from=builder --chown=svelte:svelte /app/build ./build
COPY --from=builder --chown=svelte:svelte /app/package.json ./package.json

# Install hanya production dependencies
RUN npm ci --omit=dev && npm cache clean --force

USER svelte
EXPOSE 3000

CMD ["node", "build"]

svelte.config.js untuk adapter-node:

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({
      out: 'build',
      precompress: false,
      envPrefix: ''
    }),
  },
};

export default config;

Static Export dengan adapter-static #

Untuk blog atau landing page static:

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

export default {
  kit: {
    adapter: adapter({
      pages: 'build',
      assets: 'build',
      fallback: 'index.html', // SPA mode
      precompress: false,
      strict: true
    }),
  },
};
# Stage terakhir untuk static
FROM nginx:1.27-alpine
COPY --from=builder /app/build /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

nginx.conf untuk SvelteKit static:

server {
    listen 80;
    server_name _;
    root /usr/share/nginx/html;
    index index.html;

    # Cache SvelteKit assets
    location /_app/ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # SPA fallback untuk client-side routing
    location / {
        try_files $uri $uri/ /index.html;
    }
}

docker-compose.yml untuk Development #

# docker-compose.yml
services:
  web:
    build:
      context: .
      dockerfile: Dockerfile.dev
    image: my-svelte-app:dev
    container_name: svelte-dev
    command: npm run dev -- --host 0.0.0.0
    ports:
      - "5173:5173"
    volumes:
      - ./:/app
      - /app/node_modules
      - /app/.svelte-kit
    environment:
      - NODE_ENV=development
      - DATABASE_URL=postgresql://app:dev@db:5432/myapp
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16-alpine
    environment:
      - POSTGRES_USER=app
      - POSTGRES_PASSWORD=dev
      - POSTGRES_DB=myapp
    volumes:
      - db-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d myapp"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  db-data:

Dockerfile.dev:

FROM node:20-alpine
RUN apk add --no-cache libc6-compat
WORKDIR /app

COPY package.json package-lock.json* ./
RUN npm install

COPY . .

EXPOSE 5173

CMD ["npm", "run", "dev"]

Svelte 5 Runes #

Svelte 5 membawa runes — system reactivity yang eksplisit. Berbeda dengan reactivity otomatis Svelte 4 yang bergantung pada let di top-level.

$state untuk Reactive Variable #

<!-- src/lib/components/Counter.svelte -->
<script lang="ts">
  let count = $state(0);
  let name = $state('Pengguna');
  
  function increment() {
    count++;
  }
</script>

<div>
  <p>Halo, {name}!</p>
  <p>Count: {count}</p>
  <input bind:value={name} />
  <button onclick={increment}>Tambah</button>
</div>

$derived untuk Computed Value #

<script lang="ts">
  let count = $state(0);
  let doubleCount = $derived(count * 2);
  let isEven = $derived(count % 2 === 0);
</script>

<p>Count: {count} (double: {doubleCount})</p>
<p>Status: {isEven ? 'Genap' : 'Ganjil'}</p>

$effect untuk Side Effect #

<script lang="ts">
  import { onMount } from 'svelte';
  
  let count = $state(0);
  
  // Effect: jalan otomatis saat count berubah
  $effect(() => {
    console.log(`Count berubah ke: ${count}`);
    document.title = `Count: ${count}`;
  });
  
  // Cleanup function
  $effect(() => {
    const interval = setInterval(() => {
      count++;
    }, 1000);
    
    return () => clearInterval(interval);
  });
</script>

<p>Count: {count}</p>
Kapan pakai $effect vs onMount? Pakai $effect ketika kamu butuh re-run saat state berubah. Pakai onMount ketika logikanya hanya perlu jalan sekali saat komponen mount.

$props untuk Component Props #

<!-- src/lib/components/UserCard.svelte -->
<script lang="ts">
  interface User {
    id: number;
    name: string;
    email: string;
  }
  
  interface Props {
    user: User;
    showEmail?: boolean;
    onDelete?: (id: number) => void;
  }
  
  let { user, showEmail = true, onDelete }: Props = $props();
  
  function handleDelete() {
    onDelete?.(user.id);
  }
</script>

<div class="user-card">
  <h3>{user.name}</h3>
  {#if showEmail}
    <p>{user.email}</p>
  {/if}
  {#if onDelete}
    <button onclick={handleDelete}>Hapus</button>
  {/if}
</div>

<style>
  .user-card {
    padding: 1rem;
    border: 1px solid #ccc;
  }
</style>

Pakai:

<script lang="ts">
  import UserCard from '$lib/components/UserCard.svelte';
  
  let users = $state([
    { id: 1, name: 'Andi', email: '[email protected]' },
    { id: 2, name: 'Budi', email: '[email protected]' },
  ]);
  
  function handleDelete(id: number) {
    users = users.filter(u => u.id !== id);
  }
</script>

{#each users as user (user.id)}
  <UserCard {user} onDelete={handleDelete} />
{/each}

Stores untuk State Global #

SvelteKit menyediakan writable store dari svelte/store untuk state global.

// src/lib/stores/cart.ts
import { writable, derived, get } from 'svelte/store';
import { browser } from '$app/environment';

interface CartItem {
  id: number;
  name: string;
  price: number;
  qty: number;
}

function createCartStore() {
  // Persist ke localStorage di browser
  const initial: CartItem[] = browser
    ? JSON.parse(localStorage.getItem('cart') ?? '[]')
    : [];
  
  const { subscribe, set, update } = writable<CartItem[]>(initial);
  
  // Auto-save ke localStorage
  if (browser) {
    subscribe((items) => {
      localStorage.setItem('cart', JSON.stringify(items));
    });
  }
  
  return {
    subscribe,
    add: (item: Omit<CartItem, 'qty'>) => {
      update((items) => {
        const existing = items.find(i => i.id === item.id);
        if (existing) {
          return items.map(i =>
            i.id === item.id ? { ...i, qty: i.qty + 1 } : i
          );
        }
        return [...items, { ...item, qty: 1 }];
      });
    },
    remove: (id: number) => {
      update((items) => items.filter(i => i.id !== id));
    },
    clear: () => set([]),
  };
}

export const cart = createCartStore();

// Derived store
export const cartTotal = derived(cart, ($cart) =>
  $cart.reduce((sum, i) => sum + i.price * i.qty, 0)
);

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

Pakai di komponen dengan auto-subscription $:

<script lang="ts">
  import { cart, cartTotal, cartCount } from '$lib/stores/cart';
</script>

<div>
  <p>Items: {$cartCount} — Total: Rp {$cartTotal.toLocaleString('id-ID')}</p>
  <button onclick={() => cart.add({ id: 1, name: 'Produk', price: 50000 })}>
    Tambah
  </button>
  <button onclick={() => cart.clear()}>Kosongkan</button>
</div>
Prefix $ pada store mengaktifkan auto-subscription. Tanpa $, kamu harus subscribe manual. Ini syntactic sugar yang powerful dari Svelte.

Routing dengan SvelteKit #

SvelteKit pakai file-based routing di src/routes/:

src/routes/
├── +page.svelte              # /
├── +layout.svelte            # Root layout
├── about/
│   └── +page.svelte          # /about
├── products/
│   ├── +page.svelte          # /products
│   ├── +page.ts              # Load function
│   └── [id]/
│       ├── +page.svelte      # /products/:id
│       └── +page.ts          # Dynamic load
├── api/
│   └── users/
│       └── +server.ts        # /api/users endpoint
└── +error.svelte             # Error page

Load function (server + client):

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

export const load: PageLoad = async ({ fetch }) => {
  const res = await fetch('/api/products');
  const products = await res.json();
  return { products };
};
<!-- src/routes/products/+page.svelte -->
<script lang="ts">
  import type { PageData } from './$types';
  
  let { data }: { data: PageData } = $props();
</script>

<h1>Produk</h1>
<ul>
  {#each data.products as product (product.id)}
    <li>
      <a href="/products/{product.id}">{product.name}</a>
      <span>Rp {product.price.toLocaleString('id-ID')}</span>
    </li>
  {/each}
</ul>

Server-only load:

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

export const load: PageServerLoad = async ({ params }) => {
  const product = await db.product.findUnique({
    where: { id: Number(params.id) },
  });
  
  if (!product) {
    throw error(404, 'Produk tidak ditemukan');
  }
  
  return { product };
};

Form actions (server-side mutation):

// src/routes/products/new/+page.server.ts
import { fail, redirect } from '@sveltejs/kit';
import type { Actions } from './$types';
import { db } from '$lib/server/db';

export const actions: Actions = {
  default: async ({ request }) => {
    const formData = await request.formData();
    const name = formData.get('name') as string;
    const price = Number(formData.get('price'));
    
    if (!name || isNaN(price)) {
      return fail(400, { error: 'Nama dan harga wajib diisi' });
    }
    
    await db.product.create({ data: { name, price } });
    throw redirect(303, '/products');
  },
};
<!-- src/routes/products/new/+page.svelte -->
<script lang="ts">
  import type { ActionData } from './$types';
  
  let { form }: { form: ActionData } = $props();
</script>

<h1>Produk Baru</h1>

{#if form?.error}
  <p class="error">{form.error}</p>
{/if}

<form method="POST">
  <input name="name" placeholder="Nama produk" required />
  <input name="price" type="number" placeholder="Harga" required />
  <button type="submit">Simpan</button>
</form>

API endpoint:

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

export const GET: RequestHandler = async () => {
  const users = await db.user.findMany();
  return json(users);
};

export const POST: RequestHandler = async ({ request }) => {
  const body = await request.json();
  const user = await db.user.create({ data: body });
  return json(user, { status: 201 });
};

Hot Reload dengan Vite #

SvelteKit + Vite HMR bekerja di semua level:

  • Perubahan .svelte — HMR instan, state dipertahankan
  • Perubahan +page.ts / +page.server.ts — Reload halaman
  • Perubahan +layout.svelte — Reload semua child
  • Perubahan +server.ts — API endpoint di-restart

Untuk Docker, pastikan bind mount source code dan anonymous volume untuk .svelte-kit:

volumes:
  - ./:/app
  - /app/node_modules
  - /app/.svelte-kit

Build dan Run #

# Development
docker compose up --build
# Akses: http://localhost:5173

# Production build (adapter-node)
docker build -t my-svelte-app:prod -f Dockerfile .
docker run -p 3000:3000 my-svelte-app:prod

# Production build (adapter-static)
docker build -t my-svelte-app:static -f Dockerfile.static .
docker run -p 8080:80 my-svelte-app:static

# Stop
docker compose down

Best Practice #

1. Pilih Adapter yang Tepat #

Adapter Pakai untuk
adapter-static Blog, landing page, dokumentasi
adapter-node SSR, ISR, dynamic content
adapter-auto Deploy ke Vercel/Netlify/Cloudflare

2. Pakai Runes untuk Kode Baru #

Svelte 5 runes lebih eksplisit dan powerful. Untuk project baru, langsung pakai $state, $derived, $effect, $props. Untuk project lama, migrasi bertahap.

3. Pisahkan Server vs Universal Code #

Kode dengan import ke Node-only modules (seperti pg, fs) harus di +page.server.ts atau $lib/server/. Kode di +page.ts jalan di server DAN client.

// src/lib/server/db.ts — server only
import { Pool } from 'pg';
export const pool = new Pool({ ... });
<script lang="ts">
  // ❌ JANGAN import server module di +page.svelte
  // import { pool } from '$lib/server/db';
  
  // ✅ Pakai load function
  let { data } = $props();
</script>

4. Vite host: '0.0.0.0' #

Wajib di vite.config.ts atau flag --host 0.0.0.0. Tanpa ini, dev server tidak bisa diakses dari host.

5. Anonymous Volume untuk .svelte-kit #

volumes:
  - /app/.svelte-kit

Tanpa ini, type generation dan cache build konflik dengan host.

6. Form Actions untuk Mutation #

Untuk form submission, pakai form actions di +page.server.ts daripada fetch API manual. Lebih simpel, otomatis handle CSRF, dan progressive enhancement ready.

7. Stores untuk State Lintas Halaman #

Cart, user auth, theme preferences — pakai store di $lib/stores/. Auto-subscribe dengan prefix $ di template.

8. $effect untuk Cleanup #

Selalu return cleanup function dari $effect untuk prevent memory leak:

$effect(() => {
  const handler = () => console.log('resize');
  window.addEventListener('resize', handler);
  return () => window.removeEventListener('resize', handler);
});

Troubleshooting #

HMR Tidak Bekerja #

Pastikan host: '0.0.0.0', bind mount source code, dan anonymous volume .svelte-kit.

“Cannot find module ‘$app/environment’” #

Install @sveltejs/kit dan pastikan svelte.config.js valid. Restart container.

Build Output Kecil tapi Production Error #

Cek +page.server.ts — mungkin ada kode server yang tidak sengaja di-bundle. Pastikan server-only module di-import dari $lib/server/.

Adapter-node Port Conflict #

lsof -i :3000

Ubah PORT environment variable atau port mapping Compose.

Svelte 5 Component Tidak Reactive #

Pakai runes ($state, $derived) bukan let biasa. Di Svelte 5, let tidak otomatis reactive.


Ringkasan #

  • Svelte ideal untuk SPA (adapter-static) atau SSR (adapter-node). Bundle kecil, performa tinggi.
  • Dockerfile multi-stage berbeda per adapter: adapter-node pakai node build (Node server), adapter-static pakai nginx.
  • Svelte 5 runes ($state, $derived, $effect, $props) adalah reactivity modern — lebih eksplisit dari let di Svelte 4.
  • Stores dengan writable + derived di $lib/stores/. Auto-subscribe dengan prefix $ di template.
  • SvelteKit routing dengan file-based routing di src/routes/. +page.svelte untuk UI, +page.ts/+page.server.ts untuk data loading.
  • Form actions di +page.server.ts untuk mutation. Auto-handle CSRF, progressive enhancement, no JS required.
  • API endpoints dengan +server.ts dan HTTP method exports (GET, POST, dll).
  • Adapter-node untuk SSR, adapter-static untuk SPA, adapter-auto untuk platform khusus.
  • Anonymous volume untuk node_modules dan .svelte-kit mencegah konflik dengan host.
  • Server-only code di $lib/server/ atau +page.server.ts — tidak boleh di-bundle ke client.
  • Hot reload Vite bekerja untuk .svelte, +page.ts, +layout.svelte. State dipertahankan untuk komponen.
  • Pakai $effect dengan cleanup untuk side effect yang perlu unsubscribe (event listener, interval).
  • Dockerfile dev sederhana (Vite + npm), Dockerfile prod multi-stage (Node build + nginx/node runner).
  • Troubleshooting: HMR mati (cek host + volume), module not found (restart container), reactivity error (pakai runes).
  • Bundle size Svelte lebih kecil dari React/Vue karena compiler-level optimization.
  • TypeScript dengan lang="ts" di script tag. Type inference untuk props, state, dan load functions.

← Sebelumnya: Vue 3   Berikutnya: Angular →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact