sveltekit-enhance Svelte Themes

Sveltekit Enhance

Helper functions for sveltekit applications like middleware

@sourceregistry/sveltekit-enhance

Composable middleware, guards, and form utilities for SvelteKit

Wrap actions, loads, methods, and hooks with composable enhancers. Stack auth guards, feature flags, request tracing, and form parsing without touching SvelteKit's internals.

Docs · npm · Issues


Installation

npm install @sourceregistry/sveltekit-enhance

Peer dependency: @sveltejs/kit ^2.58.0


Overview

import { enhance, Auth, RequestCorrelation, RequestMonitor, Form } from '@sourceregistry/sveltekit-enhance';

// hooks.server.ts
export const handle = enhance.handle(
    async ({ event, resolve }) => resolve(event),
    RequestCorrelation.attach,
    RequestMonitor.trace({ logger: myLogger, record: metrics.record }),
);

// +server.ts
export const POST = enhance.method(
    async (event) => new Response(JSON.stringify(event.context)),
    Auth.Bearer,
    FeatureFlag.all('PUBLIC_API_ENABLED'),
);

// +page.server.ts
export const actions = {
    default: enhance.action(
        async (event) => {
            const name = event.context.form.string$('name');
            return success({ name });
        },
        Form.schema(myValidator),
    ),
};

Core API

Import from @sourceregistry/sveltekit-enhance.

enhance.handle

Wraps SvelteKit's handle hook. Enhancers run left-to-right before the handler; their return values are merged into context.

import { enhance } from '@sourceregistry/sveltekit-enhance';

// src/hooks.server.ts
export const handle = enhance.handle(
    async ({ event, resolve, context }) => resolve(event),
    enhancerA,
    enhancerB,
);

SSE / streaming responses

enhance.handle automatically detects Server-Sent Events and other streaming requests (via Accept: text/event-stream) and bypasses the main handler entirely — going straight to SvelteKit's resolve. This prevents the handler from blocking indefinitely on await resolve(event), which never settles for a streaming response.

Consequences:

  • The main handler does not run for SSE requests — it cannot inspect or modify the response headers.
  • responseHandlers (added by enhancers via contextInput.responseHandlers.push(...)) are also skipped for responses with Content-Type: text/event-stream.
  • Enhancers (the variadic context arguments) still run normally — they can set locals, authenticate the request, or short-circuit with an error response.
// This handler is skipped for SSE requests — no change needed on your end.
export const handle = enhance.handle(
    async ({ event, resolve }) => {
        const response = await resolve(event); // not called for SSE
        response.headers.set('x-custom', 'value');
        return response;
    },
    Auth.Bearer,       // still runs — can reject unauthorized SSE requests
    SessionGuard.hook, // still runs
);

If you need to set response headers on SSE routes, do it in the route handler itself (e.g. +server.ts) rather than in the enhance.handle main handler.

enhance.load

Wraps server load functions.

// +page.server.ts
export const load = enhance.load(
    async (event) => ({ user: event.context.user }),
    Auth.Bearer,
);

enhance.action

Wraps form actions.

// +page.server.ts
export const actions = {
    submit: enhance.action(
        async (event) => success(event.context),
        Auth.Bearer,
        Form.schema(myValidator),
    ),
};

enhance.method

Wraps +server.ts endpoint handlers.

// +server.ts
export const GET = enhance.method(
    async (event) => new Response(JSON.stringify(event.context)),
    Auth.Bearer,
);

Utilities

import { fail, error, success, not_good } from '@sourceregistry/sveltekit-enhance';

fail(400, { message: 'bad input' });   // throws ActionFailure — use inside actions
error(404, { message: 'not found' });  // throws HttpError
success({ id: 1 });                    // typed identity helper
not_good(input, 403);                  // delegates to fail or error based on callType

Helpers

All helpers are available from @sourceregistry/sveltekit-enhance or @sourceregistry/sveltekit-enhance/helpers.


CSRF

Blocks cross-site form submissions on mutating methods (POST, PUT, PATCH, DELETE) with form content types. Checks the Origin header against the request origin. Absent Origin (server-side fetch, curl) is allowed through. Returns 403 — JSON body when Accept: application/json, SvelteKit error() otherwise.

import { CSRF, CSRFChecker } from '@sourceregistry/sveltekit-enhance';

export const handle = enhance.handle(
    myHandler,
    CSRF.inspect(
        CSRFChecker.list('/api/webhooks/stripe'),  // bypass paths
        myLogger,                                   // optional, defaults to console
    ),
);

Built-in bypass checkers:

Checker Description
CSRFChecker.list(...paths) Exact pathname match
CSRFChecker.regex(...patterns) RegExp match against pathname

Custom checker — any (input: EnhanceInput) => MaybePromise<boolean>:

// true = bypass CSRF check
CSRF.inspect((input) => input.url.pathname.startsWith('/api/public'))

Returns { csrf_valid: true } on pass. Locals set: none.


Auth

Extracts and validates Authorization: Bearer <token> headers.

import { Auth } from '@sourceregistry/sveltekit-enhance';

export const GET = enhance.method(
    async (event) => new Response(event.context.token),
    Auth.Bearer,
);

Returns { token: string }. Throws 401 if the header is missing or malformed.


Devtools

Silences Chrome DevTools probe requests (/.well-known/appspecific/com.chrome.devtools.json) with a 204 No Content. Logs in dev mode.

import { Devtools } from '@sourceregistry/sveltekit-enhance';

export const handle = enhance.handle(myHandler, Devtools.ignore);

FeatureFlag

Guards routes behind SvelteKit public env vars ($env/dynamic/public). Always passes in dev mode.

import { FeatureFlag } from '@sourceregistry/sveltekit-enhance';

