SvelteKitのフレームワーク構造を整理した話

Next.jsと並んでよく使うフレームワークがSvelteKitです。 基本的な層の考え方はNext.jsと共通していますが、ルーティングの仕組みや状態管理の方法が異なります。 この記事では、SvelteKit特有の構造を意識しながら、層ごとの役割を整理します。

全体像

処理の流れは以下のとおりです。

+page.svelte(UI)
   ↓ Storeを参照・Actionを呼ぶ
Store(状態管理・ロジック)
   ↓ /api/... にfetch
API Route Handler(サーバの入口)
   ↓ UseCaseを呼ぶ(省略可)
UseCase(複雑な処理フローの場合のみ)
   ↓ Serviceを呼ぶ
Service(外部API・DB通信)
   ↓
外部API・DB

Next.jsのHookに相当するのがSvelteKitではStoreです。また、SvelteKitには +page.server.ts というサーバ専用のファイルがあり、ページ単位でのデータ取得(load関数)やフォーム送信(actions)をここで扱います。

ディレクトリ構成

タスク管理アプリを例にした構成です。

src/
  routes/
    +layout.svelte          # 全ページ共通レイアウト
    +page.svelte            # トップページ
    api/
      tasks/
        +server.ts          # GET(一覧取得)・POST(新規作成)
        [id]/
          +server.ts        # PATCH(更新)・DELETE(削除)
    tasks/
      +page.svelte          # タスク一覧ページ(UI)
      +page.server.ts       # サーバ側のload・actions
      [id]/
        +page.svelte        # タスク詳細ページ(UI)
        +page.server.ts     # 詳細データのload

  lib/
    components/
      tasks/
        TaskList.svelte     # タスク一覧の表示
        TaskCard.svelte     # タスク1件の表示
        TaskForm.svelte     # タスク作成・編集フォーム

    stores/
      taskStore.ts          # タスクの状態管理

    usecases/
      createTask.ts         # タスク作成(複数処理を伴う場合)

    services/
      taskService.ts        # タスクのCRUD
      notificationService.ts # 通知送信

    utils/
      date.ts               # 日付フォーマットなど汎用関数

    types/
      task.ts               # Task型の定義

SvelteKitでは src/lib/ 配下が $lib エイリアスで参照できる共有コードの置き場所です。routes/ はNext.jsの app/ と同様にルーティング専用です。

Component(UI層)― 表示専門

配置: src/lib/components/

画面の表示とユーザー操作の受け付けを担当します。データの取得や加工はStoreに任せ、受け取った値を表示することに専念します。

やらないこと

  • fetch を直接書かない
  • ビジネスロジックを書かない
<!-- src/lib/components/tasks/TaskList.svelte -->
<script lang="ts">
  import { tasks, loading, error } from "$lib/stores/taskStore";
  import TaskCard from "./TaskCard.svelte";
</script>

