
Sveltekit Remote Functions
- 366 installs
- 92 repo stars
- Updated April 29, 2026
- spences10/svelte-skills-kit
sveltekit-remote-functions is an agent skill that implements typed SvelteKit remote functions using command(), query(), and form() in *.remote.ts files so developers can call server logic from components without custom f
About
sveltekit-remote-functions is a Svelte-focused agent skill from spences10/svelte-skills-kit that documents SvelteKit's experimental remote function patterns for type-safe server calls. Install with `npx skills add spences10/svelte-skills-kit --skill sveltekit-remote-functions`. The skill specifies *.remote.ts naming, maps one-time actions to command(), repeated reads to query(), and forms to form(), with Valibot or Zod validation via StandardSchemaV1 and JSON-serializable arguments. It notes getRequestEvent() for cookies and headers, query().refresh() for no-flicker updates, and restrictions such as avoiding src/lib/server placement. Reach for sveltekit-remote-functions when building SvelteKit features that need server execution without maintaining parallel REST endpoints and client wrappers.
- SvelteKit-native remote function pattern
- Typed server calls from components
- Less boilerplate than manual API routes
- Covers forms and data mutations
- Fits full-stack SvelteKit SaaS apps
Sveltekit Remote Functions by the numbers
- 366 all-time installs (skills.sh)
- +5 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #681 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Jul 31, 2026 (Skillselion catalog sync)
npx skills add https://github.com/spences10/svelte-skills-kit --skill sveltekit-remote-functionsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 366 |
|---|---|
| repo stars | ★ 92 |
| Last updated | April 29, 2026 |
| Repository | spences10/svelte-skills-kit ↗ |
How do you call SvelteKit server logic from components?
Implement typed SvelteKit remote functions that call server logic from components without hand-rolling fetch routes and client API glue.
Who is it for?
SvelteKit developers adopting experimental remote functions who want typed server calls without duplicate API routes and fetch glue.
Skip if: Skip sveltekit-remote-functions when the project uses only classic +page.server.ts load functions and form actions without remoteFunctions enabled.
When should I use this skill?
A developer asks about SvelteKit command(), query(), form(), .remote.ts files, or type-safe client-to-server calls in SvelteKit.
What you get
*.remote.ts modules with command(), query(), and form() handlers, Valibot or Zod schemas, and client call patterns.
- *.remote.ts handler module
- schema-validated server functions
- component call examples
By the numbers
- Covers three remote function primitives: command(), query(), and form()
- Ships inside spences10/svelte-skills-kit with nine companion Svelte skills
Files
SvelteKit Remote Functions
Current Status
Remote functions are experimental in SvelteKit 2.58. Enable them in svelte.config.js:
export default {
kit: { experimental: { remoteFunctions: true } },
compilerOptions: { experimental: { async: true } } // only for await in components
};Quick Start
File naming: export remote functions from *.remote.ts or *.remote.js. Remote files can live anywhere under src except src/lib/server.
Which function?
- Dynamic reads →
query() - Progressive forms →
form() - Event-handler mutations →
command() - Build-time/static reads →
prerender()
Example
// posts.remote.ts
import { command, query, requested } from '$app/server';
import * as v from 'valibot';
export const getPosts = query(v.object({ tag: v.optional(v.string()) }), async (filter) => {
return db.posts.find(filter);
});
export const createPost = command(v.object({ title: v.string() }), async (data) => {
await db.posts.create(data);
for (const { query } of requested(getPosts, 5)) {
void query.refresh();
}
});Client:
<script lang="ts">
import { createPost, getPosts } from './posts.remote';
const posts = $derived(await getPosts({ tag: 'svelte' }));
</script>
<button onclick={() => createPost({ title: 'New' }).updates(getPosts)}>
Create
</button>Current Rules
- Remote functions always run on the server, even when called from the browser.
- Args/returns use
devalue; avoid functions, class instances, symbols, circular refs, andRegExp. - Validate exposed inputs with Standard Schema (
valibot,zod,arktype, etc.) or use.unchecked/'unchecked'deliberately. query.batch()batches calls from the same macrotask to solve n+1 reads.form().enhance()submit()returnstruewhen submission is valid/successful andfalsefor validation failures..updates()is client-requested; server handlers must opt in withrequested(queryFn, limit).requested()now yields{ arg, query }; callquery.refresh()/query.set(...)on the bound instance.limitis required forrequested()to cap client-controlled refresh requests.- Inside command/form handlers, use
void query.refresh()/void query.set(value); SvelteKit awaits and serializes the updates. - Prefer
form()overcommand()where progressive enhancement matters. - Use
prerender()for data that changes at most once per deployment. - Last verified: SvelteKit 2.58.0, 2026-04-24
Reference Files
- references/remote-functions.md - Current patterns, examples, and gotchas
<!-- PROGRESSIVE DISCLOSURE GUIDELINES:
- Keep this file ~50 lines total (max ~150 lines)
- Use 1-2 code blocks only (recommend 1)
- Keep description <200 chars for Level 1 efficiency
- Move detailed docs to references/ for Level 3 loading
- This is Level 2 - quick reference ONLY, not a manual
LLM WORKFLOW (when editing this file): 1. Write/edit SKILL.md 2. Format (if formatter available) 3. Run: npx skills add . --list 4. If the skill is not discovered, check SKILL.md frontmatter formatting 5. Validate again to confirm -->
SvelteKit Remote Functions
SvelteKit remote functions guidance for query(), form(), command(), and prerender() in .remote.ts / .remote.js files.
Structure
SKILL.md- Main skill instructionsreferences/remote-functions.md- Current patterns, examples, and gotchas
Usage
This skill is automatically discovered by compatible agents when relevant to the task.
SvelteKit Remote Functions
Verified against SvelteKit 2.58.0 on 2026-04-24.
Remote functions are exported from .remote.ts/.remote.js files and can be called anywhere in the app. They always execute on the server. On the client, SvelteKit transforms calls into generated fetch requests.
Remote functions are still experimental. Enable them explicitly:
// svelte.config.js
export default {
kit: {
experimental: {
remoteFunctions: true
}
},
compilerOptions: {
experimental: {
async: true // optional; needed for `await` in components
}
}
};Remote files can live anywhere under src except src/lib/server.
Function Types
| Function | Use for | Notes |
|---|---|---|
query() | Dynamic server reads | Cached while rendered; supports refresh and batching |
form() | Progressive form mutations | Works without JS; supports fields, validation, enhance |
command() | Imperative/event-handler mutations | Cannot be called during render |
prerender() | Static/build-time reads | For data that changes at most once per deployment |
query()
Use query() for dynamic server reads.
import { query } from '$app/server';
import * as v from 'valibot';
export const getPost = query(v.string(), async (slug) => {
return db.posts.findBySlug(slug);
});In components with async enabled:
<script lang="ts">
import { getPost } from './posts.remote';
let { slug } = $props();
const post = $derived(await getPost(slug));
</script>
<h1>{post.title}</h1>Without async in components, use {#await}:
{#await getPost(slug) then post}
<h1>{post.title}</h1>
{/await}Query refresh
<button onclick={() => getPost(slug).refresh()}>
Refresh
</button>Queries are cached while on the page. Calling getPost(slug) repeatedly returns the same active query instance for that argument.
query.batch()
Use query.batch() for n+1 reads. Calls made in the same macrotask are grouped into one server request. The handler receives all inputs and returns a resolver.
import { query } from '$app/server';
import * as v from 'valibot';
export const getWeather = query.batch(v.string(), async (cityIds) => {
const rows = await db.weather.findMany(cityIds);
const byId = new Map(rows.map((row) => [row.city_id, row]));
return (cityId) => byId.get(cityId);
});form()
Use form() for mutations that should gracefully degrade when JavaScript is disabled.
import { form } from '$app/server';
import * as v from 'valibot';
export const createPost = form(
v.object({
title: v.pipe(v.string(), v.nonEmpty()),
content: v.pipe(v.string(), v.nonEmpty())
}),
async ({ title, content }) => {
await db.posts.create({ title, content });
}
);Use .fields.<name>.as(type) to bind typed inputs:
<form {...createPost}>
<input {...createPost.fields.title.as('text')} />
<textarea {...createPost.fields.content.as('text')}></textarea>
<button>Publish</button>
</form>.as(...) supplies name, type-specific attributes, validation state, and repopulation behavior after failed validation.
Sensitive fields
Prefix sensitive field names with _ to prevent repopulation after invalid non-enhanced submissions:
<input {...login.fields._password.as('password')} />Use this for passwords, credit card numbers, tokens, and similar secrets.
Validation
If schema validation fails, the handler does not run. Issues are exposed through field helpers:
{#each createPost.fields.title.issues() as issue}
<p class="error">{issue.message}</p>
{/each}Programmatic validation inside a handler uses invalid from @sveltejs/kit:
import { invalid } from '@sveltejs/kit';
import { form } from '$app/server';
export const login = form(schema, async (data, issue) => {
if (!(await auth.check(data))) {
invalid(issue.email('Invalid credentials'));
}
});Client-side preflight validation can prevent invalid submissions before they hit the server:
<form {...createPost.preflight(schema)}>
<!-- fields -->
</form>enhance()
enhance customizes JS-enabled submission. Since SvelteKit 2.57, submit() returns a boolean: true means the submission completed without validation failure; false means validation failed and the handler did not run.
<form {...createPost.enhance(async ({ form, submit }) => {
try {
if (await submit()) {
form.reset();
showToast('Published');
}
} catch (error) {
showToast('Something went wrong');
}
})}>
<!-- fields -->
</form>With enhance, the form is not automatically reset. Call form.reset() when you want to clear inputs.
Multiple form instances
Use .for(id) when rendering the same form many times and each instance needs isolated state.
{#each todos as todo}
{@const modify = modifyTodo.for(todo.id)}
<form {...modify}>
<input {...modify.fields.description.as('text', todo.description)} />
<button disabled={!!modify.pending}>Save</button>
</form>
{/each}Multiple submit buttons
Model the clicked submit button as a schema field and bind buttons with .as('submit', value).
<form {...loginOrRegister}>
<input {...loginOrRegister.fields.email.as('email')} />
<input {...loginOrRegister.fields._password.as('password')} />
<button {...loginOrRegister.fields.action.as('submit', 'login')}>Login</button>
<button {...loginOrRegister.fields.action.as('submit', 'register')}>Register</button>
</form>File uploads
Remote form() supports file uploads. Since SvelteKit 2.49, enhanced forms use a streaming binary upload format so server-side form handlers can access form data before large files finish uploading. SvelteKit 2.52 tightened file metadata and offset-table validation.
Practical rules:
- Keep file schemas explicit and validate type/size server-side.
- Do not trust client-provided file metadata.
- Prefer streaming processing/storage for large files.
- Handle upload errors as normal form validation or request errors.
command()
Use command() for mutations from event handlers or other imperative code. Prefer form() when progressive enhancement matters.
import { command } from '$app/server';
import * as v from 'valibot';
export const addLike = command(v.string(), async (postId) => {
await db.posts.incrementLikes(postId);
});<button onclick={() => addLike(post.id).updates(getLikes(post.id))}>
Like
</button>Commands cannot be called during render.
Single-flight mutations
Use single-flight mutations to refresh query data in the same request as a form() submission or command() invocation. This avoids an extra round-trip and prevents stale UI.
There are two sides:
1. Client requests updates with .updates(...) 2. Server accepts updates with requested(queryFn, limit)
Client: .updates()
.updates() accepts query functions, query instances, and optimistic overrides.
// Refresh all active getPosts instances
await createPost(data).updates(getPosts);
// Refresh one instance
await addLike(post.id).updates(getLikes(post.id));
// Optimistic update
await addLike(post.id).updates(getLikes(post.id).withOverride((n) => n + 1));Inside enhanced forms:
<form {...createPost.enhance(async ({ submit }) => {
await submit().updates(getPosts);
})}>
<!-- fields -->
</form>Server: requested(queryFn, limit)
requested is required for client-requested refreshes. The limit argument is required as of SvelteKit 2.58 because the list is client-controlled and each item can cause validation and data fetching.
import { command, query, requested } from '$app/server';
export const getPosts = query(filterSchema, async (filter) => {
return db.posts.find(filter);
});
export const createPost = command(createSchema, async (data) => {
await db.posts.create(data);
for (const { arg, query } of requested(getPosts, 5)) {
// arg is the validated/transformed argument
// query is bound to the original client cache key
void query.refresh();
}
});Important current behavior:
requested(queryFn, limit)yields{ arg, query }objects, not raw args.- Use the yielded
queryinstance for.refresh()/.set(...); it is bound to
the original client cache key even if validation transformed arg.
limitis required. Choose the maximum refreshes you are willing to process
per mutation. Infinity is possible but usually a DoS footgun.
- If parsing one requested argument fails, that query errors, but the whole
mutation does not fail.
Shorthand:
await requested(getPosts, 5).refreshAll();This is equivalent to looping and calling void query.refresh() for each requested query.
Server-driven updates
If the server already knows exactly what changed, call .refresh() or .set() inside the handler without waiting for a client request.
export const updatePost = command(updateSchema, async ({ id, title }) => {
const post = await db.posts.update(id, { title });
void getPost(id).set(post); // send known value back
void getPosts().refresh(); // refetch list in same response
});Use void rather than await; SvelteKit awaits and serializes these updates for the response.
prerender()
Use prerender() for data that changes at most once per deployment. Results are computed during prerendering and can be cached on a CDN.
import { prerender } from '$app/server';
import * as v from 'valibot';
export const getPosts = prerender(async () => {
return db.posts.allPublished();
});
export const getPost = prerender(v.string(), async (slug) => {
return db.posts.findBySlug(slug);
});SvelteKit's crawler automatically saves calls it discovers while prerendering. Use the inputs option when you need to enumerate values explicitly.
getRequestEvent()
Use getRequestEvent() inside remote functions for cookies, headers, locals, and other request context.
import { getRequestEvent, query } from '$app/server';
export const getMe = query(async () => {
const event = getRequestEvent();
return event.locals.user;
});Serialization
Remote arguments and return values are serialized with devalue.
Can serialize:
- primitives, arrays, plain objects
Date,Map,Set, typed arrays
Avoid:
- functions
- class instances without serialization support
- symbols
- circular references
RegExpas remote function arguments
Query cache keys are based on serialized arguments. Object keys are normalized, so { limit: 10, offset: 20 } and { offset: 20, limit: 10 } refer to the same query cache entry.
Common Gotchas
- Remote functions are HTTP endpoints; validate every exposed input.
form()invalid schema submissions do not run the handler.command()does not auto-refresh anything; use.updates()or server-driven
query updates.
form()auto-invalidates broadly after successful submissions unless you use
more targeted single-flight updates.
requested()must name each query function the server is willing to refresh;
this protects bundle size and avoids unbounded client-controlled work.
- Use
form()instead ofcommand()when no-JS behavior matters. - Do not export shared schemas from
.remote.ts; put them in a shared module or
component module script.
Related skills
How it compares
Choose sveltekit-remote-functions for experimental remoteFunctions; use sveltekit-data-flow skills when sticking to load functions and classic form actions.
FAQ
What file naming does sveltekit-remote-functions require?
sveltekit-remote-functions requires remote function modules to use the *.remote.ts suffix. Files can live anywhere under src except src/lib/server, and exported functions become type-safe client wrappers that invoke server endpoints automatically.
Which SvelteKit remote function type should I use?
sveltekit-remote-functions maps one-time mutations to command(), repeated reads to query(), and HTML form submissions to form(). Picking the wrong primitive leads to extra round trips or missing validation hooks the skill documents.