Create type-safe RPC-style functions for client-server communication with commands, queries, forms, and prerender functions.
Create command functions that execute server-side code when called from the client.
// From '$app/server'
/**
* Create remote command (mutation) callable from client
* @param fn - Command implementation (no parameters)
* @returns RemoteCommand callable from client
* @since 2.27
*/
function command<Output>(
fn: () => Output | Promise<Output>
): RemoteCommand<void, Output>;
/**
* Create remote command with input validation
* @param validate - 'unchecked' to skip validation
* @param fn - Command implementation receiving validated input
* @returns RemoteCommand callable from client
* @since 2.27
*/
function command<Input, Output>(
validate: 'unchecked',
fn: (input: Input) => Output | Promise<Output>
): RemoteCommand<Input, Output>;
/**
* Create remote command with StandardSchema validation
* @param validate - StandardSchema validator (Valibot, Zod, etc.)
* @param fn - Command implementation receiving validated input
* @returns RemoteCommand callable from client
* @since 2.27
*/
function command<Schema extends StandardSchemaV1, Output>(
validate: Schema,
fn: (input: StandardSchemaV1.InferOutput<Schema>) => Output | Promise<Output>
): RemoteCommand<StandardSchemaV1.InferInput<Schema>, Output>;
type RemoteCommand<Input, Output> = {
/** Execute the command */
(input: Input): Promise<Awaited<Output>> & {
/** Execute with query updates for optimistic UI */
updates(
...queries: Array<RemoteQuery<any> | RemoteQueryOverride>
): Promise<Awaited<Output>>;
};
/** The number of pending command executions */
readonly pending: number;
};Usage:
// Server: src/lib/server/commands.ts
import { command, getRequestEvent } from '$app/server';
import { error } from '@sveltejs/kit';
import * as v from 'valibot';
// Basic command with unchecked validation
export const deletePost = command('unchecked', async (postId: string) => {
const event = getRequestEvent();
if (!event.locals.user) error(401);
await db.posts.delete(postId);
return { success: true };
});
// Command with StandardSchema validation
const UpdatePostSchema = v.object({
id: v.string(),
title: v.string(),
content: v.string()
});
export const updatePost = command(UpdatePostSchema, async (data) => {
// data is fully typed from schema
await db.posts.update(data.id, { title: data.title, content: data.content });
return { success: true };
});
// Client usage
import { deletePost, updatePost } from '$lib/server/commands';
// Simple execution
await deletePost('post-123');
// With query updates (optimistic UI)
import { getPost } from '$lib/server/queries';
const post = getPost('post-123');
await updatePost({ id: 'post-123', title: 'New Title', content: 'Content' })
.updates(post.withOverride((current) => ({ ...current, title: 'New Title' })));
// Check pending state
console.log(deletePost.pending); // number of pending executionsCreate query functions that execute server-side code and provide reactive state.
// From '$app/server'
/**
* Create remote query (read) callable from client
* @param fn - Query implementation (no parameters)
* @returns RemoteQueryFunction callable from client
* @since 2.27
*/
function query<Output>(
fn: () => Output | Promise<Output>
): RemoteQueryFunction<void, Output>;
/**
* Create remote query with input validation
* @param validate - 'unchecked' to skip validation
* @param fn - Query implementation receiving validated input
* @returns RemoteQueryFunction callable from client
* @since 2.27
*/
function query<Input, Output>(
validate: 'unchecked',
fn: (input: Input) => Output | Promise<Output>
): RemoteQueryFunction<Input, Output>;
/**
* Create remote query with StandardSchema validation
* @param schema - StandardSchema validator (Valibot, Zod, etc.)
* @param fn - Query implementation receiving validated input
* @returns RemoteQueryFunction callable from client
* @since 2.27
*/
function query<Schema extends StandardSchemaV1, Output>(
schema: Schema,
fn: (input: StandardSchemaV1.InferOutput<Schema>) => Output | Promise<Output>
): RemoteQueryFunction<StandardSchemaV1.InferInput<Schema>, Output>;
type RemoteQueryFunction<Input, Output> = (input: Input) => RemoteQuery<Output>;
type RemoteQuery<T> = Promise<Awaited<T>> & {
/** The error in case the query fails (most often HttpError) */
readonly error: any;
/** true before the first result is available and during refreshes */
readonly loading: boolean;
/** The current value of the query (undefined until ready is true) */
readonly current: Awaited<T> | undefined;
/** true once the first result is available */
readonly ready: boolean;
/**
* Update the value of the query without re-fetching
* On the server, can be called in command/form context to send data with response
*/
set(value: T): void;
/**
* Re-fetch the query from the server
* On the server, can be called in command/form context to send refreshed data with response
*/
refresh(): Promise<void>;
/**
* Create a temporary override for optimistic updates
* Used with command.updates() or form.enhance()
*/
withOverride(update: (current: Awaited<T>) => Awaited<T>): RemoteQueryOverride;
};
interface RemoteQueryOverride {
_key: string;
release(): void;
}Usage:
// Server: src/lib/server/queries.ts
import { query } from '$app/server';
import * as v from 'valibot';
// Query with unchecked validation
export const getUser = query('unchecked', async (userId: string) => {
return await db.users.find(userId);
});
// Query with StandardSchema validation
const UserIdSchema = v.string();
export const getUserWithSchema = query(UserIdSchema, async (userId) => {
return await db.users.find(userId);
});
// Client usage
import { getUser } from '$lib/server/queries';
// Execute query - returns reactive RemoteQuery object
const user = getUser('user-123');
// Access reactive properties
console.log(user.current); // Current value (undefined until ready)
console.log(user.ready); // true when data is available
console.log(user.loading); // true during initial load and refreshes
console.log(user.error); // Error if query failed
// Await the result
const userData = await user;
// Refresh the query
await user.refresh();
// Update locally without re-fetching
user.set({ ...user.current, name: 'Updated' });
// Use in Svelte component
// user.current, user.loading, and user.error are all reactiveCreate batch queries that collect multiple concurrent calls into a single request.
// From '$app/server'
namespace query {
/**
* Create batch query that processes multiple inputs in a single request
* @param validate - 'unchecked' to skip validation
* @param fn - Batch implementation returning a resolver function
* @returns RemoteQueryFunction callable from client
* @since 2.35
*/
function batch<Input, Output>(
validate: 'unchecked',
fn: (args: Input[]) => (arg: Input, idx: number) => Output | Promise<(arg: Input, idx: number) => Output>
): RemoteQueryFunction<Input, Output>;
/**
* Create batch query with StandardSchema validation
* @param schema - StandardSchema validator (Valibot, Zod, etc.)
* @param fn - Batch implementation returning a resolver function
* @returns RemoteQueryFunction callable from client
* @since 2.35
*/
function batch<Schema extends StandardSchemaV1, Output>(
schema: Schema,
fn: (args: StandardSchemaV1.InferOutput<Schema>[]) =>
(arg: StandardSchemaV1.InferOutput<Schema>, idx: number) => Output |
Promise<(arg: StandardSchemaV1.InferOutput<Schema>, idx: number) => Output>
): RemoteQueryFunction<StandardSchemaV1.InferInput<Schema>, Output>;
}Usage:
// Server
import { query } from '$app/server';
export const getUsers = query.batch('unchecked', async (userIds: string[]) => {
// Fetch all users in one database query
const users = await db.users.findMany(userIds);
// Return resolver function
return (userId, idx) => users[idx];
});
// Client - automatically batches concurrent calls
const [user1, user2, user3] = await Promise.all([
getUsers('id-1'),
getUsers('id-2'),
getUsers('id-3')
]);
// Only one server request is made!Create form objects that can be spread onto <form> elements with progressive enhancement.
// From '$app/server'
/**
* Create remote form action callable from client
* @param fn - Form implementation (no parameters)
* @returns RemoteForm object that can be spread onto form element
* @since 2.27
*/
function form<Output>(
fn: () => Output | Promise<Output>
): RemoteForm<void, Output>;
/**
* Create remote form with input validation and issue reporting
* @param validate - 'unchecked' to skip validation
* @param fn - Form implementation receiving validated data and issue callback
* @returns RemoteForm object that can be spread onto form element
* @since 2.27
*/
function form<Input extends RemoteFormInput, Output>(
validate: 'unchecked',
fn: (data: Input, issue: InvalidField<Input>) => Output | Promise<Output>
): RemoteForm<Input, Output>;
/**
* Create remote form with StandardSchema validation
* @param validate - StandardSchema validator (Valibot, Zod, etc.)
* @param fn - Form implementation receiving validated data and issue callback
* @returns RemoteForm object that can be spread onto form element
* @since 2.27
*/
function form<Schema extends StandardSchemaV1<RemoteFormInput, Record<string, any>>, Output>(
validate: Schema,
fn: (
data: StandardSchemaV1.InferOutput<Schema>,
issue: InvalidField<StandardSchemaV1.InferInput<Schema>>
) => Output | Promise<Output>
): RemoteForm<StandardSchemaV1.InferInput<Schema>, Output>;
interface RemoteFormInput {
[key: string]: string | number | boolean | File | string[] | File[] | RemoteFormInput;
}
type RemoteForm<Input extends RemoteFormInput | void, Output> = {
/** HTTP method for the form */
method: 'POST';
/** The URL to send the form to */
action: string;
/**
* Enhance the form submission with custom logic
*/
enhance(
callback: (opts: {
form: HTMLFormElement;
data: Input;
submit: () => Promise<void> & {
updates(...queries: Array<RemoteQuery<any> | RemoteQueryOverride>): Promise<void>;
};
}) => void | Promise<void>
): RemoteForm<Input, Output>;
/**
* Create an instance for a specific ID (useful in loops)
*/
for(id: string | number): Omit<RemoteForm<Input, Output>, 'for'>;
/**
* Add preflight client-side validation
*/
preflight(schema: StandardSchemaV1<Input, any>): RemoteForm<Input, Output>;
/**
* Validate the form contents programmatically
*/
validate(options?: {
includeUntouched?: boolean;
preflightOnly?: boolean;
}): Promise<void>;
/** The result of the form submission */
readonly result: Output | undefined;
/** The number of pending submissions */
readonly pending: number;
/** Access form fields using object notation */
fields: RemoteFormFields<Input>;
/** Spread this onto a <button> or <input type="submit"> */
buttonProps: {
type: 'submit';
formmethod: 'POST';
formaction: string;
onclick: (event: Event) => void;
enhance(callback: (opts: {
form: HTMLFormElement;
data: Input;
submit: () => Promise<void> & {
updates(...queries: Array<RemoteQuery<any> | RemoteQueryOverride>): Promise<void>;
};
}) => void | Promise<void>): typeof buttonProps;
readonly pending: number;
};
};
/**
* Proxy-based form field accessor providing type-safe field access
*/
type RemoteFormFields<T> = /* Complex recursive type - see full types */;
/**
* Form field accessor with value(), set(), issues(), and as() methods
*/
type RemoteFormField<Value> = {
/** Get the values that will be submitted */
value(): Value;
/** Set the values that will be submitted */
set(input: Value): Value;
/** Get validation issues for this field */
issues(): RemoteFormIssue[] | undefined;
/**
* Returns props that can be spread onto an input element
* with correct type, aria-invalid, and value/checked getters/setters
*/
as<T extends RemoteFormFieldType<Value>>(...args: AsArgs<T, Value>): InputElementProps<T>;
};
interface RemoteFormIssue {
message: string;
path: Array<string | number>;
}
/**
* Proxy object for creating field-specific validation issues
* Access nested fields: issue.fieldName('message') or issue.nested.field('message')
* Call to throw validation error: invalid(issue.foo('error'), issue.bar('error'))
*/
type InvalidField<T> = /* Recursive proxy type mirroring input structure */;Usage:
// Server: src/lib/server/forms.ts
import { form, getRequestEvent, invalid } from '$app/server';
import * as v from 'valibot';
// Form with StandardSchema validation
const ProfileSchema = v.object({
name: v.pipe(v.string(), v.minLength(2)),
email: v.pipe(v.string(), v.email()),
age: v.optional(v.pipe(v.number(), v.minValue(18)))
});
export const updateProfile = form(ProfileSchema, async (data, issue) => {
const event = getRequestEvent();
// Schema validation happens automatically
// Use issue() for additional custom validation
if (await db.users.emailExists(data.email)) {
// Create field-specific issue
invalid(issue.email('Email already in use'));
}
await db.users.update(event.locals.user.id, data);
return { success: true };
});
// Client: +page.svelte
import { updateProfile } from '$lib/server/forms';
// Spread form props onto <form> element
<form {...updateProfile}>
<!-- Access fields via fields property -->
<input {...updateProfile.fields.name.as('text')} />
{#if updateProfile.fields.name.issues()}
<p>{updateProfile.fields.name.issues()[0].message}</p>
{/if}
<input {...updateProfile.fields.email.as('email')} />
{#if updateProfile.fields.email.issues()}
<p>{updateProfile.fields.email.issues()[0].message}</p>
{/if}
<input {...updateProfile.fields.age.as('number')} />
<button type="submit">Update Profile</button>
</form>
<!-- Or use buttonProps for submit button -->
<form {...updateProfile}>
<!-- fields -->
<button {...updateProfile.buttonProps}>Submit</button>
</form>
<!-- Access result and pending state -->
{#if updateProfile.result}
<p>Success!</p>
{/if}
{#if updateProfile.pending > 0}
<p>Submitting...</p>
{/if}
<!-- Use enhance() for custom submission logic -->
<form {...updateProfile.enhance(async ({ data, submit }) => {
console.log('Submitting:', data);
await submit();
console.log('Done!');
})}>
<!-- fields -->
</form>
<!-- Use for() in loops for separate instances -->
{#each items as item}
{@const itemForm = updateItem.for(item.id)}
<form {...itemForm}>
<!-- fields -->
{#if itemForm.result?.success}
<p>Saved!</p>
{/if}
</form>
{/each}
<!-- Programmatic validation -->
<button onclick={() => updateProfile.validate()}>Validate</button>
<button onclick={() => updateProfile.validate({ includeUntouched: true })}>
Validate All
</button>Create functions that execute during build for static site generation.
// From '$app/server'
/**
* Create remote prerender function callable during build
* @param fn - Prerender implementation (no parameters)
* @param options - Prerender options with inputs and dynamic flag
* @returns RemotePrerenderFunction
* @since 2.27
*/
function prerender<Output>(
fn: () => Output | Promise<Output>,
options?: {
inputs?: void[] | (() => void[] | Promise<void[]>);
dynamic?: boolean;
}
): RemotePrerenderFunction<void, Output>;
/**
* Create remote prerender function with input validation
* @param validate - 'unchecked' to skip validation
* @param fn - Prerender implementation receiving validated input
* @param options - Prerender options with inputs generator and dynamic flag
* @returns RemotePrerenderFunction
* @since 2.27
*/
function prerender<Input, Output>(
validate: 'unchecked',
fn: (input: Input) => Output | Promise<Output>,
options?: {
inputs?: Input[] | (() => Input[] | Promise<Input[]>);
dynamic?: boolean;
}
): RemotePrerenderFunction<Input, Output>;
/**
* Create remote prerender function with StandardSchema validation
* @param schema - StandardSchema validator (Valibot, Zod, etc.)
* @param fn - Prerender implementation receiving validated input
* @param options - Prerender options with inputs generator and dynamic flag
* @returns RemotePrerenderFunction
* @since 2.27
*/
function prerender<Schema extends StandardSchemaV1, Output>(
schema: Schema,
fn: (input: StandardSchemaV1.InferOutput<Schema>) => Output | Promise<Output>,
options?: {
inputs?: StandardSchemaV1.InferInput<Schema>[] |
(() => StandardSchemaV1.InferInput<Schema>[] | Promise<StandardSchemaV1.InferInput<Schema>[]>);
dynamic?: boolean;
}
): RemotePrerenderFunction<StandardSchemaV1.InferInput<Schema>, Output>;
type RemotePrerenderFunction<Input, Output> = (input: Input) => RemoteResource<Output>;
type RemoteResource<T> = Promise<Awaited<T>> & {
readonly error: any;
readonly loading: boolean;
readonly current: Awaited<T> | undefined;
readonly ready: boolean;
};Usage:
// Server
import { prerender } from '$app/server';
import * as v from 'valibot';
const SlugSchema = v.string();
export const getPost = prerender(
SlugSchema,
async (slug) => {
return await db.posts.findBySlug(slug);
},
{
// Provide inputs for prerendering at build time
inputs: async () => {
const posts = await db.posts.getAll();
return posts.map(p => p.slug);
}
}
);
// Used in load function - prerendered during build
export async function load() {
const post = await getPost('hello-world');
return { post };
}Throw validation errors imperatively using the invalid() function.
// From '@sveltejs/kit'
/**
* Throw a validation error to imperatively fail form validation
* @param issues - Validation issues created via issue parameter
* @throws ValidationError
* @since 2.47.3
*/
function invalid(...issues: Array<StandardSchemaV1.Issue | string>): never;
/**
* Check if an error is a ValidationError
* @param e - The error to check
* @returns Type predicate for ValidationError
* @since 2.47.3
*/
function isValidationError(e: unknown): e is ValidationError;
interface ValidationError {
issues: StandardSchemaV1.Issue[];
}Usage:
import { form, invalid } from '$app/server';
export const updateUser = form('unchecked', async (data, issue) => {
// Create issues and throw
if (!data.email) {
invalid(issue.email('Email is required'));
}
// Multiple issues
if (!data.name && !data.email) {
invalid(
issue.name('Name is required'),
issue.email('Email is required')
);
}
// Nested field issues
if (!data.address?.city) {
invalid(issue.address.city('City is required'));
}
return { success: true };
});$app/server (server-only)getRequestEvent() from $app/server if neededenhance() to customize form submission behavior and provide optimistic updates.updates() on commands and form submissions to update queries for optimistic UIas()