Putting It All Together

We’ve covered the philosophy. We’ve covered the patterns. Now let’s build something.

This is a task manager. Simple enough to understand, complex enough to demonstrate real patterns. Authentication, data loading, forms, derived state, error handling—everything we’ve discussed.

Pay attention to what’s not here. No state management library. No complex abstractions. No global stores for route data. Just SvelteKit patterns applied consistently.

The Architecture

src/
├── routes/
│   ├── +layout.svelte          # App shell
│   ├── +layout.server.ts       # Auth check, user data
│   ├── +error.svelte           # Error page
│   ├── +page.svelte            # Landing/redirect
│   │
│   ├── (auth)/
│   │   ├── login/
│   │   │   ├── +page.svelte
│   │   │   └── +page.server.ts
│   │   └── register/
│   │       ├── +page.svelte
│   │       └── +page.server.ts
│   │
│   └── (app)/
│       ├── +layout.svelte      # App layout (nav, sidebar)
│       ├── +layout.server.ts   # Require auth
│       │
│       ├── dashboard/
│       │   ├── +page.svelte
│       │   └── +page.server.ts
│       │
│       ├── tasks/
│       │   ├── +page.svelte
│       │   └── +page.server.ts
│       │
│       └── settings/
│           ├── +page.svelte
│           └── +page.server.ts

├── lib/
│   ├── components/
│   │   ├── TaskCard.svelte
│   │   ├── TaskForm.svelte
│   │   └── StatsCard.svelte
│   │
│   └── server/
│       └── db.ts

└── app.d.ts

Notice the route groups: (auth) for public pages, (app) for authenticated pages. The parentheses mean they don’t affect the URL—/login not /(auth)/login.

Authentication

Authentication lives in the root layout. Every page has access to the user.

// routes/+layout.server.ts
import { db } from '$lib/server/db'

export async function load({ cookies }) {
	const sessionId = cookies.get('session')

	if (!sessionId) {
		return { user: null }
	}

	const user = await db.getUserBySession(sessionId)

	return { user }
}

The app layout requires authentication:

// routes/(app)/+layout.server.ts
import { redirect } from '@sveltejs/kit'

export async function load({ parent }) {
	const { user } = await parent()

	if (!user) {
		redirect(303, '/login')
	}

	return { user }
}

Any page under (app) is protected. The redirect happens automatically.

The Dashboard

The dashboard shows stats and recent tasks. All data comes from the load function.

// routes/(app)/dashboard/+page.server.ts
import { db } from '$lib/server/db'

export async function load({ parent }) {
	const { user } = await parent()

	const [stats, recentTasks] = await Promise.all([
		db.getTaskStats(user.id),
		db.getRecentTasks(user.id, 5)
	])

	return { stats, recentTasks }
}
<!-- routes/(app)/dashboard/+page.svelte -->
<script>
	import StatsCard from '$lib/components/StatsCard.svelte'
	import TaskCard from '$lib/components/TaskCard.svelte'

	let { data } = $props()

	// Derived: completion percentage
	let completionRate = $derived(
		data.stats.total > 0 ? Math.round((data.stats.completed / data.stats.total) * 100) : 0
	)
</script>

<h1>Dashboard</h1>

<div class="stats-grid">
	<StatsCard label="Total Tasks" value={data.stats.total} />
	<StatsCard label="Completed" value={data.stats.completed} />
	<StatsCard label="Pending" value={data.stats.pending} />
	<StatsCard label="Completion" value="{completionRate}%" />
</div>

<h2>Recent Tasks</h2>

