サイトロゴ
SvelteKit Created: 2026/05/26 Updated: 2026/05/27

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 のルーティングとファイル構造を理解する \n +page.svelte から +server.ts まで全ファイルの役割を一気に把握する
SvelteKit

SvelteKit のルーティングとファイル構造を理解する +page.svelte から +server.ts まで全ファイルの役割を一気に把握する

SvelteKit のファイルベースルーティングを基礎から解説。+page.svelte・+layout.svelte・+page.server.ts・+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 というエイリアスでどこからでもインポートできます。共通のデータ・ユーティリティはここに置くのが定番です。

ts
// 商品の型定義
export type Product = {
  id: number;
  name: string;
  price: number;
  category: string;
  description: string;
  inStock: boolean;
};

// カテゴリの型定義
export type Category = {
  id: string;
  name: string;
};
ts
// .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 が返したデータとマージされる形になります。

ts
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 でこのデータを受け取り、サイドバーに表示します。

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)

ts
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 でデータを受け取って表示します。

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を返します。

ts
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 };
};
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 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です。

ts
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);
};

動作確認してみましょう。

bash
# 全商品を取得
$ 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回の完成コード一覧

今回追加・変更したファイルをすべてまとめます。コピーして使う場合はこのセクションを参照してください。

text
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

型定義

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

モックデータ・データアクセス関数

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

ショップ共通データ(カテゴリ一覧)

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

カテゴリサイドバー付きレイアウト

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

商品一覧データ取得

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

商品一覧ページ

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ハンドリング付き)

ts
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

商品詳細ページ

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 エンドポイント

ts
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 を使った実行環境の判定、そしてマイページの認証ガードを実装します。