さとまたwiki
🚀 思考模写シリーズ

プロエンジニアの思考模写
SvelteKitで作って公開するまで

素人は npm create から機能を付け足す。プロは仕様→データ→ルーティング→UI→テスト→デプロイまで頭の中で組み立ててからコードを書く。その思考過程と SvelteKit 独自のエッジを完全模写する。

🎯 Section 1: 今日のゴール

このページで達成するゴール

SvelteKit で作った小さなアプリを Cloudflare Pages にデプロイし、「このURL見てください」と人に渡せる状態を作る。コードを書く前に、プロがどんな順序・どんな視点で全体を設計するかを完全に模写する。

対象読者

  • • HTML/CSS/JS は書けるが SvelteKit は初めて
  • • React/Vue を触ったことがある
  • • とりあえず動くものは作れるが「正しい設計」がわからない

完成物のイメージ

  • • CRUD を含む小さなWebアプリ
  • • Cloudflare Pages にデプロイ済み
  • • 独自ドメインまたは pages.dev のURL
  • • GitHub で CI/CD が回っている

必要な前提知識

  • • JavaScript / TypeScript 基礎
  • • Git / GitHub の基本操作
  • • ターミナル操作(cd, ls, mkdir)
  • • HTTPリクエストの概念(GET/POST)

この記事の核心: プロエンジニアはコードを書く前に「頭の中でアプリが完成している」。コーディングは設計の写経に過ぎない。その思考順序と判断基準を完全に言語化する。

🔄 Section 2: 素人 vs プロの思考差

フェーズ❌ 素人の行動✅ プロの思考
スタートnpm create svelte を実行して index 画面を開く「誰が・何を・なぜ使うか」を1枚のメモに書く
データ変数を増やしながらなんとなく状態を持つエンティティとリレーションを図で確認してから DB スキーマを書く
ルーティングURL を後から考えて +page.svelte を乱造するURL 設計を先に紙に書いてディレクトリ構造を決める
認証後から「ログインも要るな」となってリファクタ地獄認証が要るかどうかを最初に決め、hooks.server.ts の設計に織り込む
デプロイ動いてから「どこに出すか」を考えてハマるCloudflare Pages + adapter-cloudflare を最初から前提に書く
テスト完成後に「テストも書かなきゃ」となって書けないビジネスロジックを純粋関数に切り出し、最初からテスタブルに設計する

プロが持っている「完成形の地図」

プロエンジニアはコードを1行も書く前に、URL一覧・テーブル設計・コンポーネントツリー・デプロイ先・テスト戦略が頭の中で確定している。コーディングはその地図に従って座標を埋める作業に過ぎない。「コードを書きながら考える」状態は、地図なしで山を登るのと同じ。

📐 Section 3: 作るものを決める(仕様とスコープ)

エッジ①: スコープを「切り捨て」で決める

プロは「何を作るか」ではなく「何を作らないか」を先に決める。最初のバージョンで要らない機能は全部スコープ外にする。スコープ外の機能は Todo リストにもしない。

例として「読書記録アプリ(Booknote)」を題材にする。この判断プロセスがどのアプリにも使えるテンプレートになる。

✅ v1.0 スコープ IN

  • • 本を登録する(タイトル・著者・読了日・メモ)
  • • 一覧表示(新しい順)
  • • 詳細ページ(1冊ずつ)
  • • 削除
  • • Cloudflare Pages でパブリック公開

❌ v1.0 スコープ OUT(後でいい)

  • • ユーザー認証・ログイン
  • • 画像アップロード(書影)
  • • タグ・カテゴリ機能
  • • 検索・フィルタ
  • • レーティング(星5評価)
  • • ソーシャル共有

プロの自問チェック(仕様決定前に必ず通す)

  1. 「このアプリが存在しない世界で、ユーザーは今何をしているか」— 代替手段を把握する
  2. 「最低限これだけあれば使ってもらえるか」— MVP(最小動作プロダクト)の境界を引く
  3. 「このデータは将来的にどう拡張するか」— スキーマを壊さない設計の余地を残す
  4. 「認証が要るか」— 要らないなら最初から排除。要るなら全設計に織り込む
  5. 「デプロイ先の制約は何か」— Cloudflare Workers ではファイルシステムが使えない等

🗄️ Section 4: データモデル設計

選択肢A: ファイル(JSON/SQLite)

  • ✅ 設定系・静的データに向く
  • ✅ デプロイ時にファイルが含まれる
  • ❌ Cloudflare Workers では /tmp のみ書き込み可
  • ❌ 並行書き込みで壊れる

