Skip to content

Framework GuidesSvelteKit

SvelteKit

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

3 min readUpdated proxy 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. A read that may be cached arrives as GET, and every other call as POST.

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, given the routes Courier writes into courier.routes.ts from the endpoints you mark .expose() — see Exposing Endpoints:

ts
import { createProxyCaller } from "@noverax/courier"
import { apiRoutes } from "./courier.routes"

// The routes name the key this source has in your `sources` map —
// it is what the proxy addresses, and it travels as `?_s=<name>`.
const caller = createProxyCaller(apiRoutes, {
  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.