Logo VelaStack

Generate a form (remote functions)

Scaffold a form that posts through SvelteKit remote functions instead of classic form actions. The generator emits a +page.svelte, a form.remote.ts handler, a Zod schema, and a server test from one field list. Standard field types — text, number, email, editor, select, bool, file — render with matching shadcn-svelte inputs, and file fields switch the form to multipart/form-data automatically. Also patches svelte.config.js to enable the experimental remote-functions and async compiler options if they are not already enabled.

$ vela generate form --remote
src/routes/(public)/contact/+page.svelte
<script lang="ts">
  import { submitContactForm } from "./form.remote";
  import { Button } from "$lib/components/ui/button";
  import { Input } from "$lib/components/ui/input";
  import { Textarea } from "$lib/components/ui/textarea";
</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 {...submitContactForm}>
      <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
        <div class="space-y-2 col-span-1">
          <label for="name" class="text-sm font-medium">Name</label>
          <Input id="name" {...submitContactForm.fields.name.as("text")} />
          {#each submitContactForm.fields.name.issues() as issue}
            <p class="text-destructive text-sm">{issue.message}</p>
          {/each}
        </div>
        <div class="space-y-2 col-span-1">
          <label for="email" class="text-sm font-medium">Email</label>
          <Input
            id="email"
            {...submitContactForm.fields.email.as("text")}
            type="email"
          />
          {#each submitContactForm.fields.email.issues() as issue}
            <p class="text-destructive text-sm">{issue.message}</p>
          {/each}
        </div>
        <div class="space-y-2 col-span-2">
          <label for="message" class="text-sm font-medium">Message</label>
          <Textarea
            id="message"
            {...submitContactForm.fields.message.as("text")}
          />
          {#each submitContactForm.fields.message.issues() as issue}
            <p class="text-destructive text-sm">{issue.message}</p>
          {/each}
        </div>
      </div>
      <div class="mt-4 flex gap-2 border-t pt-4 -mx-4 px-4">
        <Button type="submit">Submit</Button>
      </div>
    </form>
  </div>
</section>
src/routes/(public)/contact/form.remote.ts
import { form, getRequestEvent } from "$app/server";
import { setFlash } from "sveltekit-flash-message/server";
import { contactSchema } from "$lib/schemas/contact";

export const submitContactForm = form(contactSchema, async (data) => {
  const { cookies } = getRequestEvent();
  setFlash({ type: "toast", message: "Form posted successfully" }, cookies);
  return { success: true };
});
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);
    });
  });
});
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(),
});