iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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…