Skip to content

Framework GuidesSvelteKit

SvelteKit

Serve Courier's proxy from a SvelteKit endpoint, with @noverax/courier-sveltekit.

3 min readproxy mode

@noverax/courier-sveltekit serves the proxy that createProxyCaller and createProxyQueryCaller call: one route answers every endpoint, reads over GET and writes over POST, and streams server-sent events.

In one command

bash
npx @noverax/create-courier

In a SvelteKit app it adds what this page sets up by hand — the endpoint, the registration in hooks.server.ts, and Studio's plugin — with a typed example API, its mock, and a page at /courier. See Create Courier.

Install

bash
npm install @noverax/courier-sveltekit @noverax/courier @sveltejs/kit

Usage

ts
// src/hooks.server.ts
import { registerSources } from "@noverax/courier"
import { sources } from "$lib/api"

registerSources(sources)
ts
// src/routes/api/request/+server.ts
import { courierHandler } from "@noverax/courier-sveltekit"

const handler = courierHandler()
export const GET = handler
export const POST = handler

courierHandler() is zero-arg — sources are read at request time from the core global registry populated by registerSources() in hooks.server.ts. The handler uses Web Standard Request/Response natively. GET requests are routed to query-type endpoints, POST requests handle mutations.

In production

SvelteKit answers some calls before the proxy sees them. With @sveltejs/adapter-node:

  • Set ORIGIN to the URL the app is served at, such as ORIGIN=https://app.example.com node build. Without it SvelteKit cannot tell the protocol it is served over and reports an https: request URL, which a browser's http: origin never matches, so the proxy refuses every POST with 403. Behind a proxy that sets forwarding headers, PROTOCOL_HEADER and HOST_HEADER do the same job.
  • Raise BODY_SIZE_LIMIT, which is 512 KB by default and answers any larger body with its own 413 Payload Too Large — a 900 KB JSON call or any upload over it — whatever the proxy allows. The proxy reads a JSON call up to its maxBodyBytes, 1 MB, and an upload up to its maxUploadBytes, 10 MB; BODY_SIZE_LIMIT=11M covers both, and a larger maxUploadBytes needs a larger limit.
  • Callers without an Origin: SvelteKit's CSRF check refuses a multipart/form-data or text/plain POST whose Origin does not match, and one with no Origin at all, such as a server-side script, with a plain-text 403. Browser pages on the app's own origin pass it.

In vite dev, a chunked body larger than the limit resets the connection instead of answering 413, and a client that declares an oversized upload can see the connection close before it has sent it all.

The client

Pair with createProxyCaller on the client:

ts
import { createProxyCaller } from "@noverax/courier"
import type { api } from "./api"

// The first argument is the key this source has in your `sources` map —
// it is what the proxy addresses, and it travels as `?_s=<name>`.
const caller = createProxyCaller<typeof api>("api", {
  proxyPath: { http: "/api/request" },
})
const users = await caller.users.getAll()

The user's session

An API that keeps a session gets the user's session with every call a hook, a load function, a form action or an endpoint makes, once the request being served can be found — and a proxy caller in universal code, rendered on the server, reaches the proxy in process with it:

src/hooks.server.ts — SvelteKit 2.20 and later
import { getRequestEvent } from "$app/server"
import { registerRequestContext, registerSources } from "@noverax/courier"
import { svelteKitRequestContext } from "@noverax/courier-sveltekit"
import { sources } from "$lib/api"

registerSources(sources)
registerRequestContext(svelteKitRequestContext(getRequestEvent))
src/hooks.server.ts — any SvelteKit 2
import { requestContextHandle } from "@noverax/courier-sveltekit/handle"

export const handle = requestContextHandle()

getRequestEvent follows the request through every await where the platform has AsyncLocalStorage, and only until the first one where it has none. The handle hook needs AsyncLocalStorage itself — Node, Bun, Deno, Cloudflare with nodejs_als. Either way the session cookies the API sets go to cookies.set() exactly as the API wrote them, a load function's included. See User Sessions.

Cancelled calls

When the browser cancels a call — an aborted upload, a query it no longer needs — the proxy stops its own upstream request. SvelteKit's request signal does not report a client that leaves after its body has arrived, so under @sveltejs/adapter-node the handler watches the client's socket, from platform.req, instead. The Vite dev server exposes no connection: during vite dev a cancelled call still runs to completion upstream.