プロエンジニアの思考模写
SvelteKitで作って公開するまで
素人は npm create から機能を付け足す。プロは仕様→データ→ルーティング→UI→テスト→デプロイまで頭の中で組み立ててからコードを書く。その思考過程と SvelteKit 独自のエッジを完全模写する。
目次
- 今日のゴール(URLを人に渡せるアプリ公開)
- 素人 vs プロの思考差
- 作るものを決める(仕様とスコープ)
- データモデル設計(DBとSvelteKitのどこに置くか)
- ルーティング設計(File-based routing の本質)
- コンポーネント設計(+page / +layout / +server の使い分け)
- Runes と状態管理
- Form Actions と Progressive Enhancement
- SvelteKit エッジ集(独自機能10+のキーポイント)
- テスト戦略(vitest + playwright 最小構成)
- Cloudflare Pages デプロイ(adapter + wrangler)
- 素人が陥る罠10選
- 用語集
- 再現チェックリスト
- まとめ
🎯 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評価)
- • ソーシャル共有
プロの自問チェック(仕様決定前に必ず通す)
- 「このアプリが存在しない世界で、ユーザーは今何をしているか」— 代替手段を把握する
- 「最低限これだけあれば使ってもらえるか」— MVP(最小動作プロダクト)の境界を引く
- 「このデータは将来的にどう拡張するか」— スキーマを壊さない設計の余地を残す
- 「認証が要るか」— 要らないなら最初から排除。要るなら全設計に織り込む
- 「デプロイ先の制約は何か」— 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 連携で自動デプロイ
bun run build → Build output: .svelte-kit/cloudflareCloudflare 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: 用語集
| 用語 | 意味 |
|---|---|
| SSR | Server-Side Rendering。リクエストを受けたサーバーが HTML を生成してブラウザに返す。SEO と初回表示速度に有利。 |
| CSR | Client-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 なしでも動く。 |
| Runes | Svelte 5 で導入されたリアクティビティの宣言構文($state, $derived, $effect, $props)。コンパイラが処理するため実行時オーバーヘッドがほぼゼロ。 |
| Adapter | SvelteKit のビルド出力をデプロイ先の形式に変換するプラグイン。adapter-cloudflare, adapter-node, adapter-vercel 等がある。 |
| Progressive Enhancement | JS が無効でも基本機能が動き、JS が有効なときに体験が向上する設計原則。フォームが代表例。 |
| drizzle-orm | TypeScript 向け軽量 ORM。SQLite / Turso / PostgreSQL に対応。型安全なクエリが書けて、マイグレーション管理も可能。 |
| MVP | Minimum 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 エッジ一覧