Form

Build progressive forms with effect handlers and field validation.

Form wraps SvelteKit's form(). It keeps native form behavior on the client while the server handler runs inside an effect.

Use it for sign-in, settings, profile editing, and creation flows. Use Command for mutations that do not have a natural HTML form.

Define a form

Pass a schema and an effect-returning handler:

profile.remote.ts
import { Effect, Schema } from "effect";
import { Form } from "svelte-effect-runtime";

const ProfileInput = Schema.Struct({
	name: Schema.String,
	email: Schema.String,
});

export const update_profile = Form(ProfileInput, ({ data }) =>
	Effect.gen(function* () {
		yield* ProfileRepository.update(data);

		return { saved: true };
	}),
);

The browser submits the schema's encoded input. The handler receives decoded data in data.

Render the form

The exported value keeps SvelteKit's native form attributes:

profile-form.svelte
<script lang="ts">
	import { update_profile } from "./profile.remote";
</script>

<form {...update_profile}>
	<label>
		Name
		<input name="name" autocomplete="name" />
	</label>

	<label>
		Email
		<input name="email" type="email" autocomplete="email" />
	</label>

	<button>Save profile</button>
</form>

This remains an HTML form. The server handler must be correct without client JavaScript, even when you add enhancement later.

Return validation issues

The handler receives an effect-aware invalid proxy whose shape follows the decoded input. Calling the proxy creates a validation issue at that path and fails the effect, so yield or return it like any other effect:

profile.remote.ts
export const update_profile = Form(ProfileInput, ({ data, invalid }) =>
	Effect.gen(function* () {
		if (data.name.trim().length === 0) {
			return yield* invalid.name("Name is required");
		}

		if (!data.email.includes("@")) {
			return yield* invalid.email("Enter a valid email address");
		}

		return yield* ProfileRepository.update({
			name: data.name.trim(),
			email: data.email,
		});
	}),
);

The path controls where SvelteKit displays the issue:

  • invalid("Profile could not be saved") creates a form-level issue.
  • invalid.email("Enter a valid email address") creates a field issue.
  • invalid.address.city("City is required") follows nested objects.
  • invalid.items[0].name("Name is required") follows array items.

Each call stops the handler through the Effect error channel. Use return yield* when the branch ends there, or plain yield* when the surrounding generator cannot continue:

profile.remote.ts
if (yield * EmailRepository.is_taken(data.email)) {
	return yield * invalid.email("That email is already in use");
}

Schema validation runs before the handler. Use invalid for validation that depends on application state, business rules, or relationships between fields.

invalid and issue

SvelteKit's native form callback receives an issue builder and expects it to be passed to SvelteKit's invalid(...) function. SER exposes both callback arguments for compatibility, but its invalid argument is a different, Effect-aware wrapper:

HelperPurpose
invalid.email("Already in use")Builds a typed issue and fails the current effect in one step. Prefer this in SER handlers.
issue.email("Already in use")Builds SvelteKit's native issue value without failing the effect. Use it only when interoperating with an API that expects native SvelteKit issues.

Most SER handlers do not need issue. In particular, do not build an issue and then pass it to SER's invalid; SER's wrapper accepts a message and builds the issue itself.

Submit from effect code

The form is also callable and exposes submit as an effect-returning helper:

quick-profile.svelte
<script lang="ts" effect>
	import { update_profile } from "./profile.remote";

	let name = $state("Ada");
	let email = $state("ada@example.com");
</script>

<button onclick={yield* update_profile.submit({ name, email })}>
	Save
</button>

Use the HTML form surface for normal form flows. Programmatic submission is useful when the same mutation also runs from a custom control or a larger effect pipeline.

Validation and enhancement

SER preserves SvelteKit form helpers and adapts effectful operations:

  • validate() returns an effect.
  • submit() returns an effect.
  • preflight(schema) returns another SER-aware form.
  • enhance(callback) accepts callbacks that may return an effect.
  • for(id) scopes the form to a stable instance.

Enhancement should improve feedback, not become the only path that can submit the data.

Request context

Yield RequestEvent inside the handler for cookies, locals, or request metadata. Keep long-lived dependencies such as repositories in the server runtime.

On this page