SvelteKitのhooksを深掘りした話(handle・handleError・handleFetch・sequence)

SvelteKitの hooks.server.ts に書く関数を整理します。認証以外の実用的なユースケースも含めて解説します。


hooks の種類

hooks.server.ts
  ├─ handle          → 全リクエストを横断処理する
  ├─ handleError     → 予期しないエラーをキャッチする
  └─ handleFetch     → load 関数内の fetch をインターセプト

hooks.client.ts
  └─ handleError     → クライアント側のエラーをキャッチする

handle — 全リクエストを横断処理

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

export const handle: Handle = async ({ event, resolve }) => {
  // リクエスト前の処理
  console.log(`${event.request.method} ${event.url.pathname}`);

  const response = await resolve(event);

  // レスポンス後の処理
  return response;
};

resolve(event) を呼ぶ前後に処理を挟めます。全リクエストに共通の処理を書く場所です。

実用的なユースケース:

  • セッション確認・locals へのユーザー情報のセット(認証)
  • 特定パスへのアクセス制限
  • レスポンスヘッダーの追加(CORS・セキュリティヘッダー)
  • リクエストのログ記録

sequence — 複数の handle をまとめる

handle の処理が増えてきたときは sequence で分割・整理します。

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

const auth: Handle = async ({ event, resolve }) => {
  const sessionId = event.cookies.get('session_id');

  if (sessionId) {
    const db = event.platform?.env.DB;
    const session = await db?.prepare(
      'SELECT sessions.id, users.name FROM sessions JOIN users ON sessions.user_id = users.id WHERE sessions.id = ? AND sessions.expires_at > ?'
    )
      .bind(sessionId, Math.floor(Date.now() / 1000))
      .first<{ id: string; name: string }>();

    if (session) {
      event.locals.user = { name: session.name };
    } else {
      event.locals.user = null;
      event.cookies.delete('session_id', { path: '/' });
    }
  } else {
    event.locals.user = null;
  }

  return resolve(event);
};

const logger: 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} ${response.status} ${duration}ms`);

  return response;
};

export const handle = sequence(auth, logger);

sequence は上から順に実行されます。アクセスするたびにターミナルに以下のようなログが出力されます。

GET /  200  45ms
GET /posts/1  200  12ms
POST /login/github/callback  302  89ms

sequence を使うメリット:

  • 認証・ログなど関心ごとにコードが分離される
  • 新しい処理を追加するときは sequence(auth, logger, 新しい処理) と並べるだけ
  • 各 handle を個別にテストしやすくなる

handleError — 予期しないエラーをキャッチ

import type { HandleServerError } from '@sveltejs/kit';

export const handleError: HandleServerError = async ({ error, event }) => {
  // エラーをログサービスに送るなど
  console.error('予期しないエラー:', error);

  // +error.svelte に渡すメッセージを返す
  return {
    message: 'サーバーエラーが発生しました',
  };
};

error() ヘルパーで意図的に投げたエラーはここには来ません。バグや予期しない例外だけがここに来ます。


handleFetch — load関数内のfetchをインターセプト

import type { HandleFetch } from '@sveltejs/kit';

export const handleFetch: HandleFetch = async ({ request, fetch }) => {
  // 全fetchリクエストに認証ヘッダーを付ける
  const modifiedRequest = new Request(request, {
    headers: {
      ...Object.fromEntries(request.headers),
      'Authorization': `Bearer ${process.env.API_TOKEN}`,
    },
  });

  return fetch(modifiedRequest);
};

+page.server.tsload 関数内で使う fetch をインターセプトできます。外部APIへのリクエストに共通のヘッダーを付けたいときに便利です。


まとめ

hook タイミング 主な用途
handle 全リクエスト 認証・ログ・ヘッダー追加
sequence 複数handleをつなげる 処理の分離・整理
handleError 予期しないエラー発生時 エラーログ・エラーメッセージのカスタマイズ
handleFetch load内のfetch 共通ヘッダーの付与
← トップページに戻る