さとまたwiki

Hooks(フック)

リクエストの処理をカスタマイズする

Hooksとは?

日常での例え:建物の入口にいる警備員

警備員の仕事

・入館者の身分証チェック
・入館記録をつける
・不審者は入れない

Hooksの仕事

・ユーザーの認証チェック
・リクエストのログを取る
・未認証ユーザーをリダイレクト

なぜHooksが必要?

Hooksがないと困ること:

  • ログイン確認を全ページに書く必要がある(同じコードを何十回も...)
  • 「誰がいつアクセスしたか」のログを取る場所がない
  • エラーが起きても原因が分からない

Hooksがあれば:1箇所に書くだけで、全ページに適用される!

Hooksでできること

  • handle:すべてのリクエストを傍受・加工
    → 例:ログインチェック、アクセスログ記録
  • handleFetch:サーバーでのfetch(データ取得)をカスタマイズ
    → 例:外部APIへの認証ヘッダー自動追加
  • handleError:エラーを捕まえて処理
    → 例:エラーをSlackに通知、ユーザーには優しいメッセージ

ファイルの場所

src/hooks.server.ts

このファイルにhook関数を定義します

用語解説

リクエスト:ブラウザからサーバーへの「このページください」という要求

レスポンス:サーバーからブラウザへの「はい、どうぞ」という返答

Cookie(クッキー):ブラウザに保存される小さなデータ。「ログイン中」などの情報を覚えておく

セッション:ユーザーがサイトを訪れている間の「会話」のようなもの。誰がログイン中かを識別

リダイレクト:別のページに自動で移動させること(例:未ログイン→ログインページへ)

handle フック

基本的なhandle

すべてのリクエストを処理

typescript
// src/hooks.server.ts
import type { Handle } from '@sveltejs/kit';

export const handle: Handle = async ({ event, resolve }) => {
  // リクエスト処理の前に実行
  console.log('リクエスト:', event.url.pathname);

  // デフォルトの処理を実行
  const response = await resolve(event);

  // レスポンスを返す前に実行
  console.log('レスポンス:', response.status);

  return response;
};
プレビュー
// リクエスト → handle → ページ処理 → handle → レスポンス
// すべてのリクエストがここを通る

認証チェック

ログインしていないユーザーをリダイレクト

typescript
// src/hooks.server.ts
import { redirect } from '@sveltejs/kit';
import type { Handle } from '@sveltejs/kit';

export const handle: Handle = async ({ event, resolve }) => {
  // Cookieからセッションを取得
  const session = event.cookies.get('session');

  // ユーザー情報をevent.localsに保存
  if (session) {
    const user = await getUserFromSession(session);
    event.locals.user = user;
  }

  // 保護されたページへのアクセスをチェック
  if (event.url.pathname.startsWith('/dashboard')) {
    if (!event.locals.user) {
      // 未ログインならログインページへ
      throw redirect(303, '/login');
    }
  }

  return resolve(event);
};
プレビュー
/dashboard/* へのアクセス
├─ ログイン済み → ページ表示
└─ 未ログイン → /login へリダイレクト

event.localsとは

リクエスト間でデータを共有

typescript
// src/hooks.server.ts
export const handle: Handle = async ({ event, resolve }) => {
  // localsにデータを保存(このリクエスト内で共有される)
  event.locals.user = { id: 1, name: '田中' };
  event.locals.startTime = Date.now();

  return resolve(event);
};

// src/routes/+page.server.ts
export function load({ locals }) {
  // handleで設定したlocalsを使える!
  console.log(locals.user);  // { id: 1, name: '田中' }

  return {
    user: locals.user
  };
}

// src/app.d.ts で型定義
declare global {
  namespace App {
    interface Locals {
      user: { id: number; name: string } | null;
      startTime: number;
    }
  }
}
プレビュー
event.locals
// hooks → load → actions で共有
// リクエストごとにリセット

複数のhookを組み合わせる

sequence関数

hookを順番に実行

typescript
// src/hooks.server.ts
import { sequence } from '@sveltejs/kit/hooks';
import type { Handle } from '@sveltejs/kit';

// 認証hook
const authHandle: Handle = async ({ event, resolve }) => {
  const session = event.cookies.get('session');
  event.locals.user = session ? await getUser(session) : null;
  return resolve(event);
};

// ロギングhook
const loggingHandle: Handle = async ({ event, resolve }) => {
  const start = Date.now();
  const response = await resolve(event);
  const duration = Date.now() - start;

  console.log(`${event.request.method} ${event.url.pathname} - ${duration}ms`);
  return response;
};

// 順番に実行
export const handle = sequence(authHandle, loggingHandle);
プレビュー
sequence(hook1, hook2, hook3)
// 順番に実行される
// 各hookは単一責任で書ける

handleFetch フック

handleFetchとは?

load関数などでfetch()を使うとき、そのリクエストをカスタマイズできます。
外部APIへの認証ヘッダー追加などに使います。

fetchにヘッダーを追加

外部APIへの認証

typescript
// src/hooks.server.ts
import type { HandleFetch } from '@sveltejs/kit';

export const handleFetch: HandleFetch = async ({ request, fetch, event }) => {
  // 特定のAPIへのリクエストにヘッダーを追加
  if (request.url.startsWith('https://api.example.com')) {
    request = new Request(request, {
      headers: {
        ...Object.fromEntries(request.headers),
        'Authorization': 'Bearer ' + event.locals.apiToken
      }
    });
  }

  return fetch(request);
};
プレビュー
// load関数でfetch()するとき
// 自動でAuthorizationヘッダーが付く

handleError フック

エラーのログと変換

予期しないエラーを処理

typescript
// src/hooks.server.ts
import type { HandleServerError } from '@sveltejs/kit';

export const handleError: HandleServerError = async ({ error, event, status, message }) => {
  // エラーをログに記録
  console.error('サーバーエラー:', error);

  // エラートラッキングサービスに送信(Sentryなど)
  // await Sentry.captureException(error);

  // ユーザーに表示するエラー情報を返す
  // (詳細なエラー情報は見せない)
  return {
    message: 'サーバーでエラーが発生しました',
    code: 'INTERNAL_ERROR'
  };
};
プレビュー
// エラー発生時
// 1. ログに記録
// 2. 安全なメッセージをユーザーに返す

実践的な例

完全な認証フロー

セッション管理の全体像

typescript
// src/hooks.server.ts
import { redirect } from '@sveltejs/kit';
import type { Handle } from '@sveltejs/kit';
import { db } from '$lib/server/db';

export const handle: Handle = async ({ event, resolve }) => {
  // 1. セッションCookieを取得
  const sessionId = event.cookies.get('session_id');

  if (sessionId) {
    // 2. DBからセッションを検証
    const session = await db.session.findUnique({
      where: { id: sessionId },
      include: { user: true }
    });

    if (session && session.expiresAt > new Date()) {
      // 3. 有効なセッション → ユーザー情報をセット
      event.locals.user = session.user;
    } else {
      // 4. 無効なセッション → Cookie削除
      event.cookies.delete('session_id', { path: '/' });
    }
  }

  // 5. 保護ルートのチェック
  const protectedRoutes = ['/dashboard', '/settings', '/profile'];
  const isProtected = protectedRoutes.some(route =>
    event.url.pathname.startsWith(route)
  );

  if (isProtected && !event.locals.user) {
    throw redirect(303, '/login?redirect=' + event.url.pathname);
  }

  return resolve(event);
};
プレビュー
// 認証フローの全体
1. Cookie取得
2. セッション検証
3. ユーザー情報セット
4. 保護ルートチェック