// All listed flags must be enabled
FeatureFlag.all('PUBLIC_FEATURE_A', 'PUBLIC_FEATURE_B')

// At least one flag must be enabled
FeatureFlag.oneOf('PUBLIC_FEATURE_A', 'PUBLIC_FEATURE_B')

Truthy values: true, TRUE, on, ON, 1. Returns { flags } or throws 503 Feature not enabled.


RequestCorrelation

Propagates a correlation ID across the request/response cycle.

  • Reads x-correlation-id or x-request-id from incoming headers
  • Validates: max 128 chars, pattern [A-Za-z0-9._:-]+
  • Generates a UUID v4 if absent
  • Echoes the ID back via x-correlation-id response header
import { RequestCorrelation } from '@sourceregistry/sveltekit-enhance';

export const handle = enhance.handle(myHandler, RequestCorrelation.attach);
Local Type Description
correlation_id string Resolved correlation ID
request_started_at number Date.now() at attach time

RequestMonitor

Structured HTTP request logging and optional metrics collection. Instruments the full lifecycle: start, completion (log level by status), and unhandled errors — all with elapsed duration.

import { RequestMonitor } from '@sourceregistry/sveltekit-enhance';

export const handle = enhance.handle(
    myHandler,
    RequestCorrelation.attach,
    RequestMonitor.trace({
        logger: myLogger,        // optional, defaults to console
        record: metrics.record,  // optional
    }),
);

TraceOptions

Option Type Default Description
logger TraceLogger console Must implement debug, info, warn, error
record (entry: RecordTraceMetricEntry) => any Called after every request

Log events

Event Level Condition
http.request.started debug Before resolve
http.request.completed info status < 400
http.request.completed warn status 4xx
http.request.completed error status 5xx
http.request.failed error Unhandled throw

Types

type RecordTraceMetricEntry = { method: string; path: string; status: number; durationMs: number }
type TraceLogger            = { debug(...args: any[]): any; info(...args: any[]): any; warn(...args: any[]): any; error(...args: any[]): any }

Locals set: trace: { id: string; started_at: bigint }.


Form

Typed, ergonomic FormData extraction. Works standalone or as an enhancer.

As an enhancer — Form.schema

Validates and deserializes form data via a Validator<T> before the handler runs.

import { Form, enhance, success } from '@sourceregistry/sveltekit-enhance';

export const actions = {
    default: enhance.action(
        async (event) => success(event.context.form.result),
        Form.schema(myValidator),
    ),
};

Standalone — Form.handle

import { Form } from '@sourceregistry/sveltekit-enhance';

await Form.handle(request, ({ form }) => {
    const name = form.string$('name');
    const age  = form.number('age');
    return { name, age };
});

Field extractors

Optional variants return undefined when the field is absent. Required variants ($ suffix) throw fail(400).

Method Returns Notes
string(name) / string$(name) string | null | undefined
pattern$(name, pattern) string RegExp or pattern string
number(name) / number$(name) number | undefined
boolean(name) / boolean$(name) boolean | undefined Accepts true/false, 1/0, on/off
date(name, parser?) / date$(name, parser) Date | undefined Custom parser supported
json<T>(name, transformer?) / json$(name) T | undefined Optional transform fn
jsond(options) any All FormData → nested object via dot-notation keys
file(name) / file$(name) File | null | undefined
files(name) File[] Non-empty, named files only
fileRecord(prefix, removePrefix?) Record<string, File[]> Groups files by key prefix
array<T>(name, mapper?) / array$(name) T[] | undefined
enum(name, Enum) / enum$(name, Enum) keyof E | undefined
record(options?) Record<string, any> All entries; optional filter / transformer
validate(schema, options?) T Runs Validator<T>, throws fail(400) on failure

Conditional helpers

form.onlyIf(condition, trueVal, falseVal?)
form.onlyIfPresent(key, (entry) => ..., fallback?)
form.onlyIfArrayPresent(key, (entries) => ..., fallback)
form.selector({ fieldName: (entry, key) => ..., $default?, $error? })
form.selector$({ ... })    // throws fail(400) if no case matches
form.basedOn(val, processor)
form.process(name, parser, processor)

Standalone functions

All FormContext methods are also exported as standalone functions taking FormData as the first argument, plus:

arrayString(formdata, name, delimiter, mapper?)
hasOneOf(formdata, names)
reviver(key, value)   // JSON.parse reviver — coerces strings to typed primitives

Type Reference

// Core
EnhanceInput<CallType>     // event passed to all enhancers
EnhanceFunction<CallType>  // (event: EnhanceInput) => MaybePromise<object | Response>
EnhanceHandle              // final handle fn — ({ event, resolve, context }) => MaybePromise<Response>
EnhanceLoad                // final load fn
EnhanceAction              // final action fn
EnhanceMethod              // final method fn
MaybePromise<T>            // T | Promise<T>

// Form
Validator<T>               // (value: unknown, path?: ValidationPath) => ValidationResult<T>
ValidationResult<T>        // { success: true; data: T } | { success: false; errors: ValidationIssue[] }
ValidationIssue            // { path: string; message: string; code?: string }
FormContext                // fluent API returned by Form.enhance / Form.handle
InferValidator<V>          // infers T from Validator<T>

// Helpers
TraceLogger                // { debug, info, warn, error }
TraceOptions               // { logger?: TraceLogger; record?: (entry) => any }
RecordTraceMetricEntry     // { method: string; path: string; status: number; durationMs: number }
RequestTraceLocals         // { trace?: { id: string; started_at: bigint } }
RequestCorrelationLocals   // { correlation_id?: string; request_started_at?: number }
CSRFChecker                // { regex(...patterns): checker; list(...paths): checker }

License

Apache-2.0 © A.P.A. Slaa

Top categories

Loading Svelte Themes