選択肢B: Turso(libSQL)

  • ✅ エッジから低遅延でアクセス可能
  • ✅ SQLite 互換で学習コストが低い
  • ✅ 無料枠が実用的(月500MB)
  • ✅ Cloudflare Pages と相性抜群

選択肢C: Supabase / PlanetScale

  • ✅ 認証・RLSが組み込み
  • ✅ チーム開発に向く
  • ❌ コールドスタートで遅い場合あり
  • ❌ 無料枠の制限に注意

エッジ②: Turso を選ぶ理由(2026年ベストプラクティス)

Cloudflare Pages + Turso が2026年の小規模アプリの黄金構成。SQLite 互換で型付き操作が可能(drizzle-orm との組み合わせが鉄板)。D1(Cloudflare 公式)より Turso が優れている点は「マルチリージョン・レプリケーション」と「ベンダーロックインの薄さ」。

Booknote のスキーマ設計例(drizzle-orm):

// src/lib/server/db/schema.ts
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';

export const books = sqliteTable('books', {
  id:        text('id').primaryKey(),        // nanoid() で生成
  title:     text('title').notNull(),
  author:    text('author').notNull(),
  readAt:    text('read_at').notNull(),       // ISO8601 文字列
  memo:      text('memo').default(''),
  createdAt: integer('created_at', { mode: 'timestamp' })
               .notNull()
               .$defaultFn(() => new Date()),
});

プロがスキーマ設計時に考えること

  • id は nanoid で生成 — auto increment は分散環境で衝突リスク
  • 日時は ISO8601 文字列 — SQLite は DATE 型を持たない
  • nullable を最小化 — NOT NULL のほうがバグが減る
  • 将来の拡張を最初から考えない — 必要になったときにマイグレーションを書く

🗺️ Section 5: ルーティング設計(File-based routing の本質)

エッジ③: File-based routing — ディレクトリ構造がURLそのもの

SvelteKit では src/routes/ 以下のディレクトリ構造が URL に直結する。ルーティング設定ファイルは存在しない。プロはまずURLを設計し、それをディレクトリ構造として書き下す。

URL設計(先にこれを紙に書く)

URL役割
/本の一覧
/books/new新規登録フォーム
/books/[id]詳細ページ
/books/[id]/edit編集フォーム

ディレクトリ構造(URLの写経)

src/routes/
├── +layout.svelte       # 全ページ共通ナビ
├── +layout.server.ts    # 認証チェック等
├── +page.svelte         # / (一覧)
├── +page.server.ts      # DB から一覧取得
└── books/
    ├── new/
    │   ├── +page.svelte
    │   └── +page.server.ts  # Form Action
    └── [id]/
        ├── +page.svelte
        ├── +page.server.ts  # 1件取得
        └── edit/
            ├── +page.svelte
            └── +page.server.ts

動的ルートのパターン(プロの使い分け)

[id]必須パラメータ。/books/abc123 の abc123 が params.id に入る
[[optional]]任意パラメータ。存在しない場合は undefined
[...rest]残余パラメータ。/docs/a/b/c を全部受ける
(group)URLに影響しないグループ。layout を分けたいときに使う

🧩 Section 6: コンポーネント設計(+page / +layout / +server の使い分け)

エッジ④: SvelteKit の特殊ファイルの役割分担

SvelteKit には7種類の「予約ファイル名」がある。それぞれの責務を正確に理解することがプロへの第一歩。素人はこれを混在させて「どこに書いていいかわからない」状態になる。

+page.svelte

UIの定義(クライアントで動く)

そのURLの画面を描画する Svelte コンポーネント。load 関数から受け取ったデータを表示するだけにとどめる。ロジックを混ぜない。

+page.server.ts

サーバー専用 load 関数 + Form Actions

DB アクセス・秘密情報の取り扱い・認証チェックはここだけに書く。クライアントには絶対に送られない。Form Actions(POST処理)もここに集約する。

+page.ts

ユニバーサル load 関数(SSR + CSR 両方で動く)

パブリックな API にアクセスする場合など、サーバーでもクライアントでも同じコードが動いていい場合に使う。DB直結や秘密環境変数はここに書かない。

+layout.svelte

共通レイアウト(ナビ・フッター・テーマ)

子ページの共通枠を定義する。<slot /> に子ページが入る。routes/ 直下のものが全ページに適用。サブディレクトリに置くとそのグループだけに適用。

+server.ts

API エンドポイント(REST / JSON)

GET/POST/DELETE など HTTP メソッドを関数として export する。外部から JSON を返す API が必要なときだけ使う。SPA + API 構成のときに活躍。

