
SvelteKit のサーバーとクライアントの境界を理解する
.server.ts の意味・SSR の仕組み・エラーページ・認証ガードの実装
SvelteKit の「コードがどこで動くか」を体系的に解説。.server.ts の命名規則の意味・SSR と CSR の違い・$app/environment による環境判定・+error.svelte・redirect() を使った認証ガードまでを網羅します。
シリーズ:SvelteKit サーバー・クライアント動作入門
01
サーバーとクライアントの境界
02
サーバーとクライアントの境界
03
サーバーとクライアントの境界
第2回まででルーティング・データ取得の仕組みを実装しました。商品一覧・商品詳細・APIエンドポイントは動く状態になっています。ただ、+page.server.ts やsrc/lib/server/ といった「サーバー専用ファイル」を使いながら、その理由をまだ深く掘り下げていませんでした。
最終回の今回は 「なぜそこに書くのか」 の根拠となる仕組みを理解します。SSR・CSR・SPAの違い、.server という命名規則の意味、$app/environment による実行環境の判定、そして+error.svelte によるエラーページ対応と、マイページの実装でシリーズを締めくくります。
💡 前提条件
第1回・第2回で作成したsvelte-shop/ プロジェクトを使います。npm run dev で開発サーバーが起動できる状態で進めてください。

