+page.server.tsと+page.tsの使い分け

SvelteKitでページにデータを渡す方法は、+page.server.ts+page.tsの2種類があります。どちらもload関数をエクスポートする点は同じですが、実行される場所とできることが異なります。この違いを曖昧なまま実装すると、後になって「サーバー専用のAPIキーがクライアントバンドルに混ざっていた」といった事故につながりかねません。Alcogyでは新しいページを作るたびに、この2つのどちらを使うかを最初に判断するようにしています。

実行環境の違い

+page.server.tsはサーバーでのみ実行されます。データベースアクセス、環境変数、シークレットキーの参照などはすべてここに書きます。一方+page.ts(ユニバーサルload)は、SSR時はサーバーで、クライアントサイドナビゲーション時はブラウザで実行されます。つまり+page.tsに書いたコードはクライアントバンドルに含まれる可能性があるということです。

// +page.server.ts: サーバーでのみ実行される
import type { PageServerLoad } from './$types';
import { db } from '$lib/server/db';

export const load: PageServerLoad = async ({ params }) => {
	const record = await db.query.articles.findFirst({
		where: (a, { eq }) => eq(a.slug, params.slug),
	});
	return { record };
};
// +page.ts: サーバー・クライアント両方で実行される可能性がある
import type { PageLoad } from './$types';

export const load: PageLoad = async ({ fetch, params }) => {
	const res = await fetch(`/api/articles/${params.slug}`);
	return { article: await res.json() };
};

Alcogyでの判断基準

判断基準はシンプルで、「サーバー専用のリソースに触れるかどうか」です。D1やDrizzle ORMを直接叩く場合、外部APIを秘密鍵付きで呼ぶ場合は迷わず+page.server.tsを選びます。逆に、公開APIのfetchだけで完結し、クライアント側の再取得コストを抑えたいケースでは+page.tsを検討します。ただ、業務システムのOEM開発では扱うデータのほとんどが顧客企業の内部情報なので、実務上は+page.server.tsを選ぶ場面が圧倒的に多いというのが正直なところです。

もう一つの判断材料は、静的生成(プレレンダリング)との相性です。このテックブログ自体も+page.server.tsentries()を実装し、記事一覧のslugをビルド時に列挙してプレレンダリングしています。

export const entries = () => getSlugs().map((slug) => ({ slug }));

export const load: PageServerLoad = async ({ params }) => {
	const article = await getArticle(params.slug);
	if (!article) error(404, '記事が見つかりません');
	return { article };
};

entries()+page.server.ts+page.tsのどちらでも定義できますが、記事データがサーバー側のファイルシステムやビルド時専用のロジックに依存している場合は+page.server.tsにまとめた方が見通しが良くなります。データ取得ロジックとプレレンダリング設定を同じファイルに置いておくことで、「どのデータがどう生成されているか」を1箇所で追えるようにしています。

両方を組み合わせるケース

まれに、サーバーで取得した機密性の低いデータと、クライアント側の追加取得を両方使いたいケースがあります。この場合+page.server.tsloadが返した値は、同階層の+page.tsloaddataとして渡されます。Alcogyでは、初期表示に必要な最小限のデータは+page.server.tsで返し、ユーザー操作に応じた追加データ取得はコンポーネント内のfetchに任せる、という切り分けを基本にしています。load関数を肥大化させないことが、結果的にページの責務を明確に保つコツだと考えています。