Remote functions
Remote functions are SvelteKit's way of calling server code from a component without hand rolling an API route. A form remote function runs on the server, validates the submission and reports back which fields failed and
why. That report is a { message: string }[] per field, the exact shape
every issues prop in this library takes, so wiring one up is a matter of passing a field's issues() straight into the matching component.
The form below wires five components to a single remote function: two TextFields, a Select, a DockedDatePicker and a Checkbox. It validates against a Valibot schema, the way a real app would. Submit it empty to see every field flag itself, or register ada@example.com to hit a rule the schema cannot express.
<script>
import { plans, registrationSchema } from './registration.schema'
import { submitRegistration } from './registration.remote'
const registration = submitRegistration.preflight(registrationSchema)
</script>
<form {...registration}>
<TextField
label="Name"
issues={registration.fields.name.issues()}
{...registration.fields.name.as('text')}
/>
<TextField
label="Email"
issues={registration.fields.email.issues()}
{...registration.fields.email.as('email')}
/>
<Select
label="Plan"
options={plans}
issues={registration.fields.plan.issues()}
{...registration.fields.plan.as('select')}
/>
<DockedDatePicker
label="Start date"
issues={registration.fields.startDate.issues()}
{...registration.fields.startDate.as('date')}
/>
<label>
<Checkbox
issues={registration.fields.acceptedTerms.issues()}
{...registration.fields.acceptedTerms.as('checkbox')}
/>
I accept the terms
</label>
<Button type="submit" loading={registration.pending > 0}>Register</Button>
</form>// registration.schema.ts
import * as v from 'valibot'
export type Plan = 'starter' | 'pro' | 'enterprise'
export const plans: { value: Plan; label: string }[] = [
{ value: 'starter', label: 'Starter' },
{ value: 'pro', label: 'Pro' },
{ value: 'enterprise', label: 'Enterprise' },
]
export const registrationSchema = v.object({
name: v.pipe(
v.string(),
v.trim(),
v.nonEmpty('Enter your name.'),
v.maxLength(80, 'Use 80 characters or fewer.'),
),
email: v.pipe(v.string(), v.trim(), v.email('Enter a valid email address.')),
plan: v.picklist(
plans.map((plan) => plan.value),
'Choose a plan.',
),
startDate: v.pipe(v.string(), v.isoDate('Pick a start date.')),
// Checkboxes send nothing when unchecked, so the field has to be optional with a default.
acceptedTerms: v.pipe(
v.optional(v.boolean(), false),
v.literal(true, 'You must accept the terms.'),
),
})// registration.remote.ts
import { form } from '$app/server'
import { invalid } from '@sveltejs/kit'
import { registrationSchema } from './registration.schema'
// Stands in for a database. A real app would query one here.
const takenEmails = new Set(['ada@example.com'])
export const submitRegistration = form(registrationSchema, async (data, issue) => {
// The schema validates shape and format. Rules that need the database live here.
if (takenEmails.has(data.email.toLowerCase())) {
invalid(issue.email('That email is already registered.'))
}
await createRegistration(data)
return { name: data.name }
})How it works
Spreading {...registration} onto the <form> points it at the
remote function and attaches the handler that intercepts submission on the client, so nothing reloads.
Without JavaScript the same markup still works: the browser posts to the function's own URL and SvelteKit
re-renders the page with the result, issues included.
registration.fields.name.issues() returns that field's { message, path }[] array whenever the last submission flagged it, and undefined otherwise. Every component on this page reads its own field's issues() directly into its issues prop, no adapter or translation layer in between. Messages the
handler adds itself land in the same array, so a field looks the same whether the schema or a database
lookup rejected it.