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. 保護ルートチェック