Hooks & Middleware
SvelteKit hooks are server-side functions that run on every request before any route handler. The three hooks: handle (wrap every request — auth, headers, tracing), handleFetch (rewrite outbound fetch from server load functions), and handleError (centralised error logging). Use them for cross-cutting concerns, not per-route logic.
Auth, request tracing, and error reporting via hooks
EXAMPLE
// src/hooks.server.ts
import type { Handle, HandleFetch, HandleServerError } from '@sveltejs/kit';
import { randomUUID } from 'node:crypto';
import * as Sentry from '@sentry/sveltekit';
Sentry.init({ dsn: process.env.SENTRY_DSN, environment: process.env.NODE_ENV });
// 1) handle — runs around every request
export const handle: Handle = async ({ event, resolve }) => {
const t0 = performance.now();
// a) Per-request id for correlated logs
const reqId = event.request.headers.get('x-request-id') ?? randomUUID();
event.locals.reqId = reqId;
// b) Authentication — populate event.locals.user for load functions
const sid = event.cookies.get('sid');
if (sid) {
try { event.locals.user = await db.sessions.verify(sid); }
catch { event.cookies.delete('sid', { path: '/' }); }
}
// c) DB client per request (not shared across requests)
event.locals.db = db;
// d) Resolve the actual route handler
const response = await resolve(event, {
transformPageChunk: ({ html }) => html.replace('%lang%', event.locals.user?.locale ?? 'en'),
});
// e) Outbound headers — defence in depth
response.headers.set('x-request-id', reqId);
response.headers.set('x-frame-options', 'DENY');
response.headers.set('content-security-policy',
"default-src 'self'; img-src 'self' data:; script-src 'self' 'nonce-r4nd0m'");
// f) Structured access log
console.info({
reqId,
method: event.request.method,
path: event.url.pathname,
status: response.status,
ms: (performance.now() - t0).toFixed(1),
user: event.locals.user?.id ?? null,
});
return response;
};
// 2) handleFetch — every fetch() called from a load() / action runs through here.
// Use it to add an internal service token, rewrite hostnames, or short-circuit.
export const handleFetch: HandleFetch = async ({ event, request, fetch }) => {
const url = new URL(request.url);
// Rewrite an internal hostname when running inside the cluster
if (url.host === 'api.example.com' && process.env.NODE_ENV !== 'production') {
request = new Request(\`http://api:3000${url.pathname}${url.search}\`, request);
}
// Forward auth token to downstream services
if (url.host.endsWith('.example.com') && event.locals.user) {
request.headers.set('authorization', \`Bearer ${await mintServiceToken(event.locals.user.id)}\`);
}
return fetch(request);
};
// 3) handleError — centralised error reporter
export const handleError: HandleServerError = async ({ error, event }) => {
const reqId = event.locals.reqId;
Sentry.captureException(error, { extra: { reqId, path: event.url.pathname } });
console.error({ reqId, err: (error as Error).message });
return { message: 'Something went wrong', reqId };
};
// 4) Type-safe locals (src/app.d.ts)
// declare namespace App {
// interface Locals { reqId: string; user?: { id: string; locale?: string }; db: typeof db; }
// interface Error { message: string; reqId?: string; }
// }
declare const db: any;
async function mintServiceToken(_id: string) { return 'svc'; }
Why it matters
handleFetch is the unsung hero — it lets a single seam rewrite every outbound call from load() functions. Internal service URLs, signed auth tokens, request tracing headers — set them once in one file and every server-side fetch in the app inherits the behaviour, instead of threading config through every call.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
// hooks.server.js
export async function handle({ event, resolve }) {
event.locals.user = await getUser(event.request);
return resolve(event);
}
Try it Yourself »
Discussion
Loading…