さとまた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: 作るものを決める(仕様とスコープ)

  1. 1

    ゴール確定

    「誰が・何を・なぜ使うか」を1枚のメモに書く

    プロはコードより先にゴールを言語化する。例:「読書記録アプリ(Booknote)— 読んだ本を登録してURLで共有できる状態を作る」。この1文が全判断の基準になる。

  2. 2

    ユーザー視点

    「このアプリが存在しない世界でユーザーは何をしているか」

    代替手段(メモ帳・スプレッドシート・Notion)を把握することで、自分のアプリが提供すべき価値が明確になる。代替手段を超えない機能は作らない。

  3. 3

    MVP確定

    「最低限これだけあれば使ってもらえるか」でスコープ IN を決める

    Booknote の v1.0 スコープ IN: 本を登録する(タイトル・著者・読了日・メモ)、一覧表示(新しい順)、詳細ページ、削除、Cloudflare Pages でパブリック公開。これだけ。

  4. 4

    スコープ切り(エッジ①)

    「何を作らないか」を先に決める — スコープ OUT リスト

    スコープ OUT の例: ユーザー認証・ログイン、画像アップロード(書影)、タグ・カテゴリ、検索・フィルタ、レーティング(星5評価)、ソーシャル共有。スコープ外は Todo リストにも書かない。

  5. 5

    成功指標

    「認証が要るか」「デプロイ先の制約は何か」を最初に確定する

    認証が要らないなら全設計から排除。要るなら hooks.server.ts の設計に最初から織り込む。Cloudflare Workers ではファイルシステムが使えない等のデプロイ先制約も仕様フェーズで確定させる。

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

  1. 1

    テーブル設計

    エンティティとカラムを先に決める

    Booknote の books テーブル: id(nanoid)、title、author、readAt(ISO8601)、memo、createdAt(timestamp)。auto increment は分散環境で衝突リスクがあるため id は nanoid で生成する。nullable は最小化し NOT NULL にするほどバグが減る。

    // 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()),
    });
  2. 2

    リレーション

    将来の拡張を「最初から考えない」設計

    v1.0 はテーブル1枚で十分。タグ・カテゴリが必要になったらそのときにマイグレーションを書く。先読みしすぎた設計は実装コストが上がるだけで、結局要らなかったことが多い。drizzle-orm なら型安全なマイグレーションが後から書ける。

  3. 3

    どこに置くか(エッジ②)

    Turso を選ぶ理由 — 2026年ベストプラクティス

    Cloudflare Pages + Turso が2026年小規模アプリの黄金構成。ファイル(JSON/SQLite)は Cloudflare Workers で /tmp のみ書き込み可で並行書き込みに弱い。Supabase / PlanetScale はコールドスタートが遅い。Turso はエッジから低遅延、SQLite 互換、無料枠が実用的(月500MB)、ベンダーロックインが薄い。

  4. 4

    マイグレーション

    drizzle-kit で型安全なスキーマ管理

    drizzle-orm + drizzle-kit の組み合わせが鉄板。スキーマを TypeScript で書けば型が自動生成されクエリがコンパイル時に検証される。D1(Cloudflare 公式)より Turso が優れている点は「マルチリージョン・レプリケーション」と「ベンダーロックインの薄さ」。マイグレーションファイルは git で管理する。

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

  1. 1

    URLマップ

    先に「URLを紙に書く」 — これがルーティング設計の全て

    Booknote のURL設計: /(一覧)、/books/new(新規登録フォーム)、/books/[id](詳細)、/books/[id]/edit(編集フォーム)。このURL一覧が決まればディレクトリ構造は自動的に決まる。

  2. 2

    dir構造(エッジ③)

    ディレクトリ構造 = URLの写経 — 設定ファイルは存在しない

    SvelteKit では src/routes/ 以下のディレクトリ構造が 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
  3. 3

    動的ルート

    4種類の動的ルートパターンの使い分け

    [id]必須パラメータ。/books/abc123 の abc123 が params.id に入る
    [[optional]]任意パラメータ。存在しない場合は undefined
    [...rest]残余パラメータ。/docs/a/b/c を全部受ける
  4. 4

    group layout

    (group)— URLに影響しないグループでレイアウトを分ける

    カッコ付きディレクトリ (group) は URL には表れないが、そのグループ専用の +layout.svelte を持てる。例: (auth)/login(app)/dashboard で別レイアウト。認証が要るページとパブリックページのレイアウトを明確に分離できる。

  5. 5

    認可

    認証チェックは hooks.server.ts か +layout.server.ts に集約する

    認可が必要なルートグループを (auth) グループにまとめ、そのグループの +layout.server.ts でセッション検証を行う。全リクエストに適用する場合は hooks.server.ts の handle 関数に書く。各 +page.server.ts に個別に書くと漏れが生じるので必ず集約する。

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

  1. 1

    +layout役割(エッジ④)

    +layout.svelte — 共通レイアウト(ナビ・フッター・テーマ)

    子ページの共通枠を定義する。<slot /> に子ページが入る。routes/ 直下のものが全ページに適用。サブディレクトリや (group) に置くとそのグループだけに適用。ナビバー・フッター・テーマプロバイダーはここに書く。

  2. 2

    +page役割

    +page.svelte — UIの定義だけに集中(ロジックを混ぜない)

    そのURLの画面を描画する Svelte コンポーネント。load 関数から受け取ったデータを表示するだけにとどめる。DB アクセス・バリデーション・副作用を混ぜると +page.server.ts の存在意義が失われる。「表示だけ」の純粋さを保つ。

  3. 3

    +page.server役割

    +page.server.ts — サーバー専用 load 関数 + Form Actions

    DB アクセス・秘密情報の取り扱い・認証チェックはここだけに書く。クライアントには絶対に送られない。Form Actions(POST処理)もここに集約する。+page.ts(ユニバーサル)はパブリック API など DB 直結・秘密環境変数が不要な場合のみ使う。

  4. 4

    +server API

    +server.ts — 外部から叩かれる API エンドポイントのみ

    GET/POST/DELETE など HTTP メソッドを関数として export する。外部から JSON を返す API が必要なときだけ使う。SPA + API 構成のときに活躍。SvelteKit 内部の CRUD には Form Actions を使い、+server.ts は外部連携専用にする。

  5. 5

    コンポーネント切出し

    hooks.server.ts + src/lib/ — ミドルウェアと共通コンポーネント

    hooks.server.ts の handle 関数は全リクエストをインターセプトする。JWT 検証・CORS・レート制限・ログをここに書く。認証が要るアプリではここが最重要ファイル。表示を担う再利用コンポーネント(BookCard.svelte 等)は src/lib/components/ に切り出し、+page.svelte をシンプルに保つ。

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+キーポイント)

  1. 1

    エッジ①

    File-based routing — ディレクトリ構造がURLそのもの

    プロは「URL設計=ディレクトリ設計」で考える。`routes/books/[id]/+page.svelte` がそのまま `/books/:id` になる。Next.js App Router と違い、特殊ファイル名で役割が明確に分離される。

  2. 2

    エッジ②

    特殊ファイル役割分担(+page / +layout / +page.server / +server / hooks.server)

    プロは最初にこの分担表を頭に入れる。+page.svelte=UI、+page.ts=両環境load、+page.server.ts=サーバー専用load/Form Actions、+server.ts=API、+layout.svelte=共通UI、hooks.server.ts=全リクエスト横断処理。役割を跨ぐと保守性が崩れる。

  3. 3

    エッジ③

    Svelte 5 Runes($state / $derived / $effect / $props)

    プロは「再計算が必要なら $derived、副作用なら $effect、props受取なら $props」と即座に判断。VueのrefやuseStateの重い構文より認知負荷が低い。ランタイムが差分を自動追跡するので宣言的に書ける。

  4. 4

    エッジ④

    Form Actions + use:enhance で Progressive Enhancement

    プロは CRUD を Form Actions で書く。JSが無効でもフォーム送信が動き、JSが有効なら SPA的にスムーズに動く。fetch + JSON を書く必要がなくなり、CSRF 対策やサーバーバリデーションも自動で組み込める。

  5. 5

    エッジ⑤

    Load 関数の型安全なデータ受け渡し

    +page.server.ts の load が return した値は +page.svelte の data プロパティに型推論付きで届く。手動の型定義が不要で、DB変更が即ページ側の型エラーに現れる。

  6. 6

    エッジ⑥

    SSR / CSR / 静的レンダリングが1行で切替

    +page.ts で `export const ssr = false / csr = false / prerender = true` と書くだけでページごとに最適な方式を選べる。料金に直結する判断をコード1行で行える。

  7. 7

    エッジ⑦

    data-sveltekit-preload-data でリンク先を先読み

    属性ひとつでホバー・タップ直前にデータ取得を開始する。体感速度が劇的に速くなり、PWAに近い挙動になる。競合フレームワークでは実装コストが高い機能が標準装備。

  8. 8

    エッジ⑧

    $env/static/private で秘密情報を型レベル分離

    private を client で import するとビルドエラーになる。PUBLIC_ プレフィックスが無い変数はクライアントに出ない。API キー漏えいを型レベルで防ぐのがプロの設計。

  9. 9

    エッジ⑨

    Adapter を差し替えるだけでデプロイ先変更

    svelte.config.js の adapter を差し替えると Cloudflare / Vercel / Node / Static を切り替えられる。ベンダーロックインが少ない=将来の移行コストが低い。

  10. 10

    エッジ⑩

    +error.svelte で宣言的エラー境界

    load関数や Form Actions から throw error(404, ...) すると、ルート階層で最も近い +error.svelte が自動的にレンダリングされる。try/catch を散らす必要がない。

🧪 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選

  1. 1

    罠①

    +page.ts と +page.server.ts を混同する

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

  2. 2

    罠②

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

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

  3. 3

    罠③

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

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

  4. 4

    罠④

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

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

  5. 5

    罠⑤

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

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

  6. 6

    罠⑥

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

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

  7. 7

    罠⑦

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

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

  8. 8

    罠⑧

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

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

  9. 9

    罠⑨

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

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

  10. 10

    罠⑩

    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