🌊 Development Flow
Pahami pola development dengan SvelteKit agar coding lebih nyaman dan AI-friendly.
🎯 Konsep Dasar
SvelteKit menggunakan file-based routing dan server-first architecture. Ini artinya:
- File = Route - Setiap file di
src/routes/menjadi halaman - Server Default - Data di-load di server, bukan client
- Progressive Enhancement - Form work tanpa JavaScript
🆕 Svelte 5 Runes: Project ini menggunakan Svelte 5 dengan Runes untuk reactivity.
Perbedaan utama Svelte 4 → 5:
export let data→let { data } = $props()let count = 0→let count = $state(0)on:click→onclick$:reactive statements →$derived()dan$effect()
📁 Struktur File
src/routes/
├── +layout.svelte # Layout utama (navbar, dll)
├── +page.svelte # Home page (/)
├── +page.server.ts # Server code untuk home
│
├── login/
│ ├── +page.svelte # Login page (/login)
│ └── +page.server.ts # Login logic
│
├── dashboard/
│ ├── +page.svelte # Dashboard page (/dashboard)
│ └── +page.server.ts # Load data user
│
└── api/
└── users/
└── +server.ts # API endpoint (/api/users)Aturan penamaan:
+page.svelte= UI halaman+page.server.ts= Server code (load data, form actions)+layout.svelte= Wrapper layout+server.ts= API endpoint
🆕 Svelte 5: Project ini menggunakan Svelte 5 dengan Runes (
$state,$derived,$effect). Lihat Svelte 5 Docs untuk detail.
🛤️ Dynamic Routes
Untuk URL dinamis seperti /posts/first-post, gunakan bracket [slug] di nama folder.
Struktur Folder
src/routes/
├── posts/
│ ├── +page.svelte # /posts (list)
│ └── [slug]/ # /posts/:slug
│ ├── +page.svelte # UI detail post
│ └── +page.server.ts # Load data postContoh Lengkap: Blog Post
1. Schema Database
// src/lib/db/schema.ts
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
export const posts = sqliteTable("posts", {
id: integer("id").primaryKey({ autoIncrement: true }),
slug: text("slug").notNull().unique(), // URL-friendly
title: text("title").notNull(),
content: text("content").notNull(),
published: integer("published", { mode: "boolean" }).default(false),
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
});2. Server Load
// routes/posts/[slug]/+page.server.ts
import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params, locals }) => {
const { slug } = params; // "first-post"
const post = await locals.db
.selectFrom('posts')
.where('slug', '=', slug)
.selectAll()
.executeTakeFirst();
if (!post) {
throw error(404, 'Post not found');
}
return { post };
};3. Page Component
<!-- routes/posts/[slug]/+page.svelte -->
<script>
let { data } = $props();
</script>
<svelte:head>
<title>{data.post.title}</title>
</svelte:head>
<article class="max-w-2xl mx-auto p-6">
<h1 class="text-3xl font-bold mb-4">{data.post.title}</h1>
<time class="text-sm text-neutral-500">
{new Date(data.post.createdAt).toLocaleDateString('id-ID')}
</time>
<div class="prose mt-6">
{data.post.content}
</div>
</article>Generate Slug (Saat Create Post)
// utils/slug.ts
export function generateSlug(title: string): string {
return title
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-') // Ganti non-alphanumeric dengan -
.replace(/(^-|-$)/g, ''); // Hapus - di awal/akhir
}
// Penggunaan
generateSlug("Hello World!"); // "hello-world"
generateSlug("Cara Deploy SvelteKit"); // "cara-deploy-sveltekit"Pattern URL Lainnya
| Pattern | Contoh URL | Params | Keterangan |
|---|---|---|---|
[slug] | /posts/hello-world | { slug: 'hello-world' } | Parameter wajib |
[[lang]] | /en/posts atau /posts | { lang: 'en' } atau {} | Parameter opsional |
[...path] | /docs/a/b/c | { path: 'a/b/c' } | Catch-all (sisa path) |
[id=int] | /products/123 | { id: 123 } | Auto-convert ke number |
💡 Tips SEO: Gunakan slug deskriptif (
cara-deploy-sveltekit) bukan ID (post-123). Pastikan sluguniquedi database!
🔄 Data Flow Patterns
Pattern 1: Server Load (Paling Umum)
Gunakan untuk: Menampilkan data dari database
// routes/dashboard/+page.server.ts
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ locals }) => {
// Query database langsung di server dengan Kysely
const user = await locals.db
.selectFrom('users')
.where('id', '=', locals.user.id)
.selectAll()
.executeTakeFirst();
const posts = await locals.db
.selectFrom('posts')
.where('author_id', '=', locals.user.id)
.selectAll()
.execute();
// Return data - otomatis tersedia di page
return { user, posts };
};<!-- routes/dashboard/+page.svelte -->
<script>
// Svelte 5: Data otomatis masuk dari +page.server.ts
let { data } = $props();
</script>
<h1>Welcome, {data.user.name}!</h1>
{#each data.posts as post}
<article>
<h2>{post.title}</h2>
<p>{post.content}</p>
</article>
{/each}💡 Svelte 5: Gunakan
$props()untuk menerima data dari server. Ini menggantikanexport let datadi Svelte 4.
Flow:
User Request → Server Query DB → Render HTML → Browser
↓
(data sudah ada,
no loading state!)✅ Keuntungan:
- 1 request only
- SEO friendly
- No loading spinner
- Type-safe
Pattern 2: Form Actions (Untuk Mutations)
Gunakan untuk: Create, Update, Delete data
// routes/posts/new/+page.server.ts
import type { Actions } from './$types';
import { fail, redirect } from '@sveltejs/kit';
export const actions: Actions = {
create: async ({ request, locals }) => {
// 1. Get form data
const form = await request.formData();
const title = form.get('title');
const content = form.get('content');
// 2. Validate
if (!title || typeof title !== 'string') {
return fail(400, {
error: 'Title is required',
values: { title, content }
});
}
// 3. Insert to database dengan Kysely
const id = crypto.randomUUID();
await locals.db
.insertInto('posts')
.values({
id,
title: title as string,
content: content as string,
author_id: locals.user.id,
created_at: Date.now()
})
.execute();
// 4. Redirect
throw redirect(303, `/posts/${id}`);
}
};<!-- routes/posts/new/+page.svelte -->
<script>
// Svelte 5: Form data dari action
let { form } = $props();
import { enhance } from '$app/forms';
</script>
<form method="POST" action="?/create" use:enhance>
{#if form?.error}
<div class="error">{form.error}</div>
{/if}
<label>
Title
<input
name="title"
value={form?.values?.title ?? ''}
required
/>
</label>
<label>
Content
<textarea name="content">{form?.values?.content ?? ''}</textarea>
</label>
<button type="submit">Create Post</button>
</form>💡 Svelte 5: Form actions tetap sama, hanya cara menerima props yang berubah dengan
$props().
Flow:
Form Submit → Server Action → Validate → DB Insert → Redirect
↓
(Works tanpa JS!)✅ Keuntungan:
- Works tanpa JavaScript
- No API endpoint needed
- Progressive enhancement
Pattern 3: API Endpoints (Sekadarnya)
Gunakan untuk: External API, real-time updates, atau client-side fetch
// routes/api/posts/+server.ts
import type { RequestHandler } from './$types';
import { json, error } from '@sveltejs/kit';
export const GET: RequestHandler = async ({ locals, url }) => {
const limit = parseInt(url.searchParams.get('limit') ?? '10');
const posts = await locals.db
.selectFrom('posts')
.orderBy('created_at', 'desc')
.limit(limit)
.selectAll()
.execute();
return json({ posts });
};
export const POST: RequestHandler = async ({ request, locals }) => {
if (!locals.user) {
throw error(401, 'Unauthorized');
}
const body = await request.json();
// ... validate and insert
return json({ success: true, id }, { status: 201 });
};<!-- Fetch dari client -->
<script>
import { onMount } from 'svelte';
// Svelte 5: Gunakan $state untuk reactive data
let posts = $state([]);
onMount(async () => {
const res = await fetch('/api/posts?limit=5');
const data = await res.json();
posts = data.posts;
});
</script>💡 Svelte 5: Gunakan
$state()untuk variabel reactive. Ini menggantikanletbiasa yang perlu assignment untuk trigger reactivity.
⚠️ Gunakan pattern ini jika:
- External service perlu akses data
- Real-time updates (polling)
- Client-side only data
🗄️ Database Access
Setup Database
Database di-inject ke locals via hooks:
// src/hooks.server.ts
import { Kysely } from 'kysely';
import { D1Dialect } from 'kysely-d1';
import type { Database } from '$lib/db/kysely-types';
export const handle = async ({ event, resolve }) => {
// Inject Kysely DB ke locals
if (event.platform?.env?.DB) {
event.locals.db = new Kysely<Database>({
dialect: new D1Dialect({
database: event.platform.env.DB,
}),
});
}
// ... auth handling
return resolve(event);
};Query Patterns (Kysely)
// SELECT all
const users = await locals.db
.selectFrom('users')
.selectAll()
.execute();
// SELECT with WHERE
const user = await locals.db
.selectFrom('users')
.where('id', '=', userId)
.selectAll()
.executeTakeFirst();
// SELECT with JOIN
const postsWithAuthor = await locals.db
.selectFrom('posts')
.innerJoin('users', 'posts.author_id', 'users.id')
.select(['posts.title', 'users.name as author_name'])
.execute();
// INSERT
await locals.db
.insertInto('posts')
.values({
id: crypto.randomUUID(),
title: 'Hello',
content: 'World',
created_at: Date.now()
})
.execute();
// UPDATE
await locals.db
.updateTable('posts')
.set({ title: 'Updated', updated_at: Date.now() })
.where('id', '=', postId)
.execute();
// DELETE
await locals.db
.deleteFrom('posts')
.where('id', '=', postId)
.execute();🔐 Authentication Flow
Check Auth di Server
// routes/protected/+page.server.ts
import { redirect } from '@sveltejs/kit';
export const load = async ({ locals }) => {
// Check auth
if (!locals.user) {
throw redirect(303, '/login');
}
// User tersedia di locals
return { user: locals.user };
};Check Auth di Page
<script>
import { page } from '$app/state';
</script>
{#if page.data.user}
<p>Welcome, {page.data.user.name}!</p>
{:else}
<a href="/login">Login</a>
{/if}💡 Svelte 5:
$app/storesdiganti dengan$app/state. Tidak perlu$prefix lagi untuk access state.
📋 Checklist Membuat Fitur Baru
Gunakan checklist ini untuk setiap fitur baru:
## Fitur: [Nama Fitur]
### Database
- [ ] Tambah schema di drizzle/schema.ts
- [ ] Buat migration file
- [ ] Apply migration: npm run db:migrate:local
### Backend
- [ ] Buat +page.server.ts dengan load()
- [ ] Tambah form actions (jika perlu)
- [ ] Validasi input dengan Zod
### Frontend
- [ ] Buat +page.svelte dengan form
- [ ] Gunakan use:enhance untuk UX
- [ ] Handle error states
- [ ] Styling dengan Tailwind
### Testing
- [ ] Test happy path
- [ ] Test error cases
- [ ] Test tanpa JavaScript (optional)🎯 Decision Tree: Pattern Mana yang Dipakai?
Mau buat apa?
│
├─► Menampilkan data dari DB
│ └─► Gunakan: Server Load (+page.server.ts load)
│
├─► Form (Create/Update/Delete)
│ └─► Gunakan: Form Actions (+page.server.ts actions)
│
├─► API untuk external service
│ └─► Gunakan: API Endpoint (+server.ts)
│
└─► Real-time/Client-only data
└─► Gunakan: Client Fetch + API Endpoint🎨 Styling dengan Tailwind CSS 4
Project ini menggunakan Tailwind CSS 4 dengan konfigurasi CSS-first:
Config di src/app.css (Bukan tailwind.config.js)
@import "tailwindcss";
@theme {
/* Define colors */
--color-neutral-50: #fafafa;
--color-neutral-950: #0a0a0a;
--color-accent-500: #f59e0b;
/* Define fonts */
--font-sans: 'Inter', system-ui, sans-serif;
/* Define animations */
--animate-fade-in: fadeIn 0.6s ease-out;
}
@layer components {
.btn-primary {
@apply px-4 py-2 bg-accent-500 text-neutral-950 rounded-lg;
}
}Class Utility Tersedia
Starter kit sudah menyediakan class utility:
<!-- Card -->
<div class="card">
<h2 class="font-display text-xl">Title</h2>
</div>
<!-- Buttons -->
<button class="btn-primary">Primary</button>
<button class="btn-secondary">Secondary</button>
<!-- Form Input -->
<input class="input" placeholder="Type here..." />Lihat src/app.css untuk semua utility class yang tersedia.
💡 Tips Development
1. Selalu Mulai dari Server
❌ Jangan:
<script>
let data = [];
onMount(async () => {
const res = await fetch('/api/data');
data = await res.json();
});
</script>✅ Lakukan:
// +page.server.ts
export const load = async () => {
const data = await db.query...;
return { data };
};2. Gunakan Progressive Enhancement
Form harus work tanpa JavaScript:
<!-- ✅ Tanpa JS, form tetap work -->
<form method="POST" action="?/create">
<input name="title" />
<button type="submit">Submit</button>
</form>
<!-- ✅ Dengan JS, UX lebih baik -->
<form method="POST" action="?/create" use:enhance>
<!-- Loading state, optimistic UI, dll -->
</form>3. Type Safety
Selalu gunakan TypeScript types:
import type { PageServerLoad, Actions } from './$types';
export const load: PageServerLoad = async () => { ... };
export const actions: Actions = { ... };4. Svelte 5 Runes
Gunakan runes untuk reactivity:
<script>
// ✅ Props dari server
let { data } = $props();
// ✅ Local state
let count = $state(0);
// ✅ Derived state
let doubled = $derived(count * 2);
// ✅ Effects
$effect(() => {
console.log('Count changed:', count);
});
</script>
<button onclick={() => count++}>
Count: {count} (doubled: {doubled})
</button>🚀 Next Steps
| Jika ingin... | Lanjut ke... |
|---|---|
| Pahami arsitektur lengkap | Architecture |
| Lihat contoh fitur lengkap | Features |
| Siap deploy | Deployment |
| Gunakan AI untuk coding | AI-First Development |
Paham konsepnya? 🎉 Sekarang waktunya membuat fitur!