Use Zod to Validate Forms on the Server with SvelteKit

  • components
  • errors
  • svelte
  • zod
Mar 25, 2023
Render issues from Zod with a Svelte component.

What is Zod?

Zod is a schema validation library that works nicely with SvelteKit. It provides an easy API to quickly validate forms across an application with reusable schemas. Many popular projects use Zod to accomplish type checking at runtime. The Astro team uses Zod in their Content Collections feature to validate Markdown frontmatter. It can also be used with TypeScript or JavaScript. Here’s how to use Zod to validate forms on the server with SvelteKit.

Install

First create a new SvelteKit project and then add Zod as a development dependency.

npx sv@latest create
npm install -D zod

Create a User Schema

Consider this Zod UserSchema, it expects a user will have an email and a password. Here, Zod can also be used to transform data with the trim and toLowerCase methods. If the parse is successful, the transformed data will be available in safeParse.data.

// src/lib/zod-schemas.ts
import { z } from "zod";

export const UserSchema = z.object({
	email: z.string().email().trim().toLowerCase(),
	password: z
		.string()
		.regex(new RegExp("^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9]).{8,}$"), {
			message:
				"Password must be at least 8 characters and contain an uppercase letter, lowercase letter, and number",
		}),
});

The safeParse method

safeParse can be utlized on any Zod schema to validate an object without creating a runtime error. For example, we could validate a user object with our UserSchema like this:

const user = {
	email: "invalid@email",
	password: "invalidPassword",
};

const safeParse = UserSchema.safeParse(user);

This would produce an error with an array of ZodIssue(s).

I’ve got (Zod)Issues

Next we can take a look at the ZodIssue object. This object is created with the safeParse method if there are any issues in the validation. After running the code above, safeParse.error.issues would contain the following ZodIssue(s) since both fields were invalid (email is missing a .com, password doesn’t contain a number).

// safeParse.error.issues

[
	{
		validation: "email",
		code: "invalid_string",
		message: "Invalid email",
		path: ["email"],
	},
	{
		validation: "regex",
		code: "invalid_string",
		message:
			"Password must be at least 8 characters and contain an uppercase letter, lowercase letter, and number",
		path: ["password"],
	},
];

SvelteKit example

With SvelteKit, we can implement these methods to create a form with server side validation.

Create a form

<!-- src/routes/+page.svelte -->

<form method="POST">
	<div>
		<label>
			Email
			<input type="email" name="email" required />
		</label>
	</div>

	<div>
		<label>
			Password
			<input type="password" name="password" required />
		</label>
	</div>

	<button>Sign Up</button>
</form>

The name attribute on each input element is required to get the form data in the +page.server.ts file.

Validate on the server

Next we can create an action for our form to validate the user’s inputs on the server. If the safeParse is unsuccessful, we will return an array of ZodIssue(s) to render to the user.

// src/routes/+page.server.ts
import { UserSchema } from "$lib/zod-schemas";
import { fail, redirect } from "@sveltejs/kit";

export const actions = {
	default: async ({ request }) => {
		// get form data from event.request
		const formData = await request.formData();

		// use .get() with the corresponding name of the input element
		// to create a user object
		const user = {
			email: String(formData.get("email")),
			password: String(formData.get("password")),
		};

		// zod validation using .safeParse
		const safeParse = UserSchema.safeParse(user);

		// if invalid - return the array of ZodIssues
		if (!safeParse.success) {
			return fail(400, { issues: safeParse.error.issues });
		}

		// sign up user...
		// transformed is data available in `safeParse.data`

		// where to send if login successful
		redirect(303, "/app");
	},
};

ZodIssues component

If there’s any validation issues, we want to display the messages provided by our UserSchema. We can create a reusable component to render these messages in an unordered list.

<!-- src/lib/components/ZodIssues.svelte -->

<script lang="ts">
	import type { ZodIssue } from "zod";

	let { issues }: { issues: ZodIssue[] } = $props();
</script>

<ul>
	{#each issues as { message, path }}
		<li>{path[0]} - {message}</li>
	{/each}
</ul>

In case of an invalid form, we can now utilize our ZodIssues.svelte component to display the issues.

<!-- src/routes/+page.svelte -->

<script lang="ts">
	import ZodIssues from "$lib/components/ZodIssues.svelte";

	let { form } = $props();
</script>

<form method="POST">
	<div>
		<label>
			Email
			<input type="email" name="email" required />
		</label>
	</div>

	<div>
		<label>
			Password
			<input type="password" name="password" required />
		</label>
	</div>

	<button>Sign Up</button>
</form>

<!-- if there are issues -->
{#if form?.issues}
	<ZodIssues issues={form.issues} />
{/if}

Conclusion

It’s important to validate forms on the server instead of the client because anyone can modify client side code. Zod makes it easy to be consistent, you can easily reuse schemas in other actions, or utilize parts of a schema with the pick method.

Thanks for reading!


Edit