Integration library for Effect with SvelteKit. Run Effect programs in SvelteKit load functions and remote functions with automatic error handling, redirects, and form validation.
npm add effect-svelte
# or
bun add effect-svelte
Peer dependencies: effect >=3.19.15, svelte >=5, @sveltejs/kit >=2
// src/lib/server/runtime.ts
import { Effect, Layer, ManagedRuntime } from "effect";
import { createRunner } from "effect-svelte";
const AppLayer = Layer.mergeAll(
// your service layers
);
const RuntimeServer = ManagedRuntime.make(AppLayer);
export const remoteRunner = createRunner({
runtime: RuntimeServer,
before: () => Effect.log("Starting operation"),
after: () => Effect.log("Operation completed"),
onError: (err, isUnexpectedError) =>
isUnexpectedError
? Effect.logError("Operation failed", err)
: Effect.void,
});
The runner works directly inside SvelteKit load functions. Call it with an operation name, your Effect program, and an optional error-mapping pipeline:
// src/routes/posts/+page.server.ts
import { Effect } from "effect";
import { httpErrorEffect } from "effect-svelte";
import { remoteRunner } from "$lib/server/runtime";
export async function load({ params }) {
return remoteRunner(
"load-posts",
Effect.gen(function* () {
const posts = yield* fetchPosts();
if (posts.length === 0) {
yield* httpErrorEffect(404, "NOT_FOUND", "No posts found");
}
return { posts };
}),
);
}
<!-- src/routes/posts/+page.svelte -->
<script lang="ts">
let { data } = $props();
</script>
<ul>
{#each data.posts as post (post.id)}
<li>{post.title}</li>
{/each}
</ul>
The same runner works with SvelteKit's experimental remote functions:
// src/routes/posts.remote.ts
import { Effect } from "effect";
import { query } from "$app/server";
import { remoteRunner } from "$lib/server/runtime";
export const getPosts = query(() =>
remoteRunner(
"get-posts",
Effect.gen(function* () {
yield* Effect.logInfo("Fetching posts");
return { posts: [{ id: 1, title: "Hello" }] };
}),
),
);
<!-- src/routes/+page.svelte -->
<script lang="ts">
import { getPosts } from "./posts.remote.ts";
</script>
<svelte:boundary>
{#snippet pending()}
<p>loading...</p>
{/snippet}
{#snippet failed(error)}
<span>Error: {error}</span>
{/snippet}
{@const { posts } = await getPosts()}
<ul>
{#each posts as post (post.id)}
<li>{post.title}</li>
{/each}
</ul>
</svelte:boundary>
The library provides three tagged error types that map to SvelteKit's control flow:
import { Effect } from "effect";
import {
SvelteKitRedirect,
SvelteKitHttpError,
SvelteKitInvalidError,
redirectEffect,
httpErrorEffect,
invalidEffect,
} from "effect-svelte";
Effect.gen(function* () {
// Redirect (throws SvelteKit redirect())
yield* redirectEffect(303, "/login");
// or: yield* Effect.fail(SvelteKitRedirect.make(303, "/login"));
// HTTP error (throws SvelteKit error())
yield* httpErrorEffect(404, "NOT_FOUND", "Resource not found");
// or: yield* Effect.fail(SvelteKitHttpError.make(404, "NOT_FOUND", "Not found"));
// Form validation error (throws SvelteKit invalid())
yield* invalidEffect("email", "Invalid email format");
// or: yield* Effect.fail(SvelteKitInvalidError.make({ email: "Invalid" }));
});
Use the third argument of the runner to map domain errors to SvelteKit errors:
export const listProjects = remoteRunner(
"listProjects",
Effect.gen(function* () {
const service = yield* ProjectService;
return yield* service.all();
}),
(effect) =>
effect.pipe(
Effect.catchTags({
ParseError: () =>
httpErrorEffect(500, "PARSE_ERROR", "Unexpected data from the server"),
ResponseError: () =>
httpErrorEffect(500, "GENERIC_ERROR", "Unexpected server response"),
}),
),
);
createRunner(options)Creates a runner function that executes Effect programs in a SvelteKit context.
Options:
| Option | Type | Description |
|---|---|---|
runtime |
ManagedRuntime<R, never> |
The Effect runtime to use |
before |
() => Effect<void> |
Runs before the effect |
after |
(result: A) => Effect<void> |
Runs after successful execution |
onError |
(err, isUnexpectedError) => Effect<void> |
Error handler. isUnexpectedError is true for 500+ errors and unrecognized failures, false for redirects, validation errors, and <500 HTTP errors |
Returns: Runner<R> - a function with signature:
(operationName: string, effect: Effect<A, E, R>, pipeline?: PipelineFn<R>) => Promise<A>
Execution order: before → effect → pipeline → onError (on failure) → after (on success) → withSpan(operationName)
| Class | Converts to | Factory |
|---|---|---|
SvelteKitRedirect |
redirect(status, location) |
SvelteKitRedirect.make(status, location) |
SvelteKitHttpError |
error(status, body) |
SvelteKitHttpError.make(status, code, message, details?) |
SvelteKitInvalidError |
invalid(issues) |
SvelteKitInvalidError.make(issues) |
| Function | Description |
|---|---|
redirectEffect(status, location) |
Returns Effect.fail(SvelteKitRedirect.make(...)) |
httpErrorEffect(status, code, message, details?) |
Returns Effect.fail(SvelteKitHttpError.make(...)) |
invalidEffect(issues) |
Returns Effect.fail(SvelteKitInvalidError.make(...)) |
SvelteEffect.CodeError code type: "GENERIC_ERROR" | "PARSE_ERROR" | "NOT_FOUND" | "UNAUTHORIZED"
MIT