
SvelteKit のデータ取得を理解する
+page.server.ts・+page.ts・+server.ts の使い分けと load 関数の仕組み
SvelteKit でページにデータを渡す3つの方法を実践解説。+page.server.ts・+page.ts・+server.ts の違いと使いどころを、商品カタログアプリの load 関数実装を通じて体系的に学びます。
シリーズ:SvelteKit サーバー・クライアント動作入門
01
ルーティングとファイル構造
02
データ取得:load と API Route
03
サーバーとクライアントの境界
前回は商品カタログアプリのルート構造を作り、+page.svelte や+layout.svelte といったファイルの役割を把握しました。ただ、各ページにはまだ「中身のないプレースホルダー」しか置いていません。
今回はいよいよ ページにデータを渡す仕組み を実装します。SvelteKitにはデータ取得の方法が3種類あり、それぞれ動く場所と使いどころが異なります。3つの違いをしっかり理解することが、この回の最大のゴールです。
💡 前提条件
第1回「 」で作成したsvelte-shop/ プロジェクトを使います。src/routes/ 以下に第1回のファイル構造が揃っている前提で進めます。

SvelteKit のルーティングとファイル構造を理解する +page.svelte から +server.ts まで全ファイルの役割を一気に把握する
データ取得の3種類を俯瞰する
SvelteKitでページにデータを渡す方法は以下の3つです。今回はこの3つをすべて実際に実装しながら理解していきます。
| ファイル | 動く場所 | 主な用途 | ページへの渡し方 |
|---|---|---|---|
+page.server.ts | サーバーのみ | DBアクセス・秘密鍵・認証が必要なデータ取得 | export let data |
+layout.server.ts | サーバーのみ | レイアウト配下の全ページに渡す共通データ | export let data |
+page.ts | 両方(universal) | 公開APIへのfetchなど秘密情報不要なデータ取得 | export let data |
+server.ts | サーバーのみ | REST APIエンドポイント(ページを持たない) | HTTPレスポンスとして返す |
今回は+page.server.ts を主軸 に商品カタログアプリを実装し、+layout.server.ts と+server.ts も順番に追加します。+page.ts (universal load)については後半に使い分けの観点から解説します。
データが渡る流れ
まずデータがどう流れるかを図で把握しておきましょう。load 関数が返した値は、自動的に+page.svelte のdata プロパティとして受け取れます。
DB / 外部API
(データソース)
→
+page.server.ts
load 関数
→
{ return { ... } }
戻り値のオブジェクト
→
+page.svelte
data プロパティ
✨ load 関数とは
load 関数は+page.server.ts などのファイルにexport function load() という名前でエクスポートする関数です。SvelteKitがページのリクエスト時に自動で呼び出し、その戻り値が+page.svelte のdata に渡されます。開発者がこの関数を手動で呼ぶ必要はありません。
下準備:型定義とモックデータを用意する
実際のDBの代わりに、モックデータを使ってデータ取得の仕組みを学びます。まず商品の型と、仮のデータを返す関数を用意します。
💡 今回追加するファイル
src/lib/ はSvelteKitが特別扱いするフォルダで、$lib というエイリアスでどこからでもインポートできます。共通のデータ・ユーティリティはここに置くのが定番です。
// 商品の型定義
export type Product = {
id: number;
name: string;
price: number;
category: string;
description: string;
inStock: boolean;
};
// カテゴリの型定義
export type Category = {
id: string;
name: string;
};// .server. が付いているのでブラウザには送られない(第3回で詳解)
import type { Product, Category } from '$lib/types';
// 本番ではここにDBクエリを書く。今はモックデータで代用
const products: Product[] = [
{ id: 1, name: 'ワイヤレスイヤホン', price: 12800, category: 'electronics', description: 'ノイズキャンセリング搭載の高音質イヤホン', inStock: true },
{ id: 2, name: 'メカニカルキーボード', price: 18500, category: 'electronics', description: '打鍵感にこだわったゲーミングキーボード', inStock: true },
{ id: 3, name: 'コットンTシャツ', price: 3200, category: 'fashion', description: 'オーガニックコットン100%のシンプルTシャツ', inStock: true },
{ id: 4, name: 'デニムジャケット', price: 15600, category: 'fashion', description: 'ヴィンテージウォッシュ加工のデニムジャケット', inStock: false },
{ id: 5, name: '水筒(500ml)', price: 4800, category: 'kitchen', description: '真空断熱で24時間保温・保冷', inStock: true },
];
const categories: Category[] = [
{ id: 'electronics', name: '家電・ガジェット' },
{ id: 'fashion', name: 'ファッション' },
{ id: 'kitchen', name: 'キッチン' },
];
// 全商品を返す
export function getProducts(): Product[] {
return products;
}
// IDで1件取得。見つからなければ null を返す
export function getProduct(id: number): Product | null {
return products.find((p) => p.id === id) ?? null;
}
// 全カテゴリを返す
export function getCategories(): Category[] {
return categories;
}✨ src/lib/server/ について
src/lib/server/ に置いたファイルは、SvelteKitによってブラウザ向けのバンドルに含められません。DBの接続情報や秘密鍵など、絶対にブラウザに送ってはいけないコードはここに置きます。詳しくは第3回で解説します。
+layout.server.ts ― 共通データをレイアウト全体に渡す
🔒 サーバーのみで実行
カテゴリ一覧はサイドバー((shop)/+layout.svelte )で表示します。商品一覧・商品詳細・マイページ、すべての画面で使うデータです。このように 複数ページで共通して必要なデータ は、+layout.server.ts のload で一度だけ取得するのが効率的です。
💡 layout の load と page の load の関係
+layout.server.ts のload が返したデータは、そのレイアウト配下の すべてのページでdata からアクセスできます 。+page.server.ts のload が返したデータとマージされる形になります。
import { getCategories } from '$lib/server/db';
import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = async () => {
const categories = getCategories();
// ここで返したデータはレイアウト配下の全ページで data.categories として使える
return { categories };
}; 次に+layout.svelte でこのデータを受け取り、サイドバーに表示します。
<script lang="ts">
import type { LayoutData } from './$types';
// export let data で load の戻り値を受け取る(SvelteKit のルール)
export let data: LayoutData;
</script>
<div class="shop-layout">
<aside>
<p class="nav-title">カテゴリ</p>
<ul>
<li><a href="/products">すべての商品</a></li>
<!-- +layout.server.ts が返した categories をループ表示 -->
{#each data.categories as category}
<li><a href="/products?category={category.id}">{category.name}</a></li>
{/each}
</ul>
</aside>
<section>
<slot />
</section>
</div> +page.server.ts ― ページのデータをサーバーで取得する
🔒 サーバーのみで実行
+page.server.ts のload 関数は、そのページが表示されるたびにサーバー側で実行されます。DBアクセスや秘密鍵を使う処理を安全に書けます。まず商品一覧ページから実装しましょう。
商品一覧ページ(/products)
import { getProducts } from '$lib/server/db';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async (event) => {
// URLのクエリパラメータを取得(例:?category=electronics)
const category = event.url.searchParams.get('category');
let products = getProducts();
// カテゴリが指定されていればフィルタリング
if (category) {
products = products.filter((p) => p.category === category);
}
// 戻り値のオブジェクトが +page.svelte の data プロパティになる
return { products, selectedCategory: category };
}; 続いて+page.svelte でデータを受け取って表示します。
<script lang="ts">
import type { PageData } from './$types';
// load 関数の戻り値 + layout の load の戻り値がマージされて渡ってくる
export let data: PageData;
</script>
<h1>
{data.selectedCategory ? `${data.selectedCategory} の商品` : 'すべての商品'}
</h1>
<ul class="product-grid">
{#each data.products as product}
<li>
<a href="/products/{product.id}">
<p class="name">{product.name}</p>
<p class="price">¥{product.price.toLocaleString()}</p>
{#if !product.inStock}
class="sold-out">売り切れ
{/if}
</a>
</li>
{/each}
</ul>🎯 ここで確認できること
http://localhost:5173/products にアクセスして商品一覧が表示されることを確認してください。?category=electronics をURLに付けると家電のみに絞られます。このフィルタリングはサーバー側で行われているため、ブラウザからDBのコードは一切見えません。
商品詳細ページ(/products/[id])― params でURLパラメータを受け取る
動的ルートの場合、load 関数の引数event.params から URL の[id] 部分を取得できます。また、商品が存在しない場合はerror() を使って404を返します。
import { getProduct } from '$lib/server/db';
import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async (event) => {
// event.params.id で URL の [id] 部分を文字列として取得
const id = Number(event.params.id);
// NaN チェック(/products/abc のような不正なアクセス対策)
if (isNaN(id)) {
error(404, '商品が見つかりません');
}
const product = getProduct(id);
// 商品が存在しなければ 404 を返す(error() は例外をスローする)
if (!product) {
error(404, `ID:${id} の商品は存在しません`);
}
return { product };
};<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
</script>
<a href="/products">← 一覧に戻る</a>
<article>
<h1>{data.product.name}</h1>
<p class="price">¥{data.product.price.toLocaleString()}</p>
<p class="description">{data.product.description}</p>
{#if data.product.inStock}
<button>カートに追加</button>
{:else}
<p class="sold-out">現在売り切れ中です</p>
{/if}
</article>✨ error() は例外をスローする
error() は@sveltejs/kit からインポートする関数で、呼び出すと例外をスローしてその場で処理が止まります。return する必要はなく、if (!product) error(404, ...) とすればそれ以降はproduct が確実に存在することをTypeScriptが認識します。
+page.ts ― universal load との違いと使いどころ
🔄 サーバーとブラウザ両方で実行
今回のサンプルアプリは+page.server.ts を主軸に実装しますが、 universal load(+page.ts ) も重要な概念なので使い分けをしっかり理解しましょう。
| +page.server.ts | +page.ts(universal) | |
|---|---|---|
| 実行場所 | サーバーのみ | 初回はサーバー・以降はブラウザ |
| DBアクセス | ✓ 可能 | ✗ 不可(ブラウザで動くため) |
| 秘密鍵・環境変数 | ✓ 安全 | ✗ ブラウザに露出するリスク |
| fetch の動作 | サーバー上のfetch | 環境に応じたfetch(自動切替) |
| Svelteコンポーネントの返却 | ✗ 不可 | ✓ 可能 |
| 主な使い所 | DBアクセス・認証・秘密情報を含む処理 | 公開APIへのfetch・SEO不要なデータ取得 |
重要なのは 「初回リクエストはサーバーで動くが、ページ遷移(クライアントナビゲーション)はブラウザで動く」 という点です。そのため+page.ts にはDBアクセスのコードを書いてはいけません。ブラウザで動いたときにDBドライバが存在せずエラーになります。
Q. データ取得に DB・秘密鍵・認証情報が必要か?
Yes →
+page.server.ts
サーバーでしか動かない。安全。
No →
+page.server.ts
公開APIのfetchなど秘密情報が不要な場合
今回の商品カタログはDBアクセスが必要なので+page.server.ts で統一します。+page.ts が活躍するのは、たとえば「公開されている天気APIからデータを取得してページに渡す」といったケースです。
+server.ts ― REST API エンドポイントを作る
🌐 サーバーのみで実行(APIエンドポイント)
+server.ts は ページUIを持たない、純粋なAPIエンドポイント です。GET /POST /PUT /DELETE に対応した関数をエクスポートすることで、そのHTTPメソッドのリクエストを処理できます。
商品カタログアプリでは/api/products にGETエンドポイントを追加します。外部サービスや将来のモバイルアプリからの利用を想定したAPIです。
import { getProducts } from '$lib/server/db';
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
// GET /api/products に対応するハンドラ
export const GET: RequestHandler = async (event) => {
const category = event.url.searchParams.get('category');
let products = getProducts();
if (category) {
products = products.filter((p) => p.category === category);
}
// json() ヘルパーで Content-Type: application/json のレスポンスを返す
return json(products);
};動作確認してみましょう。
# 全商品を取得
$ curl http://localhost:5173/api/products
[{"id":1,"name":"ワイヤレスイヤホン","price":12800,...}, ...]
# カテゴリで絞り込み
$ curl "http://localhost:5173/api/products?category=electronics"
[{"id":1,"name":"ワイヤレスイヤホン",...}, {"id":2,"name":"メカニカルキーボード",...}]🎯 +page.server.ts と +server.ts の使い分け
どちらもサーバーで動きますが、 目的が違います 。+page.server.ts は「+page.svelte にデータを渡すため」のもので、ブラウザからHTTPで直接叩くことを意図していません。+server.ts は「外部からHTTPで叩かれることを意図したAPIエンドポイント」です。内部でページにデータを渡したい場合は+page.server.ts 、JSON APIを公開したい場合は+server.ts と使い分けてください。
トラブルシュート
❓ data.products にアクセスしようとすると TypeScript のエラーが出る
./$types からインポートするPageData /PageServerLoad などの型は、開発サーバー起動中(npm run dev )に自動生成されます。型エラーが出る場合はいったん開発サーバーを再起動してみてください。それでも解消しない場合はnpm run check を実行して型エラーの詳細を確認します。
❓ +page.server.ts を作ったのにデータが渡らない
export const load = ... のようにexport キーワードを忘れている ケースが最多です。また+page.svelte 側でexport let data の記述が抜けている場合も同様の症状になります。両方確認してください。
❓ $lib/server/db をインポートしたら「ブラウザで動かせない」エラーが出た
src/lib/server/ 以下のファイルをブラウザで動くファイル(+page.ts や+layout.ts )からインポートするとこのエラーが出ます。src/lib/server/ のファイルは.server.ts のファイルからのみインポートしてください。
第2回の完成コード一覧
今回追加・変更したファイルをすべてまとめます。コピーして使う場合はこのセクションを参照してください。
src/
├─ lib/
│ ├─ types.ts NEW
│ └─ server/
│ └─ db.ts NEW
└─ routes/
├─ (shop)/
├─ +layout.server.ts NEW
├─ +layout.svelte UPDATE
│ ├─ products/
│ │ ├─ +page.server.ts NEW
│ │ ├─ +page.svelte UPDATE
│ │ └─ [id]/
│ │ ├─ +page.server.ts NEW
│ │ └─ +page.svelte UPDATE
│ └─ mypage/
│ └─ +page.svelte (第3回で実装)
└─ api/products/
└─ +server.ts NEW
📄
src/lib/types.ts
型定義
export type Product = {
id: number;
name: string;
price: number;
category: string;
description: string;
inStock: boolean;
};
export type Category = {
id: string;
name: string;
};📄
src/lib/server/db.ts
モックデータ・データアクセス関数
import type { Product, Category } from '$lib/types';
const products: Product[] = [
{ id: 1, name: 'ワイヤレスイヤホン', price: 12800, category: 'electronics', description: 'ノイズキャンセリング搭載の高音質イヤホン', inStock: true },
{ id: 2, name: 'メカニカルキーボード', price: 18500, category: 'electronics', description: '打鍵感にこだわったゲーミングキーボード', inStock: true },
{ id: 3, name: 'コットンTシャツ', price: 3200, category: 'fashion', description: 'オーガニックコットン100%のシンプルTシャツ', inStock: true },
{ id: 4, name: 'デニムジャケット', price: 15600, category: 'fashion', description: 'ヴィンテージウォッシュ加工のデニムジャケット', inStock: false },
{ id: 5, name: '水筒(500ml)', price: 4800, category: 'kitchen', description: '真空断熱で24時間保温・保冷', inStock: true },
];
const categories: Category[] = [
{ id: 'electronics', name: '家電・ガジェット' },
{ id: 'fashion', name: 'ファッション' },
{ id: 'kitchen', name: 'キッチン' },
];
export function getProducts(): Product[] { return products; }
export function getProduct(id: number): Product | null { return products.find((p) => p.id === id) ?? null; }
export function getCategories(): Category[] { return categories; }📄
src/routes/(shop)/+layout.server.ts
ショップ共通データ(カテゴリ一覧)
import { getCategories } from '$lib/server/db';
import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = async () => {
return { categories: getCategories() };
};📄
src/routes/(shop)/+layout.svelte
カテゴリサイドバー付きレイアウト
<script lang="ts">
import type { LayoutData } from './$types';
export let data: LayoutData;
</script>
<div class="shop-layout">
<aside>
<p class="nav-title">カテゴリ</p>
<ul>
<li><a href="/products">すべての商品</a></li>
{#each data.categories as category}
<li><a href="/products?category={category.id}">{category.name}</a></li>
{/each}
</ul>
</aside>
<section><slot /></section>
</div>📄
src/routes/(shop)/products/+page.server.ts
商品一覧データ取得
import { getProducts } from '$lib/server/db';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async (event) => {
const category = event.url.searchParams.get('category');
let products = getProducts();
if (category) products = products.filter((p) => p.category === category);
return { products, selectedCategory: category };
};📄
src/routes/(shop)/products/+page.svelte
商品一覧ページ
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
</script>
<h1>{data.selectedCategory ? `${data.selectedCategory} の商品` : 'すべての商品'}</h1>
<ul class="product-grid">
{#each data.products as product}
<li>
<a href="/products/{product.id}">
<p class="name">{product.name}</p>
<p class="price">¥{product.price.toLocaleString()}</p>
{#if !product.inStock}<span class="sold-out">売り切れ</span>{/if}
</a>
</li>
{/each}
</ul>📄
src/routes/(shop)/products/[id]/+page.server.ts
商品詳細データ取得(404ハンドリング付き)
import { getProduct } from '$lib/server/db';
import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async (event) => {
const id = Number(event.params.id);
if (isNaN(id)) error(404, '商品が見つかりません');
const product = getProduct(id);
if (!product) error(404, `ID:${id} の商品は存在しません`);
return { product };
};📄
src/routes/(shop)/products/[id]/+page.svelte
商品詳細ページ
<script lang="ts">
import type { PageData } from './$types';
export let data: PageData;
</script>
<a href="/products">← 一覧に戻る</a>
<article>
<h1>{data.product.name}</h1>
<p class="price">¥{data.product.price.toLocaleString()}</p>
<p>{data.product.description}</p>
{#if data.product.inStock}
<button>カートに追加</button>
{:else}
<p class="sold-out">現在売り切れ中です</p>
{/if}
</article>📄
src/routes/api/products/+server.ts
商品一覧 REST API エンドポイント
import { getProducts } from '$lib/server/db';
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async (event) => {
const category = event.url.searchParams.get('category');
let products = getProducts();
if (category) products = products.filter((p) => p.category === category);
return json(products);
}; 第2回のまとめ
今回学んだこと
+page.server.tsのload関数がサーバーで実行され、戻り値が+page.svelteのexport let dataに渡される仕組みを理解した+layout.server.tsのloadは配下の全ページで共通して使うデータ(カテゴリ一覧など)の取得に使う- 動的ルート
[id]のパラメータはevent.params.idで取得できる。error()で404などのエラーレスポンスを返せる +page.ts(universal load)はサーバー・ブラウザ両方で動くため、DBアクセスや秘密鍵の使用は不可。公開APIへのfetchなどに使う+server.tsは純粋なREST APIエンドポイント。GETなどの関数をエクスポートしてHTTPリクエストを処理するsrc/lib/server/に置いたファイルはブラウザに送られない。DBアクセスコードはここに置くのが安全
🎯 第2回のまとめ
商品一覧・商品詳細・APIエンドポイントの実装を通じて、SvelteKitのデータ取得の仕組みを実践的に理解しました。src/lib/server/ というフォルダの存在が示すように、SvelteKitはファイルの配置でサーバー/クライアントの境界を制御しています。次回はこの 「サーバーとクライアントの境界」 をより深く掘り下げ、SSR・CSR・SPAの違いや$app/environment を使った実行環境の判定、そしてマイページの認証ガードを実装します。
