Form validation with Zod and Svelte

et's learn how to implement solid form validation with Zod and Svelte, two of the coolest frameworks in web dev right now.

Let's learn how to implement solid form validation with Zod and Svelte, two of the coolest frameworks in web dev right now.

ℹī¸
This post assumes that you use Svelte with Typescript!

Now, as a creative web dev, I really like animations and good UI/UX. Part of that includes well-designed form errors. You could get away with using those browser-default styles, but why... why do that when you could have something so much nicer for your user!

Here's an example of what form errors look like in my upcoming app:

However, today's post isn't about getting creative with your form errors (I'll probably make a video about this on my YouTube channel instead), rather, let's talk about the nitty-gritty: how to do form validation with Zod, the TypeScript-based validation library within Svelte.

It's actually... not that bad (despite Zod's API being hard to work with a bit).

Why Zod

I feel it's important to put a quick forward here about why we're using Zod and not something else like Yup. Let me just keep it simple:

  1. Zod supports Typescript natively
  2. Zod gives you an easy-ish way to format errors
  3. It's extremely popular among TypeScript developers and web framework authors

At the end of the day, it is just a validation library.

Install Zod

First things first, we gotta install Zod!

npm i zod

Most Svelte projects setup with TS will already have strict mode enabled, but if you don't, go ahead and enable it in your tsconfig.json:

// tsconfig.json

{
  // ...
  "compilerOptions": {
    // ...
    "strict": true
  }
}

Validating form input

Alright, so how exactly should we validate our form inputs?

Should we let the <Input /> component validate its own values?

Should we just validate the whole form in the same file where we define it?

How do we go about this? The answer is kinda up to you but I personally recommend the later, we just validate the form input wherever we define our form and the state values for our form (to me this is the most straight forward).

I'm going to assume you have some custom <TextInput /> components already made as well as a form where you use them. It's not really important (yet), what is is setting up Zod:

(Oh yeah, I'm also assuming SvelteKit is being used here, not very important though)

// +page.svelte

<script>
    import { z } from "zod"
    
    const formSchema = z.object({
    	name: z.string().nonempty("Please enter your name"),
        password: z.string().nonempty("Please enter your password")
    })
</script>

Alright so two things here:

  1. We setup a formSchema which mimicks the fields our form contain (in this example, we working with a login form like in the GIF from earlier)
  2. We define some validations against our schema using the string() and nonempty() Zod functions. There are a lot of these depending on the data type you're working with. Checkout the Zod docs for more info.

Excluded in this snippet is our state variables for our form, let's go ahead and add those in as well as form component:

// +page.svelte

<script>
    import { z } from "zod"
    
    const formSchema = z.object({
    	name: z.string().nonempty("Please enter your name"),
        password: z.string().nonempty("Please enter your password")
    })
    
    let name;
    let password;
    
</script>

<form>
	<TextInput name="name" type="text" bind:value={name}></TextInput>
    <TextInput name="password" type="password" bind:value={password}>	 </TextInput>
</form>

Now let's implement the validation logic, this will run when the user submits the form:

// +page.svelte

<script>
    // ...
    let errors = {_errors: []};
    
    const handleSubmit = () => {
    	const result = formSchema.safeParse({name, password});
        
        if (!result.success) {
			errors = result.error.format();
			return;
		} else {
			// Continue with form submission...
		}
            
    }
</script>

<form on:submit|preventDefault={handleSubmit}>
	// ...
</form>

Two new things here:

  1. We defined a new state variable errors, this is where we'll keep errors if Zod has any for us
  2. We use formSchema.safeParse to validate our name and password. safeParse lets us handle validation without working with error handling (by default, Zod throws an error if it finds one, we don't want that because it's harder to deal with in the context of form validation)

Alright, with that out of the way, we just need to add our errors to the <TextInput /> fields when Zod reports an error.

Unfortunately, this part is a little weird.

You'll notice that we're using a result.error.format() function. This takes the Zod error and formats it to be a little nicer to work with, although not by much. Here's an example of what a formatted Zod error looks like:

Yeah...

My recommendation for your error messages, just pass them as string props to your form components.

You can do something like this:

<TextInput name="name" type="text" bind:value={name} error={errors["name"]?._errors[0]}></TextInput>

Although I find that really annoying to do for every form component I have, so instead I just recommend you write a parseError utility function that you can then import and use:

// parseError.ts

import type { ZodFormattedError } from 'zod';

export const parseError = <T>(name: string, error: ZodFormattedError<T>) => {
	// @ts-ignore
	return error[name]?._errors[0] ?? '';
};
<TextInput name="name" type="text" bind:value={name} error={parseError(errors, 'name')}></TextInput>

đŸ’Ĩ BOOM. Form validation with Zod in Svelte, done my way 👏.