Logo VelaStack

Generate a form

Scaffold a typed SvelteKit form in seconds. Generates a page, a Zod schema, and a matching sveltekit-superforms handler from a single field list, wiring validation end-to-end. Text, number, email, password, URL, date, editor, boolean, select, and file fields each render with the appropriate shadcn-svelte input. A server test is included so the form's POST handler is covered from the moment it lands in your repo.

$ vela generate form
src/routes/(public)/contact/+page.svelte
<script lang="ts">
  import { untrack } from "svelte";
  import { superForm } from "sveltekit-superforms";
  import { zod4Client } from "sveltekit-superforms/adapters";
  import { contactSchema } from "$lib/schemas/contact";
  import * as Form from "$lib/components/ui/form";
  import { Input } from "$lib/components/ui/input";
  import { Textarea } from "$lib/components/ui/textarea";

  let { data } = $props();
  const form = superForm(
    untrack(() => data.form),
    {
      validators: zod4Client(contactSchema),
    },
  );

  const { form: formData } = form;
</script>

<section data-role="content">
  <div class="flex justify-between items-center mb-4">
    <h1 class="text-3xl font-bold tracking-tight">Contact</h1>
  </div>
  <div class="bg-card rounded-lg shadow-sm border p-4">
    <form method="POST">
      <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
        <Form.Field {form} name="name" class="col-span-1">
          <Form.Control>
            {#snippet children({ props })}
              <Form.Label>Name</Form.Label>
              <Input {...props} type="text" bind:value={$formData.name} />
            {/snippet}
          </Form.Control>
          <Form.FieldErrors class="contents text-destructive" />
        </Form.Field>
        <Form.Field {form} name="email" class="col-span-1">
          <Form.Control>
            {#snippet children({ props })}
              <Form.Label>Email</Form.Label>
              <Input {...props} type="email" bind:value={$formData.email} />
            {/snippet}
          </Form.Control>
          <Form.FieldErrors class="contents text-destructive" />
        </Form.Field>
        <Form.Field {form} name="message" class="col-span-2">
          <Form.Control>
            {#snippet children({ props })}
              <Form.Label>Message</Form.Label>
              <Textarea {...props} bind:value={$formData.message} />
            {/snippet}
          </Form.Control>
          <Form.FieldErrors class="contents text-destructive" />
        </Form.Field>
      </div>
      <div class="mt-4 flex gap-2 border-t pt-4 -mx-4 px-4">
        <Form.Button>Submit</Form.Button>
      </div>
    </form>
  </div>
</section>
src/routes/(public)/contact/+page.server.ts
import { fail, superValidate } from "sveltekit-superforms";
import { zod4 } from "sveltekit-superforms/adapters";
import { contactSchema } from "$lib/schemas/contact";
import { setFlash } from "sveltekit-flash-message/server";

export const load = async () => {
  const form = await superValidate(zod4(contactSchema));
  return { form };
};

export const actions = {
  default: async ({ request, cookies }) => {
    const form = await superValidate(request, zod4(contactSchema));

    if (!form.valid) {
      return fail(400, { form });
    }

    setFlash({ type: "toast", message: "Form posted successfully" }, cookies);

    return { form };
  },
};
src/routes/(public)/contact/server.test.ts
import { afterEach, beforeEach, describe, expect, it } from "vitest";

describe("/contact", () => {
  describe("GET /contact", () => {
    it("should return a 200 status code", async (context) => {
      const response = await context.request.get("/contact");
      expect(response.status).toBe(200);
    });
  });

  describe("POST /contact", () => {
    it("should submit the form successfully", async (context) => {
      const response = await context.request
        .post("/contact")
        .type("form")
        .send({
          name: "name value",
          email: "test-email@example.com",
          message: "message value",
        });
      expect(response.body.status).toBe(200);
    });
  });
});
src/lib/schemas/contact.ts
import { z } from "zod";

export const contactSchema = z.object({
  name: z.string().optional(),
  email: z.email().optional(),
  message: z.string().optional(),
});