iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Form Actions

Forms in Svelte and SvelteKit fall into two flavours: classic client-side reactive forms (bind:value, validation in a $: block), and SvelteKits progressive-enhancement form actions (forms that POST to ?/action and work without JavaScript). Use form actions for anything that hits the server — they survive a no-JS user, give you free type-safety, and play well with SSR.

Reactive client form + SvelteKit form action with validation

EXAMPLE
<!-- src/routes/contact/+page.svelte — client-side reactive form -->
<script lang='ts'>
  let name    = '';
  let email   = '';
  let message = '';

  // Reactive validation block
  $: errors = {
    name:    name.trim().length    < 2 ? 'Name is too short' : '',
    email:   /^\S+@\S+\.\S+$/.test(email) ? '' : 'Email looks wrong',
    message: message.trim().length < 10 ? 'Tell us a bit more' : '',
  };
  $: hasErrors = Object.values(errors).some(Boolean);

  let submitting = false;
  async function submit() {
    if (hasErrors) return;
    submitting = true;
    try {
      const res = await fetch('/api/contact', {
        method: 'POST', headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ name, email, message }),
      });
      if (!res.ok) throw new Error('send failed');
      name = email = message = '';
      alert('Thanks!');
    } catch (e) { alert((e as Error).message); }
    finally { submitting = false; }
  }
</script>

<form on:submit|preventDefault={submit}>
  <label>Name <input bind:value={name}></label>
  {#if errors.name} <small class='err'>{errors.name}</small> {/if}
  <label>Email <input type='email' bind:value={email}></label>
  {#if errors.email} <small class='err'>{errors.email}</small> {/if}
  <label>Message <textarea bind:value={message} /></label>
  {#if errors.message} <small class='err'>{errors.message}</small> {/if}
  <button disabled={hasErrors || submitting}>Send</button>
</form>

<!-- src/routes/login/+page.server.ts — SvelteKit form action -->
import { fail, redirect } from '@sveltejs/kit';
import type { Actions } from './$types';

export const actions: Actions = {
  default: async ({ request, cookies, locals }) => {
    const data = await request.formData();
    const email = String(data.get('email') ?? '').trim().toLowerCase();
    const pw    = String(data.get('password') ?? '');

    // Validation — return per-field errors and the user's typed values
    const issues: Record<string, string> = {};
    if (!/^\S+@\S+\.\S+$/.test(email)) issues.email = 'Invalid email';
    if (pw.length < 8)                  issues.password = 'At least 8 characters';
    if (Object.keys(issues).length) return fail(400, { email, issues });

    const user = await locals.auth.verify(email, pw);
    if (!user) return fail(401, { email, issues: { form: 'Wrong email or password' } });

    cookies.set('sid', await locals.auth.startSession(user), {
      path: '/', httpOnly: true, sameSite: 'lax', secure: true, maxAge: 60*60*24*7,
    });
    throw redirect(303, '/dashboard');
  },
};

<!-- src/routes/login/+page.svelte -->
<script>
  import { enhance } from '$app/forms';
  export let form;          // populated by fail() above
</script>

<form method='POST' use:enhance>
  <label>Email <input name='email' value={form?.email ?? ''}></label>
  {#if form?.issues?.email}<small class='err'>{form.issues.email}</small>{/if}
  <label>Password <input name='password' type='password'></label>
  {#if form?.issues?.password}<small class='err'>{form.issues.password}</small>{/if}
  {#if form?.issues?.form}<p class='err'>{form.issues.form}</p>{/if}
  <button>Sign in</button>
</form>

Why it matters

Form actions + use:enhance are the SvelteKit superpower: the same HTML form works with and without JavaScript, the action is fully type-safe, and validation errors round-trip without you writing any custom plumbing. Reach for client-only forms when the form is genuinely client-only (a search filter); use actions for everything that hits the server.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
// +page.server.js
export const actions = {
    create: async ({ request }) => {
        const data = await request.formData();
        await db.posts.create({ title: data.get('title') });
    },
};
Try it Yourself »

Discussion

Loading…