{#if data.recentTasks.length === 0}
	<p class="empty">No tasks yet. <a href="/tasks">Create one</a>.</p>
{:else}
	<div class="task-list">
		{#each data.recentTasks as task (task.id)}
			<TaskCard {task} />
		{/each}
	</div>
{/if}

Notice:

  • Data from load function, not fetched in effect
  • Derived values for computed stats
  • Simple conditional for empty state
  • Components receive data as props, not from stores

The Tasks Page

This is where CRUD happens. Load, create, update, delete—all through load functions and form actions.

// routes/(app)/tasks/+page.server.ts
import { fail } from '@sveltejs/kit'
import { db } from '$lib/server/db'

export async function load({ parent, url }) {
	const { user } = await parent()

	const filter = url.searchParams.get('filter') || 'all'
	const tasks = await db.getTasks(user.id)

	return { tasks, filter }
}

export const actions = {
	create: async ({ request, locals }) => {
		const formData = await request.formData()
		const title = formData.get('title')
		const description = formData.get('description')

		if (!title || typeof title !== 'string' || title.trim().length === 0) {
			return fail(400, {
				error: 'Title is required',
				title,
				description
			})
		}

		await db.createTask({
			userId: locals.user.id,
			title: title.trim(),
			description: description?.toString() || ''
		})

		return { success: true }
	},

	toggle: async ({ request, locals }) => {
		const formData = await request.formData()
		const taskId = formData.get('taskId')

		const task = await db.getTask(taskId)

		if (!task || task.userId !== locals.user.id) {
			return fail(404, { error: 'Task not found' })
		}

		await db.updateTask(taskId, { completed: !task.completed })

		return { success: true }
	},

	delete: async ({ request, locals }) => {
		const formData = await request.formData()
		const taskId = formData.get('taskId')

		const task = await db.getTask(taskId)

		if (!task || task.userId !== locals.user.id) {
			return fail(404, { error: 'Task not found' })
		}

		await db.deleteTask(taskId)

		return { success: true }
	}
}
<!-- routes/(app)/tasks/+page.svelte -->
<script>
	import { enhance } from '$app/forms'
	import TaskForm from '$lib/components/TaskForm.svelte'

	let { data, form } = $props()

	// Local UI state
	let filter = $state(data.filter)
	let showForm = $state(false)

	// Derived: filtered tasks
	let filteredTasks = $derived.by(() => {
		if (filter === 'all') return data.tasks
		if (filter === 'pending') return data.tasks.filter((t) => !t.completed)
		if (filter === 'completed') return data.tasks.filter((t) => t.completed)
		return data.tasks
	})

	// Derived: counts
	let counts = $derived({
		all: data.tasks.length,
		pending: data.tasks.filter((t) => !t.completed).length,
		completed: data.tasks.filter((t) => t.completed).length
	})
</script>

<div class="tasks-header">
	<h1>Tasks</h1>
	<button onclick={() => (showForm = !showForm)}>
		{showForm ? 'Cancel' : 'New Task'}
	</button>
</div>

{#if showForm}
	<TaskForm error={form?.error} onsubmit={() => (showForm = false)} />
{/if}

<div class="filters">
	<button class:active={filter === 'all'} onclick={() => (filter = 'all')}>
		All ({counts.all})
	</button>
	<button class:active={filter === 'pending'} onclick={() => (filter = 'pending')}>
		Pending ({counts.pending})
	</button>
	<button class:active={filter === 'completed'} onclick={() => (filter = 'completed')}>
		Completed ({counts.completed})
	</button>
</div>

{#if filteredTasks.length === 0}
	<p class="empty">No {filter === 'all' ? '' : filter} tasks.</p>
{:else}
	<ul class="task-list">
		{#each filteredTasks as task (task.id)}
			<li class:completed={task.completed}>
				<form method="POST" action="?/toggle" use:enhance>
					<input type="hidden" name="taskId" value={task.id} />
					<button type="submit" class="toggle">
						{task.completed ? '' : ''}
					</button>
				</form>

				<div class="task-content">
					<span class="title">{task.title}</span>
					{#if task.description}
						<span class="description">{task.description}</span>
					{/if}
				</div>

				<form method="POST" action="?/delete" use:enhance>
					<input type="hidden" name="taskId" value={task.id} />
					<button type="submit" class="delete">×</button>
				</form>
			</li>
		{/each}
	</ul>
{/if}

Notice:

  • Local UI state for filter and form visibility ($state)
  • Derived filtering happens client-side—no server round-trip for filters
  • Form actions for mutations—progressive enhancement works
  • Each task has its own tiny forms for toggle and delete
  • No global stores—everything scoped to this route

The Task Form Component

A simple form component that works with the parent’s action:

<!-- lib/components/TaskForm.svelte -->
<script>
	import { enhance } from '$app/forms'

	let { error, onsubmit } = $props()

	let title = $state('')
	let description = $state('')
	let submitting = $state(false)
</script>

<form
	method="POST"
	action="?/create"
	use:enhance={() => {
		submitting = true

		return async ({ result, update }) => {
			submitting = false

			if (result.type === 'success') {
				title = ''
				description = ''
				onsubmit?.()
			}

			await update()
		}
	}}
>
	{#if error}
		<p class="error">{error}</p>
	{/if}

	<label>
		Title
		<input name="title" bind:value={title} required disabled={submitting} />
	</label>

	<label>
		Description (optional)
		<textarea name="description" bind:value={description} disabled={submitting}></textarea>
	</label>

	<button type="submit" disabled={submitting}>
		{submitting ? 'Creating...' : 'Create Task'}
	</button>
</form>

The component:

  • Manages its own form state
  • Uses use:enhance for no-reload submission
  • Clears itself on success
  • Calls parent callback when done

Settings with Form Actions

Settings demonstrates validation and updates:

// routes/(app)/settings/+page.server.ts
import { fail } from '@sveltejs/kit'
import { db } from '$lib/server/db'

export async function load({ parent }) {
	const { user } = await parent()
	return { user }
}

export const actions = {
	updateProfile: async ({ request, locals }) => {
		const formData = await request.formData()
		const name = formData.get('name')
		const email = formData.get('email')

		const errors: Record<string, string> = {}

		if (!name || typeof name !== 'string' || name.length < 2) {
			errors.name = 'Name must be at least 2 characters'
		}

		if (!email || typeof email !== 'string' || !email.includes('@')) {
			errors.email = 'Valid email required'
		}

		if (Object.keys(errors).length > 0) {
			return fail(400, { errors, name, email })
		}

		// Check email uniqueness
		const existing = await db.getUserByEmail(email)
		if (existing && existing.id !== locals.user.id) {
			return fail(400, {
				errors: { email: 'Email already in use' },
				name,
				email
			})
		}

		await db.updateUser(locals.user.id, {
			name: name.toString(),
			email: email.toString()
		})

		return { success: true }
	}
}
<!-- routes/(app)/settings/+page.svelte -->
<script>
	import { enhance } from '$app/forms'

	let { data, form } = $props()
</script>

<h1>Settings</h1>

{#if form?.success}
	<div class="success">Settings saved!</div>
{/if}

<form method="POST" action="?/updateProfile" use:enhance>
	<label>
		Name
		<input
			name="name"
			value={form?.name ?? data.user.name}
			aria-invalid={form?.errors?.name ? 'true' : undefined}
		/>
		{#if form?.errors?.name}
			<span class="error">{form.errors.name}</span>
		{/if}
	</label>

	<label>
		Email
		<input
			name="email"
			type="email"
			value={form?.email ?? data.user.email}
			aria-invalid={form?.errors?.email ? 'true' : undefined}
		/>
		{#if form?.errors?.email}
			<span class="error">{form.errors.email}</span>
		{/if}
	</label>

	<button type="submit">Save Changes</button>
</form>

Standard pattern: server-side validation, return fail() with errors, repopulate form with submitted values.

What’s Not Here

Notice what we didn’t need:

  • No state management library. $state and $derived handle everything.
  • No stores folder. Route data stays in routes.
  • No fetch calls in components. Load functions handle data.
  • No useEffect-style data loading. No $effect for fetching.
  • No prop drilling more than 2 levels. Components get what they need.
  • No complex abstractions. Components are straightforward.

The total is around 500 lines of application code. You could understand any part by reading just that part. Dependencies are explicit. State is scoped. Patterns are consistent.

The Calm Checklist

Before calling a system “calm,” check:

  • Can you understand a route by reading its folder?
  • Does each component have obvious data sources?
  • Is mutation handled through forms or explicit functions?
  • Are derived values used instead of synchronized state?
  • Is error handling at boundaries, not scattered?
  • Would a new developer find their way?

If yes to all, you’ve built a calm system.

What’s Next?

You’ve now seen every pattern applied together. You understand:

  • Why data belongs in routes, not stores
  • How derived state prevents bugs
  • When $effect is (and isn’t) appropriate
  • How forms work with progressive enhancement
  • Where errors should be handled

The goal was never to memorize patterns. It was to develop intuition for what calm code looks like. When you’re building and something feels complicated, trust that feeling. There’s probably a simpler way.

Build things. Keep them simple. Revisit these patterns when something feels off.

Welcome to calm.