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 を省略できます。用途に応じて使い分けるのがポイントです。