or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

examples

edge-cases.mdreal-world-scenarios.md
index.md
tile.json

remote-functions.mddocs/reference/

Remote Functions

Create type-safe RPC-style functions for client-server communication with commands, queries, forms, and prerender functions.

Capabilities

Remote Commands

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 executions

Remote Queries

Create 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 reactive

Batch Queries

Create 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!

Remote Forms

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>

Remote Prerender Functions

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 };
}

Validation Error Handling

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 };
});

Notes

  • Experimental feature - API may change
  • Import functions from $app/server (server-only)
  • Remote functions provide type-safe RPC between client and server
  • Commands: For mutations (write operations) with pending state tracking
  • Queries: For reads with reactive state (current, loading, error, ready)
  • Forms: For form submissions with field-level validation and progressive enhancement
  • Prerender: For build-time data fetching with static site generation
  • Validation options:
    • 'unchecked': Skip validation (use with caution)
    • StandardSchema: Valibot, Zod, ArkType, or any library implementing StandardSchema
  • All remote functions automatically serialize/deserialize data
  • Batch queries automatically combine concurrent calls into single request
  • Functions do NOT receive RequestEvent as a parameter - use getRequestEvent() from $app/server if needed
  • StandardSchema validation errors in forms are automatically converted to field-level issues
  • Use enhance() to customize form submission behavior and provide optimistic updates
  • Use .updates() on commands and form submissions to update queries for optimistic UI
  • Form fields provide reactive access to values, issues, and input element props via as()