[Svelte Series 20] Capstone: A Mini App That Brings Together State, Routing, and Accessibility

한국어 버전

Welcome to the final installment. Everything you learned now converges into Task Orbit, a small task manager that stitches together routing, state, accessibility, and deployment.

Before you begin

  • What you build: a Task Orbit mini app with / (home), /stats, and /activity routes that integrates server actions, stores, accessibility announcements, and deployment scripts.
  • Why it matters: chaining chapters 1–19 into one flow cements the patterns so you can reuse them in real projects.
  • Watch out: lock your folder structure first, then document how server actions, stores, and routes exchange data to avoid knots during refactors.

Key terms

  1. Capstone: an end-to-end project that applies every technique—from design to deployment—in one run.
  2. Routing: picking which page component to render for /stats, /activity, and other paths.
  3. Accessibility live region: an aria-live area or screen-reader-only block that announces updates without a visible UI change.

Core ideas

A capstone glues together previously isolated lessons. State represents the data your components remember, derived stores compute additional values, routing swaps screens based on URLs, and accessibility ensures everyone can use the app—including assistive technology users.

Understanding the Task Orbit App Map

Client UIsrc/routes/+layout.server.tssrc/routes/+page.server.tssrc/routes/stats/+page.server.tssrc/routes/activity/+page.server.tssrc/lib/stores/tasks.tsTaskForm·TaskList·StatCard·ActivityLogsrc/routes/api/tasks/+server.tsPlanetScale/Postgressrc/hooks.server.ts request + session cookieprepare locals.userlayout dataaction POST create/togglefetch RESTCRUD tasksJSONinitial datasubscribederived metricsSSE stream

👉 Mini recap: the layout checks the session, each page server module gathers its data, and every screen consumes the shared tasks store. /stats adds derived metrics, while /activity bolts on an SSE subscription.

Code tour

Start with the folder layout and key modules.

  lib/
    stores/tasks.ts
    router.ts
    accessibility.ts
    server/
      notifications.ts
  routes/
    +layout.svelte
    +layout.server.ts
    +page.svelte
    +page.server.ts
    stats/+page.svelte
    stats/+page.server.ts
    activity/+page.svelte
    api/tasks/+server.ts
  components/
    TaskForm.svelte
    TaskList.svelte
    StatCard.svelte
    ActivityLog.svelte
  app.d.ts
// src/lib/stores/tasks.ts

type Task = {
  id: string;
  title: string;
  priority: 'low' | 'medium' | 'high';
  done: boolean;
  createdAt: number;
};

export const tasks = writable<Task[]>([]);

export const pendingTasks = derived(tasks, ($tasks) =>
  $tasks.filter((task) => !task.done)
);

export const stats = derived(tasks, ($tasks) => ({
  total: $tasks.length,
  done: $tasks.filter((task) => task.done).length,
  ratio: $tasks.length
    ? Math.round(($tasks.filter((task) => task.done).length / $tasks.length) * 100)
    : 0
}));
<!-- src/components/TaskForm.svelte -->
<script lang="ts">
  import { enhance } from '$app/forms';
  import { invalidate } from '$app/navigation';
  export let action = '?/create';
</script>

<form method="POST" {action}
  use:enhance={({ result }) => {
    if (result.type === 'success') invalidate('data:tasks');
  }}>
  <label>
    Title
    <input name="title" placeholder="New task" required />
  </label>
  <label>
    Priority
    <select name="priority">
      <option value="low">Low</option>
      <option value="medium" selected>Medium</option>
      <option value="high">High</option>
    </select>
  </label>
  <button type="submit">Add</button>
</form>
<!-- src/routes/+page.svelte -->
<script lang="ts">
  import TaskForm from '$components/TaskForm.svelte';
  import TaskList from '$components/TaskList.svelte';
  export let data;
</script>

<TaskForm />
<TaskList tasks={data.tasks} />

<section aria-live="polite" class="sr-only">
  Completion rate {data.stats.ratio}%
</section>
// src/routes/+page.server.ts

export const load = async ({ fetch, depends }) => {
  depends('data:tasks');
  const [tasks, stats] = await Promise.all([
    fetch('/api/tasks').then((r) => r.json()),
    fetch('/api/tasks/stats').then((r) => r.json())
  ]);
  return { tasks, stats };
};

export const actions = {
  create: async ({ request, fetch }) => {
    const form = Object.fromEntries(await request.formData());
    if (!form.title) {
      return fail(400, { errors: { title: 'Title is required.' } });
    }
    const res = await fetch('/api/tasks', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(form)
    });
    if (!res.ok) {
      return fail(500, { errors: { title: 'Could not save the task.' } });
    }
    return { success: true };
  }
};
// src/lib/router.ts

export const currentSection = derived(page, ($page) => $page.url.pathname.replace('/', '') || 'home');

export function navigate(section: string) {
  goto(section === 'home' ? '/' : `/${section}`);
}
pnpm run lint
pnpm run test
pnpm run check
pnpm run build

Ship to Vercel or Netlify once linting, tests, and type checks are green.

https://task-orbit-demo.local
DEV

UI Preview

CRUD, metrics, and realtime logs in one

Adding a task updates the home and stats views simultaneously, and the Activity tab streams new events over SSE.

SvelteKit actionsSSE

Tasks

Home view

TaskForm + TaskList + aria-live.

Stats

Derived store

Calculates totals and completion.

Activity

Realtime

EventSource keeps the log fresh.

👉 Mini recap: Home handles CRUD plus accessibility, Stats only needs derived stores, and Activity layers on SSE. Remember that all three rely on the same data source.

Why it matters

When concepts stay modular, you immediately know where to edit when requirements change. Once routing and state work together, performance and accessibility become easier to audit. The capstone also gives you a portfolio-ready talking point.

Practice

  • Try it: copy the folder structure and implement CRUD, stats, and the activity feed.
  • Extend it: use the currentSection store to add a unified nav or keyboard shortcuts.
  • Debug it: when accessibility warnings arise, inspect aria-live, aria-invalid, and focus management.
  • Definition of done: adding tasks, computing metrics, changing sections, and hearing announcements all work, and pnpm run check, pnpm run test, plus pnpm run build succeed.

Wrap-up

This capstone ties together state management, routing, accessibility, and deployment. If chapter 12 was a checkpoint, chapter 20 is the finish line. Completing it means you know how to assemble a small Svelte product end to end—now extend it with your backend APIs or design system and shape your own SaaS prototype.

💬 댓글

이 글에 대한 의견을 남겨주세요