Introduction
Building modern web applications means constantly moving data between client and server. We've all felt the friction: defining API routes, writing fetch calls, managing error states, and keeping TypeScript types in sync across the boundary. SvelteKit offers a better way—remote functions: a practical Svelte & SvelteKit guide that eliminates boilerplate and brings type safety to your server calls. In this article, we'll walk through query, form and command helpers that let you run code on the server and call it directly from your components, replacing the need for many explicit API routes.
What are remote functions?
Remote functions are server-side procedures you can invoke directly from Svelte components with full TypeScript support. Rather than manually wiring HTTP endpoints, you write a function on the server, and SvelteKit's helpers automatically create the glue between client and server. The result is less code, stronger types, and a developer experience that feels almost like calling local functions.
Think of them as RPC (remote procedure call) but tailored for SvelteKit's architecture. They're not a new concept, but they've matured significantly and are now a practical foundation for many applications beyond simple CRUD operations.
query: Read operations with caching
The query helper is for read-only operations. It's designed to fetch data from the server with built-in request deduplication and browser caching. When multiple components request the same data in parallel, the query is executed only once.
Real-world scenario: You're building an e-commerce dashboard. Multiple widgets need to fetch the current user's subscription status. Instead of making separate API calls, query ensures a single request reaches your database.
// src/lib/queries.server.ts
import { query } from 'sveltekit-query';
export const getUserSubscription = query(async (userId: string) => {
const response = await db.subscriptions.findUnique({
where: { userId },
});
return response;
});
// src/routes/dashboard/+page.svelte
<script>
import { getUserSubscription } from '$lib/queries.server';
const subscription = getUserSubscription(userId);
</script>
{#await subscription}
Loading subscription...
{:then sub}
<p>Plan: {sub.tier}</p>
{/await}
The TypeScript compiler knows that subscription is a promise resolving to your subscription object. No manual type annotations needed. The query is cached in the browser and deduplicated across concurrent requests on the server.
form: Mutations with validation
The form helper handles mutations—creating, updating, or deleting data. It integrates with SvelteKit's form actions, giving you validation on both client and server, optimistic UI updates, and automatic loading states.
Real-world scenario: A ride-hailing platform needs to let drivers update their availability status. The change must be validated, persisted, and reflected in the UI immediately without waiting for the server response.
// src/lib/forms.server.ts
import { form } from 'sveltekit-form';
export const updateDriverStatus = form(
async (data: { driverId: string; status: 'online' | 'offline' }) => {
// Validation happens here
if (!data.driverId || !data.status) {
throw new Error('Missing required fields');
}
await db.drivers.update({
where: { id: data.driverId },
data: { status: data.status },
});
return { success: true, status: data.status };
}
);
// src/routes/driver/status/+page.svelte
<script>
import { updateDriverStatus } from '$lib/forms.server';
let status = 'offline';
let loading = false;
async function toggleStatus() {
loading = true;
try {
const result = await updateDriverStatus({
driverId: userId,
status: status === 'online' ? 'offline' : 'online',
});
status = result.status;
} catch (err) {
console.error('Update failed:', err.message);
} finally {
loading = false;
}
}
</script>
<button on:click={toggleStatus} disabled={loading}>
{loading ? 'Updating...' : `Go ${status === 'online' ? 'Offline' : 'Online'}`}
</button>
The form helper automatically serializes data, handles errors, and validates on both sides of the wire. You define the logic once and get a type-safe client-side interface.
command: Fire-and-forget actions
The command helper is for actions that don't require a response—analytics events, notifications, or logging. It's lightweight and doesn't block the UI.
Real-world scenario: A SaaS app tracks user interactions for analytics. When a user clicks "upgrade now," you send a command to the server without waiting for a response.
// src/lib/commands.server.ts
import { command } from 'sveltekit-command';
export const trackEvent = command(
async (data: { userId: string; event: string; metadata?: object }) => {
await db.events.create({
data: {
userId: data.userId,
event: data.event,
metadata: data.metadata,
timestamp: new Date(),
},
});
// No return value needed
}
);
// src/routes/pricing/+page.svelte
<script>
import { trackEvent } from '$lib/commands.server';
function handleUpgradeClick() {
// Fire the command without waiting
trackEvent({
userId: currentUserId,
event: 'upgrade_clicked',
metadata: { plan: 'pro' },
});
// Navigate immediately
goto('/checkout');
}
</script>
<button on:click={handleUpgradeClick}>Upgrade Now</button>
Commands are ideal for telemetry, logging, or side effects where the client doesn't need a result. They're fire-and-forget by design.
Run on the server, call from components
One of the core strengths of remote functions is the separation of concern. Your sensitive logic—database queries, API calls to third-party services, authentication checks—runs only on the server. Your component code calls these functions as if they were local, but the actual execution happens server-side.
This means you can safely put secrets in your server functions without worrying about exposure. API keys, database credentials, and auth tokens stay server-side.
// src/lib/auth.server.ts
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY;
export const createPaymentIntent = form(
async (data: { amount: number; customerId: string }) => {
const intent = await stripe.paymentIntents.create({
amount: data.amount,
customer: data.customerId,
currency: 'usd',
});
return { clientSecret: intent.client_secret };
}
);
The STRIPE_SECRET_KEY is never exposed to the browser. Only the result is sent back to the client. This is a cleaner model than managing separate API routes and CORS configurations.
Replace many API routes
Traditional SvelteKit projects often accumulate many route files for API endpoints. A +server.ts for fetching user data, another for updating profile, another for logging events. Each one needs its own request handling, error middleware, and response formatting.
Remote functions consolidate this. Instead of /api/user.ts, /api/profile.ts, /api/events.ts, you organize your server logic in library files and import them directly into components. The routing layer remains minimal.
Before (traditional API routes): 10+
+server.tsfiles, manual fetch calls, separate type definitions for request/response.After (remote functions): Organized library files, direct imports from components, single source of truth for types.
This doesn't mean you eliminate all +server.ts files—you'll still need them for webhooks, OAuth callbacks, or third-party integrations. But for your app's primary client-server communication, remote functions are simpler and more maintainable.
Current stability status
As of 2025, the remote functions ecosystem in SvelteKit is stable but evolving. The core patterns—using server-only code, leveraging form actions, and integrating with load functions—are production-ready and widely used. However, the specific library names and APIs may vary depending on your SvelteKit version and chosen integration.
Check the official SvelteKit documentation and your chosen query/mutation library's release notes for the latest guidance. Popular options include:
- TanStack Query (React Query reborn) – Mature, feature-rich, with SvelteKit support.
- SvelteKit form actions – Built-in, requires less setup, good for simple forms.
- Custom server-only modules – Minimal overhead, full control.
The patterns are stable; the tooling continues to improve. Start with built-in form actions and load functions, then adopt query libraries when your app's data fetching needs become more complex.
Common mistakes
- Calling server functions from the client bundle: Always import from
+server.tsor.server.tsfiles to ensure code doesn't leak to the browser. Use TypeScript's// @ts-expect-errorsparingly and only when intentional. - Forgetting to validate on the server: Remote function validation happens server-side. Don't rely on client-side checks alone; always re-validate in your server logic to prevent malicious inputs.
- Over-fetching with queries: Design your query keys carefully. Too granular, and you lose caching benefits. Too coarse, and you fetch unnecessary data. Strike a balance based on your access patterns.
- Not handling loading and error states: Remote functions return promises. Always wrap them in
#awaitblocks or promise chains to show loading indicators and error messages. - Mixing concerns: Keep business logic in server functions, UI state in components. Don't embed database queries in your Svelte components just because remote functions make it easy.
Key takeaways
- Remote functions eliminate boilerplate by letting you call server code directly from components with full TypeScript safety.
queryhandles read operations with automatic caching and request deduplication;formmanages mutations with validation;commandsends fire-and-forget actions.- Logic runs on the server, keeping secrets secure. Components invoke these functions and handle the returned promises.
- Remote functions replace many explicit API routes, reducing file count and improving maintainability for typical CRUD and data-fetching scenarios.
- The ecosystem is stable and production-ready. Start with SvelteKit's built-in form actions, then adopt specialized libraries as your needs scale.
- Validate inputs on the server, handle loading and error states in components, and keep concerns separated between server functions and UI logic.






Comments
Loading comments…