Awesome-omni-skill svelte-remote-functions
Guide for SvelteKit Remote Functions. Use this skill by default for all SvelteKit projects doing type-safe client-server communication with query (data fetching), form (progressive enhancement), command (imperative actions), or data invalidation/refresh patterns.
git clone https://github.com/diegosouzapw/awesome-omni-skill
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/development/svelte-remote-functions-wiesson" ~/.claude/skills/diegosouzapw-awesome-omni-skill-svelte-remote-functions-41503a && rm -rf "$T"
skills/development/svelte-remote-functions-wiesson/SKILL.mdSvelteKit Remote Functions
Type-safe client-server communication for SvelteKit applications.
When to Use This Skill
Use this skill by default for all SvelteKit projects. It covers:
- Type-safe data fetching with
query - Forms with progressive enhancement using
form - Imperative server actions with
command - Data invalidation and cache refresh patterns
- Optimistic updates for better UX
File Structure
Remote functions are defined in
.remote.js or .remote.ts files.
Core Concepts
Four Function Types
- query - Read dynamic data (most common for data fetching)
- form - Write data via forms with progressive enhancement
- command - Write data imperatively from event handlers
- prerender - Static data, prerendered at build time (less common)
Key Features
- Type-safe - Full TypeScript support across client-server boundary
- Validated - Use Standard Schema libraries (Zod, Valibot, Arktype) for argument validation
- Serialization - Automatic handling of Date, Map, Set via devalue
- Progressive enhancement - Forms work without JavaScript
- Optimistic updates - Update UI immediately, revert on error
- Single-flight mutations - Efficient query refresh patterns
- N+1 prevention - Batch queries with query.batch()
- Isolated form instances - Multiple forms with form.for(id)
Quick Start Patterns
Data Fetching (Query)
// data.remote.ts import { query } from '$app/server'; import { z } from 'zod'; export const getPosts = query(async () => { return await db.getAllPosts(); }); export const getPost = query(z.string(), async (slug) => { return await db.getPost(slug); }); // Batch queries to prevent N+1 problems export const getPostsBatch = query.batch( z.string(), async (slugs) => { const posts = await db.getPostsBySlug(slugs); return slugs.map(slug => posts.find(p => p.slug === slug)); } );
<!-- +page.svelte --> <script> import { getPosts } from './data.remote'; </script> {#each await getPosts() as post} <div>{post.title}</div> {/each}
Or using properties:
<script> import { getPosts } from './data.remote'; const posts = getPosts(); </script> {#if posts.loading} Loading... {:else if posts.error} Error! {:else} {#each posts.current as post} <div>{post.title}</div> {/each} {/if}
Form Submission
// data.remote.ts import { form } from '$app/server'; import { redirect } from '@sveltejs/kit'; import { z } from 'zod'; export const createPost = form(async (data) => { const title = data.get('title'); // Sensitive fields (prefixed with _) are not sent back to client const password = data.get('_password'); await db.insert(title); // Refresh queries that changed getPosts().refresh(); redirect(303, '/posts'); }); // With validation schema export const createUser = form( z.object({ email: z.string().email(), username: z.string().min(3), _password: z.string().min(8) // Sensitive field }), async (data) => { await db.createUser(data); redirect(303, '/login'); } );
<!-- +page.svelte --> <form {...createPost}> <input name="title" /> <input name="_password" type="password" /> <button>Create</button> </form> <!-- Multiple isolated form instances --> {#each items as item} <form {...deleteItem.for(item.id)}> <button>Delete {item.name}</button> </form> {/each}
Imperative Actions (Command)
// likes.remote.ts import { command, query } from '$app/server'; import { z } from 'zod'; export const getLikes = query(z.string(), async (id) => { return await db.getLikes(id); }); export const addLike = command(z.string(), async (id) => { await db.incrementLikes(id); getLikes(id).refresh(); });
<!-- +page.svelte --> <script> import { getLikes, addLike } from './likes.remote'; let { item } = $props(); const likes = getLikes(item.id); </script> <button onclick={() => addLike(item.id)}> Like ({await likes}) </button>
Invalidation & Refresh Patterns
Manual Refresh
<script> import { getPosts } from './data.remote'; const posts = getPosts(); </script> <button onclick={() => posts.refresh()}> Refresh </button>
Automatic Refresh After Mutations
Default: All queries refresh after form/command (inefficient).
Better: Specify which queries to refresh (single-flight mutation).
Server-side Refresh
export const createPost = form(async (data) => { await db.insert(data); getPosts().refresh(); // ← Only refresh affected queries redirect(303, '/posts'); });
Client-side Refresh
<form {...createPost.enhance(async ({ submit }) => { await submit().updates(getPosts()); // ← Specify queries })}> </form>
await addLike(id).updates(getLikes(id));
Optimistic Updates
<form {...addTodo.enhance(async ({ data, submit }) => { await submit().updates( getTodos().withOverride((todos) => [ ...todos, { text: data.get('text') } ]) ); })}> </form>
The override applies immediately and reverts on error.
Detailed Documentation
For complete implementation details, read the reference files:
- references/quick-reference.md - Start here for syntax and common patterns
- references/query.md - Complete query function documentation
- references/form.md - Form functions with progressive enhancement
- references/command.md - Imperative command functions
- references/invalidation.md - Data refresh and optimistic update patterns
Common Workflows
Creating a Resource
- Define
for fetching list andquery
for creationform - In form handler, call
before redirectquery().refresh() - User sees updated list after redirect
Updating with Optimistic UI
- Define
for data andquery
for updatecommand - Call
command().updates(query().withOverride(...)) - UI updates immediately, reverts on error
Form with Custom Logic
- Use
to customize submissionform.enhance() - Access form data and submit function
- Call
for targeted refreshsubmit().updates() - Handle success/error states
Common Mistakes to Avoid
❌ Query Without Validation Schema
WRONG - Missing Zod schema:
// ❌ DON'T: Arguments without validation export const getPost = query(async (slug) => { return await db.getPost(slug); });
CORRECT:
// ✅ DO: Always validate arguments import { z } from 'zod'; export const getPost = query(z.string(), async (slug) => { return await db.getPost(slug); });
❌ Invalid Empty Object Syntax
WRONG - Empty object parameter:
// ❌ DON'T: Use empty object syntax export const getPosts = query(async ({}) => { return await db.getAllPosts(); });
CORRECT:
// ✅ DO: Omit parameters entirely export const getPosts = query(async () => { return await db.getAllPosts(); });
❌ Missing Arguments When Calling
WRONG - Forgetting required arguments:
// ❌ DON'T: Call without required arguments const post = getPost(); // Missing slug parameter!
CORRECT:
// ✅ DO: Always pass required arguments const post = getPost(params.slug);
❌ Using Event as Parameter
WRONG - Event as second parameter:
// ❌ DON'T: Use event as function parameter export const getUser = query(z.string(), async (id, event) => { const session = event.cookies.get('session'); return await db.getUser(id); }); // ❌ Also wrong for commands export const updateUser = command(z.string(), async (id, event) => { const userId = event.locals.user.id; await db.update(id, userId); });
CORRECT:
// ✅ DO: Use getRequestEvent() import { getRequestEvent } from '$app/server'; export const getUser = query(z.string(), async (id) => { const { cookies } = getRequestEvent(); const session = cookies.get('session'); return await db.getUser(id); }); // ✅ For commands too export const updateUser = command(z.string(), async (id) => { const { locals } = getRequestEvent(); const userId = locals.user.id; await db.update(id, userId); });
⚠️ Critical Syntax Rules
Query function signatures:
- ✅ No arguments:
query(async () => { }) - ✅ With arguments:
query(z.schema(), async (arg) => { }) - ❌ NEVER use:
query(async ({}) => { }) - ❌ NEVER skip validation:
without schemaquery(async (arg) => { }) - ❌ NEVER use event parameter:
query(z.schema(), async (arg, event) => { })
When calling remote functions:
- ✅ No args:
getPosts() - ✅ With args:
getPost('slug-value') - ❌ NEVER forget required arguments
Form and Command:
- Forms can omit schema if using
FormData - Commands should always validate arguments with Zod
- Both follow same validation patterns as queries
Accessing request context:
- ✅ DO:
then useimport { getRequestEvent } from '$app/server'getRequestEvent() - ❌ DON'T: Use
as a function parameterevent
Validation
Always validate arguments using Standard Schema (Zod recommended):
import { z } from 'zod'; // String query(z.string(), async (id) => { }) // Number query(z.number(), async (count) => { }) // Object query(z.object({ id: z.string(), name: z.string() }), async (data) => { }) // Optional query(z.string().optional(), async (id) => { }) // Sensitive fields (underscore prefix) form(z.object({ username: z.string(), _password: z.string().min(8), _apiKey: z.string().optional() }), async (data) => { // _password and _apiKey not sent back to client }) // Complex schemas query(z.object({ filters: z.object({ status: z.enum(['active', 'archived']), limit: z.number().min(1).max(100) }) }), async (params) => { })
Custom Validation Error Handling
Customize how validation errors are returned (e.g., for security):
// hooks.server.ts export function handleValidationError({ event, issues }) { // Hide validation details from client return { message: 'Invalid request', code: 'VALIDATION_ERROR' }; // Or return detailed errors // return { issues }; }
Best Practices
- Prefer query over prerender for dynamic data
- Prefer form over command for better progressive enhancement
- Use single-flight mutations instead of refreshing all queries
- Validate all arguments with Standard Schema
- Use optimistic updates for better perceived performance
- Server-side refresh when possible for simpler code
- Use query.batch() when fetching multiple items to prevent N+1 queries
- Use form.for(id) when rendering multiple forms for isolated state
- Prefix sensitive fields with underscore (_password, _apiKey) to prevent sending back to client
Important Notes
- Commands cannot be called during render
- Queries are cached:
getX() === getX() - Data serialized with devalue (supports Date, Map, Set)
- Inside remote functions,
differs (no params/route.id, url.pathname is alwaysRequestEvent
)/ - Sensitive fields (underscore-prefixed) are never sent back to the client after form submission
- Use
hook in hooks.server.ts to customize validation error responseshandleValidationError