SvelteKit form actions with fail() and Zod, unit-tested (Vitest): working versions (tested 2026-10-15)
SvelteKit +page.server.ts form actions that validate with Zod, return fail(400) with field errors, and redirect on success, tested by calling the actions with a fake RequestEvent.
passed3 passed · 0 failed2026-10-15 11:57 UTC- Status
- passed
- Last passed
- 2026-10-15 11:57 UTC
- Tests
- 3 / 3
- Language
- TypeScript
- Runtime
- node v22.23.3
npm install --no-audit --no-fund --ignore-scripts=false npm test
node v22.23.3 linux 6.17.0-1022-azure x64 runner: github-actions 38730348195
The code
Exactly the files that passed. Your agent can pull them with get_recipe("sveltekit-form-actions") (install), or see them on GitHub.
{
"name": "sveltekit-form-actions",
"private": true,
"type": "module",
"scripts": {
"test": "vitest run --reporter=default --reporter=json --outputFile=test-report.json"
},
"dependencies": {
"@sveltejs/kit": "3.1.0",
"svelte": "5.57.2",
"zod": "4.6.7"
},
"devDependencies": {
"@types/node": "24.19.2",
"vitest": "5.0.4"
}
}
import { fail, redirect, type Actions } from "@sveltejs/kit";
import { z } from "zod";
const Signup = z.object({ email: z.email(), password: z.string().min(8) });
export const users: string[] = [];
export const actions = {
default: async ({ request, cookies }) => {
const form = Object.fromEntries(await request.formData()) as Record<string, string>;
const parsed = Signup.safeParse(form);
if (!parsed.success) {
// never echo the password back to the page
return fail(400, { email: form.email, errors: z.flattenError(parsed.error).fieldErrors });
}
if (users.includes(parsed.data.email)) return fail(409, { email: form.email, errors: { email: ["Already registered"] } });
users.push(parsed.data.email);
cookies.set("session", `s_${users.length}`, { path: "/", httpOnly: true, sameSite: "lax" });
redirect(303, "/welcome");
},
} satisfies Actions;
import { describe, it, expect } from "vitest";
import { isActionFailure, isRedirect } from "@sveltejs/kit";
import { actions } from "../src/routes/signup/+page.server";
function event(fields: Record<string, string>) {
const jar = new Map<string, string>();
const ev = { request: new Request("http://app/signup", { method: "POST", body: new URLSearchParams(fields) }), cookies: { set: (k: string, v: string) => jar.set(k, v) } } as any;
return { ev, jar };
}
describe("signup action", () => {
it("fails with field errors and no password echo", async () => {
const r: any = await actions.default(event({ email: "bad", password: "123" }).ev);
expect(isActionFailure(r)).toBe(true);
expect(r.status).toBe(400);
expect(Object.keys(r.data.errors).sort()).toEqual(["email", "password"]);
expect(r.data.password).toBeUndefined();
});
it("sets a cookie and redirects with 303", async () => {
const { ev, jar } = event({ email: "a@example.com", password: "longenough" });
const err = await actions.default(ev).catch((e) => e);
expect(isRedirect(err)).toBe(true);
expect(err.status).toBe(303);
expect(err.location).toBe("/welcome");
expect(jar.get("session")).toBe("s_1");
});
it("409 on duplicate email", async () => {
const r: any = await actions.default(event({ email: "a@example.com", password: "longenough" }).ev);
expect(r.status).toBe(409);
});
});
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.
Related stacks
Hono + Drizzle ORM + SQLite REST API (Vitest)
CRUD REST API in Hono backed by Drizzle ORM on better-sqlite3, with Zod validation, tested with Vitest and app.request().
tRPC 11 client + server over fetch with superjson (Vitest)
End-to-end typed tRPC 11: fetch adapter server, createTRPCClient with httpBatchLink and superjson transformer for Dates and Maps, tested in-process with Vitest.
Anthropic TypeScript SDK tool use loop, tested with a mocked fetch
Run a Claude tool_use loop with the official Anthropic SDK and Zod-validated tool inputs, tested offline with an injected fetch.
Astro 7 component rendering tests with the Container API (Vitest)
Render Astro components to HTML in Vitest with the Container API and getViteConfig, checking props, slots and conditional markup.
All @sveltejs/kit recipesAll svelte recipesAll Zod recipesAll vitest recipes