Learn SvelteKit - Forms, Validation, & Interaction
Episode 11 of 24

Learn SvelteKit - Forms, Validation, & Interaction

This episode covers forms in depth: submission and validation flows, server-side validation with UI feedback, progressive enhancement with a no-JS fallback, and file uploads with multipart forms. You will build forms that are secure and still work without JavaScript.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Introduction

Forms are the meeting point between users and data: registration, login, comments, even image uploads. Episode 11 covers forms thoroughly — from correct submission flows, server-side validation, to the no-JavaScript experience.

The core principle SvelteKit holds is progressive enhancement: a form written with standard HTML works even when JavaScript is disabled. The JS layer then adds a smoother experience — instant validation, no lost state, and responsive feedback.

After this episode, you can build forms that are secure, provide clear feedback to users, and handle file uploads without being tied to a specific technology.

Submission Flow and Core Principles

Standard HTML Forms

Everything starts with a plain HTML form: a form element with method and action attributes. Without JavaScript, the browser sends the data as a POST and reloads the page — this is the baseline that must always work.

Basic form
<form method="POST" action="?/daftar">
    <label for="email">Email</label>
    <input id="email" name="email" type="email" required />
 
    <label for="password">Password</label>
    <input id="password" name="password" type="password" required minlength="8" />
 
    <button type="submit">Daftar</button>
</form>

The action="?/daftar" attribute targets a server action named daftar in the matching +page.server.js. Every input element must have a name attribute, because that name is the data key on the server.

Reading FormData on the Server

A server action receives request and reads the data via request.formData(). Each value is read as a string; numbers must be converted manually.

Server-Side Validation

Returning Values Alongside Errors

Primary validation should always be on the server — client validation is convenience, not security. Use the fail helper from @sveltejs/kit to return the form values together with an error message and a 4xx status.

JSValidation and fail
import { fail } from "@sveltejs/kit";
 
export const actions = {
    daftar: async ({ request }) => {
        const form = await request.formData();
        const nama = String(form.get("nama") ?? "");
        const email = String(form.get("email") ?? "");
 
        if (nama.length < 3) {
            return fail(400, { nama, email, error: "Nama minimal 3 karakter" });
        }
 
        if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
            return fail(400, { nama, email, error: "Format email tidak valid" });
        }
 
        await simpanPengguna({ nama, email });
 
        return { sukses: true };
    }
};

The values returned from an action become the form prop in the component. By returning the invalid inputs, you can refill the fields without making the user retype them.

UI Feedback

Feedback from an action result
<script>
    import { enhance } from "$app/forms";
 
    let { form } = $props();
</script>
 
<form method="POST" action="?/daftar" use:enhance>
    <label for="nama">Nama</label>
    <input id="nama" name="nama" value={form?.nama ?? ""} />
 
    {#if form?.error}
        <p class="error" role="alert">{form.error}</p>
    {/if}
 
    <button type="submit">Daftar</button>
</form>

The form prop becomes available after the action runs for the first time. Note the role="alert" for accessibility: screen readers announce the error without forcibly moving focus.

Progressive Enhancement and No-JS Fallback

use:enhance as a Layer

The use:enhance directive turns a native submit into a fetch. The result: no page reload, the form doesn't lose focus, and developers can add transitions or partial updates.

use:enhance with loading feedback
<script>
    import { enhance } from "$app/forms";
 
    let { form } = $props();
    let mengirim = $state(false);
</script>
 
<form method="POST" action="?/daftar"
    use:enhance={() => {
        mengirim = true;
        return async ({ update }) => {
            await update();
            mengirim = false;
        };
    }}>
    <button type="submit" disabled={mengirim}>
        {mengirim ? "Mengirim..." : "Daftar"}
    </button>
</form>

The function returned from the use:enhance callback receives an object containing update, result, and form. Call update() to apply the action's result to the page. If JavaScript fails to load, the form still runs as a native POST — no data is lost.

Testing Without JS

Always test forms in a no-JS condition, for example by disabling JavaScript in devtools or through an E2E test like npx playwright test. This guarantees the most basic path still works and becomes a safety net for users on slow connections or browsers that restrict scripts.

File Upload and Multipart Forms

Reading Files in an Action

File uploads work through a form with enctype="multipart/form-data". On the server, the file value appears as a File object inside the FormData.

JSHandling file uploads
export const actions = {
    unggah: async ({ request }) => {
        const form = await request.formData();
        const file = form.get("gambar");
 
        if (!(file instanceof File) || file.size === 0) {
            return fail(400, { error: "Pilih file terlebih dahulu" });
        }
 
        if (file.size > 5 * 1024 * 1024) {
            return fail(400, { error: "Ukuran file maksimal 5 MB" });
        }
 
        const bytes = new Uint8Array(await file.arrayBuffer());
        await simpanKeStorage(file.name, file.type, bytes);
 
        return { sukses: true };
    }
};

Always enforce file size and type limits on the server, not just the client. For large files, consider saving directly to object storage (for example S3 or R2) and storing only the metadata in the database.

Payload Limits

Serverless and edge runtimes often have request size limits. Check your provider's limits and give a clear message when an upload exceeds them. For very large files, the right strategy is direct upload to storage via a presigned URL, where the server only signs the permission without moving the bytes.

Closing

Key takeaways:

  • Standard HTML forms are the baseline; server actions handle POSTs securely.
  • Primary validation is on the server; fail returns values and errors with a 4xx status.
  • The form prop carries action results to the component for UI feedback.
  • use:enhance adds a JS layer without removing the native fallback.
  • Always test forms in a no-JS condition to guarantee the basic path works.
  • File uploads need size and type validation on the server, and consider object storage for large files.

In the next episode we get into authentication & authorization: auth patterns in SvelteKit, session handling with cookies and server-side auth, protected routes and authorization guards, plus external auth providers like OAuth and OIDC.

Learn SvelteKit - Forms, Validation, & Interaction | Learn SvelteKit