hooks.server.ts

全リクエストのミドルウェア

handle 関数で全リクエストをインターセプト。JWT 検証・CORS・レート制限・ログをここに書く。認証が要るアプリではここが最重要ファイル。

Section 7: Runes と状態管理(Svelte 5)

エッジ⑤: Runes — リアクティビティの完全刷新

Svelte 5 で導入された Runes($state, $derived, $effect, $props)は Svelte 4 の暗黙的リアクティビティを明示的に置き換えた。React の hooks に近い構文だが、コンパイラが最適化するため実行時オーバーヘッドがほぼゼロ。

$state — 変更を追跡するリアクティブな変数

<script lang="ts">
  // Svelte 4(廃止予定)
  let count = 0;  // $: が自動追跡

  // Svelte 5 Runes(推奨)
  let count = $state(0);  // 明示的に宣言
  // ネストしたオブジェクトも deep reactive
  let user = $state({ name: 'Alice', age: 30 });
</script>

$derived — 他の状態から計算される値

<script lang="ts">
  let books = $state<Book[]>([]);

  // $derived: books が変わると自動再計算
  let total = $derived(books.length);
  let recent = $derived(
    books
      .slice()
      .sort((a, b) => b.readAt.localeCompare(a.readAt))
      .slice(0, 5)
  );
</script>

$effect — 副作用(DOM操作・外部API呼び出し)

<script lang="ts">
  let query = $state('');

  $effect(() => {
    // query が変わるたびに実行
    // クリーンアップ関数を return できる
    const timer = setTimeout(() => fetchResults(query), 300);
    return () => clearTimeout(timer);  // デバウンス
  });
</script>

$props — コンポーネントのプロパティ定義

<!-- BookCard.svelte -->
<script lang="ts">
  // Svelte 4: export let book: Book;
  // Svelte 5 Runes:
  let { book, onDelete = () => {} } = $props<{
    book: Book;
    onDelete?: () => void;
  }>();
</script>

いつ store を使い、いつ Runes を使うか

Runes ($state) を使う場面

  • • そのコンポーネント内でだけ使う状態
  • • 親から子に渡すデータ
  • • フォームの入力値

store ($writable など) を使う場面

  • • 複数のコンポーネントで共有する状態
  • • テーマ・ロケール・認証ユーザー情報
  • • URL をまたいで保持したい状態

📬 Section 8: Form Actions と Progressive Enhancement

エッジ⑥: Form Actions — JS なしでも動くフォーム処理

Progressive Enhancement(プログレッシブエンハンスメント)とは「JavaScriptが無効でも基本機能が動き、JSが有効なときは体験が向上する」設計原則。SvelteKit の Form Actions はこれを標準でサポートする唯一のメジャーフレームワーク機能。

+page.server.ts(Action の定義)

// src/routes/books/new/+page.server.ts
import type { Actions } from './$types';
import { db } from '$lib/server/db';
import { books } from '$lib/server/db/schema';
import { nanoid } from 'nanoid';
import { redirect } from '@sveltejs/kit';

export const actions: Actions = {
  default: async ({ request }) => {
    const data = await request.formData();
    const title  = data.get('title') as string;
    const author = data.get('author') as string;
    const readAt = data.get('readAt') as string;
    const memo   = data.get('memo') as string;

    if (!title || !author || !readAt) {
      return { error: '必須項目を入力してください' };
    }

    await db.insert(books).values({
      id: nanoid(), title, author, readAt, memo
    });

    redirect(303, '/');
  }
}

+page.svelte(enhance でUXを向上)

<script lang="ts">
  import { enhance } from '$app/forms';
  let { form } = $props();
</script>

