Full-stack frameworks
SvelteKit
Serve your API from a SvelteKit endpoint and call it from load.
On this page
Serve your API from a SvelteKit +server.ts endpoint. We’ll make an HTTP request to check it, then call it from a page’s load function.
Import endpoint helpers from @hulla/api-sveltekit/server. For operations used only by your SvelteKit app, you can also use remote functions; their helpers live in @hulla/api-sveltekit/remote.
Start with an existing SvelteKit 2 application. The adapter supports @sveltejs/kit 2.x and Node.js 22 or newer.
bun add @hulla/api @hulla/api-sveltekit zodnpm install @hulla/api @hulla/api-sveltekit zodpnpm add @hulla/api @hulla/api-sveltekit zodyarn add @hulla/api @hulla/api-sveltekit zodKeep the contract in shared code. This example declares GET /api/health and POST /api/rename:
// src/lib/api/contract.ts
import { defineContract, response, route } from '@hulla/api'
import { z } from 'zod'
export const renameInput = z.object({ name: z.string().min(1) })
export const healthUnavailable = z.object({ message: z.string() })
export const contract = defineContract({
basePath: '/api',
routes: {
health: route.get('/health', {
responses: {
200: response.text(),
503: response.json(healthUnavailable),
},
}),
rename: route.post('/rename', {
body: renameInput,
responses: { 200: response.json(renameInput) },
}),
},
})Implement the same route tree in server code:
// src/lib/server/api.ts
import { defineServer } from '@hulla/api/server'
import { contract } from '$lib/api/contract'
export const implementation = defineServer(contract).implement({
health: ({ response }) => response(200, 'ok'),
rename: ({ body, response }) => response(200, body),
})See contract authoring and server implementations for the complete APIs.
Create a rest-parameter endpoint and export the mounted handler only under the methods declared by the sample contract:
// src/routes/api/[...api]/+server.ts
import { svelteKitAdapter } from '@hulla/api-sveltekit/server'
import { implementation } from '$lib/server/api'
const handler = svelteKitAdapter().mount(implementation)
export { handler as GET, handler as POST }SvelteKit discovers endpoint methods from static named exports, so mount() returns one handler rather than a dynamically generated method map. The file route selects the catch-all; @hulla/api then matches the complete request method and URL against the mounted contract.
Export only the methods present in the mounted implementation. Static method exports are part of SvelteKit’s endpoint contract.
SvelteKit rest parameters can match an empty tail, so this file covers /api as well as nested paths such as /api/health. When a GET export exists without HEAD, SvelteKit supplies the GET handler for HEAD. The adapter dispatches that request through the contract’s GET route and returns the status and headers without a body.
SvelteKit endpoints cannot expose @hulla/api’s QUERY method. mount() rejects an implementation or fragment containing one. Keep framework-specific OPTIONS behavior in the endpoint or a server hook when it is not part of the shared contract.
Start the application with its normal development command. The port may differ in your project; the examples use http://localhost:5173.
curl -i http://localhost:5173/api/healthThe endpoint returns the declared text response:
HTTP/1.1 200 OK
content-type: text/plain
okcurl -i \
-X POST \
-H 'content-type: application/json' \
-d '{"name":"Ada"}' \
http://localhost:5173/api/renameThe endpoint returns the declared JSON response:
HTTP/1.1 200 OK
content-type: application/json
{"name":"Ada"}Use SvelteKit’s enhanced fetch inside load so server rendering inherits same-origin credentials, resolves relative requests, and can invoke internal endpoints without a network round trip:
// src/routes/+page.server.ts
import { createClient } from '@hulla/api/client'
import { fetchTransport } from '@hulla/api/fetch'
import { contract } from '$lib/api/contract'
export async function load({ fetch, url }) {
const api = createClient(contract, {
transport: fetchTransport({ baseUrl: url.origin, fetch }),
})
const result = await api.health()
if (result.status !== 200)
throw new Error(`Could not load health: ${result.status}`)
return { health: result.body }
}Keep SvelteKit form actions in charge of progressive enhancement and invalidation. They can call the same client when the deployed HTTP boundary itself must be exercised. For SvelteKit’s zero-Fetch remote-function path, see SvelteKit remote functions.
Use adapter.context() when context construction needs SvelteKit request state:
// src/lib/api/server-with-context.ts
import { defineServer } from '@hulla/api/server'
import { svelteKitAdapter } from '@hulla/api-sveltekit/server'
import { contract } from '$lib/api/contract'
const adapter = svelteKitAdapter()
export const implementation = defineServer(contract, {
context: adapter.context(({ request, route, svelteKitEvent }) => ({
actor: svelteKitEvent.locals.actor,
clientAddress: svelteKitEvent.getClientAddress(),
operation: route.key,
platform: svelteKitEvent.platform,
request,
})),
}).implement({
health: ({ response }) => response(200, 'ok'),
rename: ({ body, response }) => response(200, body),
})svelteKitEvent is SvelteKit’s complete RequestEvent, including cookies, enhanced fetch, locals, file-route
params, platform, route ID, URL, and response-header helpers. request is also exposed directly for consistency with
the other Fetch-based adapters. The route value is contract-derived operation metadata and is separate from
svelteKitEvent.route and svelteKitEvent.params, which describe the host file route.
Type locals through SvelteKit’s standard application augmentation:
// src/app.d.ts
declare global {
namespace App {
// Interface declaration is required here because SvelteKit uses declaration merging.
interface Locals {
actor: { readonly userId: string }
}
}
}
export {}A server handle hook should authenticate the request and populate event.locals. The @hulla/api context can then translate framework state into the smaller application context used by contract handlers. A context factory wrapped by adapter.context() is bound to sveltekit; mounting it through another adapter or generic inProcessTransport() fails early. The SvelteKit remote transport deliberately satisfies that same adapter requirement.
Set a shared error hook on the adapter, or override it for one mounted fragment:
import { svelteKitAdapter } from '@hulla/api-sveltekit/server'
import { implementation } from '$lib/server/api'
const adapter = svelteKitAdapter({
onError({ error, phase, request, route, svelteKitEvent, defaultResponse }) {
console.error(
phase,
request.url,
route?.key,
svelteKitEvent.route.id,
error
)
return defaultResponse
},
})
const handler = adapter.mount(implementation)
export { handler as GET, handler as POST }The hook receives @hulla/api’s protocol-safe default Response, the original SvelteKit event, the request, and matched contract metadata when available. Return a replacement Response to customize the result, or undefined to retain the default. Declared contract failures remain normal responses; use this hook for adapter or handler failures.
Use a fragment when a route group needs separate code-splitting, middleware, deployment, or ownership boundaries. Mount it from a narrower +server.ts file and export only the methods declared by that fragment. See fragments and composition for the complete defineServer(contract.routes...) patterns.
Keep SvelteKit’s application data flow native: load orchestrates page data, form actions own progressive enhancement, and remote functions own their separate protocol and refresh lifecycle. For the remote transport and its zero-Fetch in-server dispatch, continue to SvelteKit remote functions.
Next, read server and browser calls to choose a transport, context and middleware for shared policy, and errors for declared application failures.