{#if $loading}
  <p>読み込み中...</p>
{:else if $error}
  <p>エラーが発生しました</p>
{:else}
  <ul>
    {#each $tasks as task (task.id)}
      <TaskCard {task} />
    {/each}
  </ul>
{/if}

Store(状態管理・ロジック層)― 状態の管理者

配置: src/lib/stores/

Next.jsのHookに相当する層です。Svelteの writable / readable を使って状態を管理し、/api/... へのfetchやデータ整形を行います。複数のComponentから参照できるのがHookとの大きな違いです。

やらないこと

  • 外部APIのURLを直書きしない(/api/... は書いてよい)
  • 秘密情報(APIキーなど)を扱わない
// src/lib/stores/taskStore.ts
import { writable, derived } from "svelte/store";
import type { Task } from "$lib/types/task";

export const tasks = writable<Task[]>([]);
export const loading = writable(true);
export const error = writable<string | null>(null);

export const fetchTasks = async () => {
  loading.set(true);
  error.set(null);
  try {
    const res = await fetch("/api/tasks");
    if (!res.ok) throw new Error("取得失敗");
    const data: Task[] = await res.json();
    tasks.set(data);
  } catch (e) {
    error.set(e instanceof Error ? e.message : "エラー");
  } finally {
    loading.set(false);
  }
};

// 未完了タスクのみを絞り込んだ派生Store
export const incompleteTasks = derived(tasks, $tasks =>
  $tasks.filter(t => !t.completed)
);

+page.server.ts(サーバ専用ファイル)― SvelteKit独自の仕組み

配置: src/routes/**/+page.server.ts

SvelteKitにはNext.jsにはない +page.server.ts という仕組みがあります。ページのサーバ側処理をここに書き、load 関数でページに必要なデータを事前取得したり、actions でフォーム送信を処理したりします。

// src/routes/tasks/+page.server.ts
import { getAllTasks, createTask } from "$lib/services/taskService";
import type { PageServerLoad, Actions } from "./$types";

// ページ表示時にサーバ側でデータを取得
export const load: PageServerLoad = async () => {
  const tasks = await getAllTasks();
  return { tasks };
};

// フォーム送信の処理
export const actions: Actions = {
  create: async ({ request }) => {
    const data = await request.formData();
    const title = data.get("title") as string;
    await createTask({ title });
    return { success: true };
  },
};

API Route Handler(サーバの入口)― クライアントとサーバの境界

配置: src/routes/api/

クライアント(Store)からのfetchを受け取り、ServiceまたはUseCaseを呼び出して結果を返します。APIキーなどの秘密情報はここより内側でのみ扱います。

やらないこと

  • UIロジックを書かない
  • ビジネスロジックを直接書かない(Serviceに委譲する)
// src/routes/api/tasks/+server.ts
import { getAllTasks, createTask } from "$lib/services/taskService";
import { json } from "@sveltejs/kit";
import type { RequestHandler } from "./$types";

export const GET: RequestHandler = async () => {
  const tasks = await getAllTasks();
  return json(tasks);
};

export const POST: RequestHandler = async ({ request }) => {
  const body = await request.json();
  const task = await createTask(body);
  return json(task, { status: 201 });
};

UseCase(ビジネスロジック層)― 複数処理の調整役

配置: src/lib/usecases/

複数のServiceを組み合わせて一連の処理を実現します。シンプルな処理であれば省略してAPI RouteやServerから直接Serviceを呼んでかまいません。

やらないこと

  • UIロジックを書かない
  • 外部APIに直接アクセスしない(Serviceに委譲する)
// src/lib/usecases/createTask.ts
import { createTask } from "$lib/services/taskService";
import { sendNotification } from "$lib/services/notificationService";
import { formatDate } from "$lib/utils/date";

type CreateTaskInput = {
  title: string;
  dueDate?: string;
};

export async function createTaskWithNotification(input: CreateTaskInput) {
  const task = await createTask({
    title: input.title,
    dueDate: input.dueDate,
    createdAt: formatDate(new Date()),
  });

  await sendNotification({
    message: `タスク「${task.title}」が作成されました`,
  });

  return task;
}

Service(外部通信層)― 外部世界との窓口

配置: src/lib/services/

実際の fetch やDB操作を書く層です。外部APIやDBの仕様に依存する部分をここに集約します。

やらないこと

  • UIロジックを書かない
  • 複数のServiceをまたいだ処理を書かない(それはUseCaseの役割)
// src/lib/services/taskService.ts
import { db } from "$lib/db";
import type { Task } from "$lib/types/task";

export async function getAllTasks(): Promise<Task[]> {
  return db.task.findMany({ orderBy: { createdAt: "desc" } });
}

export async function createTask(data: Omit<Task, "id">): Promise<Task> {
  return db.task.create({ data });
}

Next.jsとの主な違い

項目 Next.js SvelteKit
状態管理 Hook(useState Store(writable
サーバ処理 API Route Handler +page.server.ts または API Route Handler
共有コードの置き場 src/ 配下(任意) src/lib/$lib エイリアス)
UIファイル .tsx .svelte
APIレスポンス Response.json() json()@sveltejs/kit

まとめ

各層の責務を一覧にまとめます。

配置 主な責務
Component lib/components/ 表示・ユーザー操作
Store lib/stores/ 状態管理・API呼び出し
+page.server.ts routes/**/ サーバ側のデータ取得・フォーム処理
API Route Handler routes/api/ サーバの入口・秘密情報の保護
UseCase lib/usecases/ 複数Serviceの組み合わせ(省略可)
Service lib/services/ 外部APIやDBとの通信

SvelteKit独自の +page.server.ts をうまく使うと、APIを経由せずにサーバ側でデータを取得してページに渡せるため、シンプルなページ表示であれば Store や API Route Handler を省略できます。用途に応じて使い分けるのがポイントです。

← トップページに戻る