<!-- JS なしでも動く(標準フォーム送信) -->
<form method="POST" use:enhance>
  <!-- use:enhance でページ全リロードを防ぐ -->
  <input name="title" required />
  <input name="author" required />
  <input name="readAt" type="date" required />
  <textarea name="memo"></textarea>

  {#if form?.error}
    <p class="text-red-500">{ form.error }</p>
  {/if}

  <button type="submit">登録</button>
</form>

use:enhance のカスタマイズ(プロの使い方)

<form method="POST" use:enhance={ () => {
  isSubmitting = true;  // ローディング状態を手動制御
  return async ({ result, update }) => {
    isSubmitting = false;
    if (result.type === 'success') toast('登録しました');
    await update();  // form データ更新
  };
} }>

🔬 Section 9: SvelteKit エッジ集(独自機能10+キーポイント)

エッジ⑦: SSR vs CSR の選択が1行

// +page.ts
export const ssr = false;   // CSR のみ
export const csr = false;   // 静的HTML
export const prerender = true; // ビルド時生成

ページごとに SSR/CSR/静的を切り替えられる。Next.js より直感的。

エッジ⑧: Load 関数のデータ受け渡し

// +page.server.ts
export async function load({ params }) {
  const book = await db.query.books
    .findFirst({ where: eq(books.id, params.id) });
  if (!book) error(404, 'Not Found');
  return { book };  // 型安全で page に渡る
}

return した値が +page.svelte の data プロパティに型安全で渡る。

エッジ⑨: $app/navigation の prefetch

<!-- ホバーした瞬間にデータ取得開始 -->
<a href="/books/{ book.id }" data-sveltekit-preload-data="hover">
  { book.title }
</a>

属性1つでリンク先のデータをプリフェッチ。体感速度が劇的に向上。

エッジ⑪: $env — 環境変数の安全な分離

// サーバー専用(クライアントに漏れない)
import { TURSO_URL } from '$env/static/private';

// クライアント公開OK(PUBLIC_ プレフィックス必須)
import { PUBLIC_API_URL } from '$env/static/public';

private を client で import するとビルドエラー。型レベルで秘密情報漏えいを防ぐ。

エッジ⑫: Adapter — デプロイ先を差し替え可能

// svelte.config.js
import adapter from '@sveltejs/adapter-cloudflare';

export default {
  kit: { adapter: adapter() }
};

adapter-auto, adapter-node, adapter-vercel, adapter-cloudflare を切り替えるだけでデプロイ先を変更できる。

🧪 Section 10: テスト戦略(vitest + playwright 最小構成)

ユニットテスト(vitest)

対象: ビジネスロジックの純粋関数

  • • バリデーション関数
  • • 日付フォーマット関数
  • • スキーマ変換ロジック

⚡ 高速・安定。最優先で書く

統合テスト(vitest + @testing-library)

対象: load 関数・Form Actions

  • • DB への insert/select が正しいか
  • • バリデーションエラーが返るか
  • • 404 が正しく返るか

🔄 適度に書く

E2E テスト(playwright)

対象: クリティカルパス

  • • 本の登録→一覧表示の流れ
  • • 削除後に一覧から消えるか

🐢 遅いので最小限に

テスタブルな設計のコツ

「テストが書きにくい」と感じたら設計に問題がある。バリデーション・変換・計算を src/lib/utils.ts の純粋関数として切り出せば、DB や DOM に依存せずテストできる。load 関数の中でロジックを完結させると一切テストできない。

☁️ Section 11: Cloudflare Pages デプロイ(adapter + wrangler)

adapter-cloudflare — Workers 環境に最適化

Cloudflare Pages は Node.js ではなく V8 isolates 上で動く。adapter-cloudflare を設定することで SvelteKit が自動的に Workers 互換のバンドルを生成する。

ステップ1: adapter をインストール・設定

bun add -D @sveltejs/adapter-cloudflare

# svelte.config.js
import adapter from '@sveltejs/adapter-cloudflare';

export default {
  kit: {
    adapter: adapter({
      routes: { include: ['/*'], exclude: [''] }
    }),
  }
};

ステップ3: GitHub 連携で自動デプロイ

1.Cloudflare ダッシュボード → Workers & Pages → Create application → Pages → Connect to Git
2.リポジトリを選択 → Branch: main → Build command: bun run build → Build output: .svelte-kit/cloudflare
3.Environment Variables タブで TURSO_URL / TURSO_AUTH_TOKEN を設定
4.Save and Deploy → main push のたびに自動デプロイ

Cloudflare Workers で使えないもの(注意)

❌ 使えない

  • • Node.js の fs(ファイルシステム)
  • • Node.js の crypto(Web Crypto API を使う)
  • • 一部の npm パッケージ(Node 依存のもの)
  • • 長時間実行(最大 50ms CPU 時間)

✅ 使える

  • • Fetch API
  • • Web Crypto API
  • • Turso / D1(HTTP ベース)
  • • KV / R2 / Durable Objects

⚠️ Section 12: 素人が陥る罠10選

罠①: +page.ts と +page.server.ts を混同する

対策: 秘密情報・DB アクセスは全て .server.ts に書く。$env/static/private を使えばビルド時に型エラーになる。

罠②: fetch を load 関数の外でクライアント実行する

症状: SSR 時に window is not defined でクラッシュ。対策: データ取得は load 関数に集約。クライアント専用処理は $effect / onMount の中だけ。

罠③: 環境変数を PUBLIC_ なしでクライアントに渡す

症状: API キーが DevTools から丸見えになる。対策: クライアントに渡す値は PUBLIC_ プレフィックス必須。$env/static/private は .server.ts のみ。

罠④: writable store をサーバーで共有して別ユーザーのデータが混入

対策: サーバーでモジュールレベルの状態を持たない。セッション情報は event.locals / cookie で管理する。

罠⑤: リロードで $state の値が消える

対策: $state はメモリ上の値。永続化が必要な値は localStorage / URL パラメータに持つ。

罠⑥: .env ファイルを git に commit する

対策: プロジェクト作成直後に .env, .env.local を .gitignore に追加。Cloudflare の環境変数はダッシュボードで管理する。

罠⑦: adapter 選択を間違えてデプロイが壊れる

対策: Cloudflare Pages には必ず adapter-cloudflare を使う。adapter-auto は開発中のみ使い、本番前に差し替える。

罠⑧: Form Action なしで fetch + JSON で CRUD を実装する

対策: SvelteKit では Form Actions + use:enhance が標準。API エンドポイントは外部から叩かれる場合だけ作る。

罠⑨: $effect を使いすぎて無限ループになる

対策: $effect は外部副作用(DOM 操作・タイマー・外部 API)のみ。データ変換には $derived を使う。

罠⑩: Workers の CPU 時間制限(50ms)を超える処理

対策: 画像処理・PDF 生成は Cloudflare Queue や外部サービスに移譲する。Workers はデータ取得・変換・返却に徹する。

📖 Section 13: 用語集

用語意味
SSRServer-Side Rendering。リクエストを受けたサーバーが HTML を生成してブラウザに返す。SEO と初回表示速度に有利。
CSRClient-Side Rendering。ブラウザが JavaScript を実行して画面を描画する。初回は空の HTML だが操作が多いアプリに向く。
ハイドレーションサーバーが生成した静的 HTML に JavaScript の動的な振る舞いを後から結びつける処理。SvelteKit は自動的に最小限のハイドレーションを行う。
Load 関数+page.svelte のデータを事前取得するための関数。export function load() として定義し、return した値が data プロパティで受け取れる。
Form Actions+page.server.ts で定義する POST 処理のハンドラ。export const actions = { default: async ({ request }) => {} }; の形で書く。JS なしでも動く。
RunesSvelte 5 で導入されたリアクティビティの宣言構文($state, $derived, $effect, $props)。コンパイラが処理するため実行時オーバーヘッドがほぼゼロ。
AdapterSvelteKit のビルド出力をデプロイ先の形式に変換するプラグイン。adapter-cloudflare, adapter-node, adapter-vercel 等がある。
Progressive EnhancementJS が無効でも基本機能が動き、JS が有効なときに体験が向上する設計原則。フォームが代表例。
drizzle-ormTypeScript 向け軽量 ORM。SQLite / Turso / PostgreSQL に対応。型安全なクエリが書けて、マイグレーション管理も可能。
MVPMinimum Viable Product(最小動作プロダクト)。ユーザーに価値を届けられる最小限の機能セット。最初のリリースはここまでと決める。

Section 14: 再現チェックリスト

設計フェーズ

実装フェーズ

テスト・デプロイ

🏁 Section 15: まとめ

プロエンジニアの思考を一言で

"コードを書く前に、頭の中でアプリが完成している。"

仕様→データモデル→URL設計→コンポーネント責務→テスト戦略→デプロイ先。この順番で思考し、全ての判断に「なぜ」が言える状態でコーディングに入る。SvelteKit はこの思考を最小の摩擦でコードに落とすための構造を持っている。

今日から変えること

  • • npm create の前に URL 設計を書く
  • • DB スキーマを先に決める
  • • .server.ts で DB を触る

SvelteKit の強みを使う

  • • Form Actions で POST を簡潔に
  • • Runes で宣言的に状態管理
  • • adapter-cloudflare で無料エッジ公開

次のステップ

  • • Turso + drizzle-orm を実際に繋ぐ
  • • hooks.server.ts で認証を追加
  • • playwright で E2E テストを1本書く

採用した SvelteKit エッジ一覧

① スコープ切り捨て ② Turso ベストプラクティス ③ File-based routing ④ 特殊ファイル役割分担 ⑤ Svelte 5 Runes ⑥ Form Actions + PE ⑦ SSR/CSR 切り替え ⑧ Load 関数データ渡し ⑨ $env 環境変数分離 ⑩ adapter-cloudflare