SvelteKit のデータ取得を理解する +page.server.ts・+page.ts・+server.ts の使い分けと load 関数の仕組み
SSR・CSR・SPA ― 3つのレンダリングモード
「このコードはサーバーで動く?ブラウザで動く?」という疑問に答えるには、まずSvelteKitが持つ3つのレンダリングモードを理解する必要があります。
SSR
- ・Server-Side Rendering
- ✔ HTMLをサーバーで生成してから送る
- ✔ 初回表示が速い・SEOに有利
- ✔ SvelteKitのデフォルト
- ・ページ遷移後はCSRに切り替わる
CSR
- ・Client-Side Rendering
- ✔ ページ遷移がブラウザ内で完結
- ✔ 遷移がスムーズで高速
- ✘ 初回はJSのロードが必要
- ・SSR後のナビゲーションはCSR
SPA
- ・Single Page Application
- ✔ 完全クライアント動作も可能
- ✘ SSRなし・SEOに不利
- ✘ 初回ロードが重くなりがち
- ・設定で切り替え可能
SvelteKitは デフォルトでSSR+CSRのハイブリッド で動きます。ユーザーが最初にURLを打ち込んだとき(または再読み込みしたとき)はSSRでHTMLを生成し、以降のページ遷移はブラウザ上のCSRで動きます。
リクエストの流れ:初回アクセス vs ページ遷移
SSR+CSRのハイブリッドで、具体的に何がどこで動くかを追ってみましょう。
1
ブラウザが /products にアクセス(初回・再読み込み)
HTTPリクエストがSvelteKitサーバーに届く。
ブラウザ
2
+layout.server.ts と +page.server.ts の load が実行される
DBアクセスや認証チェックなどサーバー側の処理が走る。
サーバー
3
+page.svelte がサーバー上でレンダリングされHTMLが生成される
データ取得済みの状態でHTMLが作られるため、初回表示が速い。
サーバー
4
HTMLとJavaScriptがブラウザに届き、ハイドレーションが起きる
サーバーで作ったHTMLにJavaScriptを「接続」して、インタラクティブにする処理。
ブラウザ
5
以降のページ遷移(リンクをクリック)はCSRで処理される
サーバーへのリクエストは最小限に抑えられ、ブラウザ上でページが切り替わる。このとき +page.server.ts の load はサーバーで実行されるが、結果だけがJSONとして送られる 。
ブラウザ主体
✨ ハイドレーションとは
サーバーが生成した静的なHTMLに、JavaScriptのイベントリスナーやリアクティビティを「あとから接続する」処理のことです。初回表示は速いけれど、JavaScriptが読み込まれるまでボタンがクリックできない状態が短時間発生します。SvelteKitはこのハイドレーションを自動で行います。
.server という命名規則の意味
第1回・第2回で登場した+page.server.ts やsrc/lib/server/ のserver という名前は、単なる慣習ではありません。 SvelteKitがそのファイルをブラウザのバンドルに含めないことを保証する仕組み です。
SERVER — ブラウザには絶対に送られない
+page.server.ts
+layout.server.ts
+server.ts
src/lib/server/*.ts
*.server.ts
DBのパスワード・APIシークレット・個人情報 の処理はここに書く
SHARED — サーバーとブラウザの両方で動く
+page.svelte
+layout.svelte
+page.svelte
+layout.ts
src/lib/*.ts(serverなし)
秘密情報をここに書いてはいけない。ブラウザに送られる可能性がある
BROWSER — ブラウザでのみ動く
*.client.ts
src/lib/client/*.ts
window・document など、サーバー上に存在しないAPIを使うコード
重要なのは真ん中の「SHARED」ゾーンです。+page.ts やsrc/lib/ (serverなし)に書いたコードは ブラウザに送られます 。ここにDB接続情報を書いてしまうと、ブラウザの開発者ツールで誰でも見られる状態になります。
⚠️ ️ やってはいけない例
以下のコードを+page.ts (.server なし)に書くと、DBのパスワードがブラウザに送られてしまいます。
// ❌ これは危険!ブラウザに送られてしまう
import { createConnection } from 'mysql2';
const db = createConnection({ password: 'super-secret-password' });
// ✅ DBアクセスは必ず +page.server.ts に書く
// → src/routes/.../+page.server.ts で import { getProducts } from '$lib/server/db' SvelteKitがどうやって境界を守るか
SvelteKitはビルド時に ファイル名のパターンを見て 、ブラウザバンドルに含めるかどうかを判断します。
| パターン | ブラウザに送られるか | 理由 |
|---|---|---|
+page.server.ts | 送られない | ファイル名に.server が含まれる |
src/lib/server/db.ts | 送られない | パスに/server/ が含まれる |
utils.server.ts | 送られない | ファイル名に.server が含まれる |
+page.ts | 送られる可能性がある | shared(サーバー+ブラウザ両方で動く) |
src/lib/utils.ts | 送られる可能性がある | shared(server がパスに含まれない) |
$app/environment ― 実行環境を判定する
🔄 サーバー・ブラウザ両方で使用可能
共有ファイル(+page.svelte など)の中で、「今サーバーで動いているのかブラウザで動いているのか」を判定したい場面があります。SvelteKitが提供する$app/environment モジュールで取得できます。
import { browser, server, dev, building } from '$app/environment';
// browser: ブラウザで動いているとき true
if (browser) {
// window や document などブラウザ専用APIを安全に使える
console.log('ブラウザで動いています');
localStorage.setItem('visited', 'true');
}
// dev: 開発サーバー起動中のとき true(本番では false)
if (dev) {
console.log('開発モードです');
}| 変数 | true になる条件 | 主な用途 |
|---|---|---|
browser | ブラウザで動いているとき | localStorage ・window ・イベントリスナーの利用 |
server | サーバーで動いているとき | サーバー専用処理を shared コード内に書く場合 |
dev | 開発環境(npm run dev )のとき | デバッグログ・詳細エラー表示 |
building | ビルド中(npm run build )のとき | ビルド時の特別処理 |
❌ browser ガードのよくある使い方
+page.svelte の<script> 内でlocalStorage やwindow を直接使うと、SSR時(サーバー上)に存在しないためエラーになります。if (browser) { ... } で囲むか、onMount (ブラウザでのみ実行されるライフサイクル)の中で使うのが定番パターンです。
<script lang="ts">
import { browser } from '$app/environment';
import { onMount } from 'svelte';
let theme = 'light';
// onMount はブラウザでのみ実行される。SSR時は実行されない
onMount(() => {
theme = localStorage.getItem('theme') ?? 'light';
});
// browser ガードで明示的に判定することもできる
if (browser) {
console.log('ブラウザで動いています');
}
</script> +error.svelte ― エラーページをカスタマイズする
第2回でerror(404, ...) を使って存在しない商品IDへのアクセスを処理しました。しかし今の状態ではSvelteKitのデフォルトのエラー画面が表示されます。これを+error.svelte でカスタマイズしましょう。
+error.svelte の配置と適用範囲
第1回で全ファイルの一覧を紹介した際に触れた通り、+error.svelte はそのフォルダ以下でエラーが発生したときに表示されます。配置場所によって適用範囲が変わります。
src/routes/
├─ +error.svelte ← すべてのルートに適用(最終フォールバック)
└─ (shop)/
├─ +error.svelte ← (shop) 配下のルートだけに適用
└─ products/
└─ +error.svelte ← /products 配下だけに適用(より狭いスコープ)今回はルートレベルに1つ置き、サイト全体のエラーを拾います。
<script lang="ts">
import { page } from '$app/stores';
// $page.status にHTTPステータスコード、$page.error.message にエラーメッセージが入る
</script>
<div class="error-page">
<p class="status">{$page.status}</p>
{#if $page.status === 404}
<h1>ページが見つかりません</h1>
<p>お探しのページは存在しないか、移動した可能性があります。</p>
{:else}
<h1>エラーが発生しました</h1>
<p>{$page.error?.message}</p>
{/if}
<a href="/">トップページへ戻る</a>
</div>❌ 確認
http://localhost:5173/products/99999 にアクセスしてカスタムエラーページが表示されることを確認してください。ステータスコードに応じてメッセージが切り替わります。
マイページの実装 ― 認証ガードのパターン
🔒 サーバーのみで実行
最後にマイページを実装します。本格的な認証機構(JWT・Cookieセッションなど)はシリーズBのHooksで扱いますが、ここでは 「認証されていないユーザーをリダイレクトする」 パターンを+page.server.ts で実装します。これはSvelteKitの認証実装で最もよく使われるパターンです。
redirect() でログインページへ飛ばす
error() と同様に、redirect() も@sveltejs/kit からインポートして使います。呼び出すとその場で例外をスローし、指定のURLへリダイレクトされます。
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async (event) => {
// 本来はここで Cookie / セッション / JWT を検証する
// 今回はクエリパラメータで簡易的にログイン状態をシミュレート
// 例:/mypage?loggedIn=true でアクセス
const loggedIn = event.url.searchParams.get('loggedIn') === 'true';
// 未認証なら /login にリダイレクト(303: See Other が標準的なステータスコード)
if (!loggedIn) {
redirect(303, '/login');
}
// 認証済みの場合はユーザーデータを返す
// 実際はDBからユーザー情報を取得する
return {
user: {
name: '山田 太郎',
email: 'taro@example.com',
memberSince: '2024-01',
},
};
};<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
</script>
<h1>マイページ</h1>
<section class="user-info">
<dl>
<dt>お名前</dt>
<dd>{data.user.name}</dd>
<dt>メールアドレス</dt>
<dd>{data.user.email}</dd>
<dt>会員登録日</dt>
<dd>{data.user.memberSince}</dd>
</dl>
</section>
<a href="/products">商品一覧を見る</a>🎯 確認
http://localhost:5173/mypage にアクセスすると/login にリダイレクトされます(まだ /login ページは未実装のため404になりますが、リダイレクト自体は動作します)。http://localhost:5173/mypage?loggedIn=true でアクセスするとマイページが表示されます。
💡 本番での認証は Hooks で実装する
今回はクエリパラメータで認証を「シミュレート」しましたが、実際の認証ではevent.cookies でCookieを読み取り、セッションIDやJWTを検証します。さらに「すべてのページで同じ認証チェックを走らせたい」場合は、各ページの+page.server.ts に同じコードを書くのではなく、 次のシリーズBで扱うhooks.server.ts のhandle 関数 で横断的に処理するのが定番パターンです。
レンダリングモードを切り替える
SvelteKitはデフォルトでSSRが有効ですが、ページごとまたはアプリ全体でモードを切り替えられます。+page.ts または+layout.ts に特定の変数をエクスポートするだけです。
// SSRを無効にする(このページはCSRのみで動く)
export const ssr = false;
// クライアントサイドルーティングを無効にする(毎回フルページロード)
export const csr = false;
// プリレンダリングを有効にする(ビルド時にHTMLを生成・静的配信に向く)
export const prerender = true;
// trailingSlash の制御(URLの末尾スラッシュの扱い)
export const trailingSlash = 'always'; // 'never' | 'always' | 'ignore'| 設定 | デフォルト | 変更する場面 |
|---|---|---|
ssr | true | false にするのは管理画面など認証後のみ表示されSEO不要なページ |
csr | true | false にするとJavaScriptなしで動く静的なページを作れる |
prerender | false | true にするとビルド時にHTMLを生成。ブログ・ドキュメントなど内容が変わらないページ向け |
✨ 商品カタログアプリの場合
商品データがリアルタイムに変わる前提なら、デフォルト(SSR有効)のままが適切です。商品数が少なく更新頻度が低ければprerender = true で静的配信にして高速化も検討できます。マイページは認証が必要でSEO不要なのでssr = false でCSRのみにする選択肢もあります。
第3回の完成コード一覧
今回追加・変更したファイルをまとめます。
src/routes/
├─ +error.svelte NEW ← ルートレベルのエラーページ
└─ (shop)/
└─ mypage/
├─ +page.server.ts NEW ← 認証ガード・ユーザーデータ取得
└─ +page.svelte UPDATE
📄
src/routes/+error.svelte
サイト全体共通エラーページ
<script lang="ts">
import { page } from '$app/stores';
</script>
<div class="error-page">
<p class="status">{$page.status}</p>
{#if $page.status === 404}
<h1>ページが見つかりません</h1>
<p>お探しのページは存在しないか、移動した可能性があります。</p>
{:else}
<h1>エラーが発生しました</h1>
<p>{$page.error?.message}</p>
{/if}
<a href="/">トップページへ戻る</a>
</div>📄
src/routes/(shop)/mypage/+page.server.ts
認証ガード付きマイページデータ取得
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async (event) => {
// 本番では event.cookies.get('session') などで認証状態を確認する
const loggedIn = event.url.searchParams.get('loggedIn') === 'true';
if (!loggedIn) {
redirect(303, '/login');
}
return {
user: {
name: '山田 太郎',
email: 'taro@example.com',
memberSince: '2024-01',
},
};
};📄
src/routes/(shop)/mypage/+page.svelte
マイページ
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
</script>
<h1>マイページ</h1>
<section class="user-info">
<dl>
<dt>お名前</dt> <dd>{data.user.name}</dd>
<dt>メールアドレス</dt><dd>{data.user.email}</dd>
<dt>会員登録日</dt> <dd>{data.user.memberSince}</dd>
</dl>
</section>
<a href="/products">商品一覧を見る</a> 第3回・シリーズ全体のまとめ
今回学んだこと
- SvelteKitはデフォルトで SSR+CSRのハイブリッド 。初回アクセスはサーバーでHTMLを生成し、以降のページ遷移はブラウザで動く
.serverという命名規則はブラウザバンドルへの不含を SvelteKitが保証する仕組み 。DBパスワードや秘密鍵は.serverファイルにのみ書くsrc/lib/(server なし)のファイルはブラウザに送られる可能性がある。秘密情報はsrc/lib/server/に置く$app/environmentのbrowserフラグで実行環境を判定できる。localStorageなどブラウザ専用APIはif (browser)かonMountの中で使う+error.svelteでエラーページをカスタマイズ。$page.statusでステータスコード、$page.error.messageでエラーメッセージを取得できるredirect()で認証ガードを実装。本番ではHooksと組み合わせて全ページ横断の認証チェックにするssr/csr/prerenderエクスポートでレンダリングモードをページごとに制御できる
🎯 シリーズ全体のまとめ
3回を通じて、SvelteKitの「ファイル名と配置場所がすべてを決める」という設計の全体像を把握しました。第1回でファイル構造とルーティング、第2回でデータ取得の3パターン、第3回でサーバー/クライアントの境界と仕上げの実装を学んだことで、商品カタログアプリの骨格が完成しています。 次のシリーズBでは、今回「認証はここでは省略」としていた部分をhooks.server.ts とhooks.client.ts で実装します。Hooksを使うと、このシリーズで学んだリクエスト処理に 横断的に割り込む ことができます。
🎉
シリーズ完結、おめでとうございます!
SvelteKitのサーバー・クライアント動作の全体像を3回かけて体系的に学びました。
このシリーズで作った商品カタログは、次のシリーズで 認証・ログ・エラー処理 を加えてさらに本番に近い形に育てていきます。
✅ ファイルベースルーティング
✅ load 関数によるデータ取得
✅ サーバー/クライアントの境界
✅ .server.ts の安全な使い方
✅ エラーページのカスタマイズ
✅ 認証ガードの基本パターン
次のシリーズ(SvelteKit Hooks 完全ガイド)準備中
次は「 hooks.server.ts / hooks.client.ts / hooks.ts の使い方 」を解説予定です。