SvelteKit Form Example with 10 Mistakes to Avoid
Tags: deno, testing, webdev
Published: 2023-08-18 14:24:27

---
title: "SvelteKit Form Example with 10 Mistakes to Avoid"
published: "true"
description: "SvelteKit form example 📝 code to get a contact form working with SvelteKit actions, and 10 tips to avoid common pitfalls 🍀."
tags: "deno, testing, webdev"
canonical_url: "https://rodneylab.com/sveltekit-form-example-with-10-mistakes-to-avoid/"
cover_image: "https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3dwzmbtt1venl4ue0ux0.png"
---

## 🤞🏽 Making your SvelteKit Form Work

We start with some basic SvelteKit Form example snippets, in this post.  SvelteKit form issues are one of the topics I get asked for help on most often. So, with the basic example out of the way, we look at 10 pitfalls you should try to avoid in your SvelteKit forms.  This should help you debug any issues.  If you are still having issues not covered here though, the best piece of advice I can offer is to cut out features until you have things working.  Then, once you have the issues fixed, add the features back in one-by&#8209;one.

Let&rsquo;s start with that base example.  We will later add feedback for missing fields, server-side validation using Zod and user experience enhancements.
</div>

## 🧱 Minimal SvelteKit Form Example

![SvelteKit Form Example: screen capture shows a contact form with email and message fields, and a send button.  All fields are blank.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mnjsue1o8p0ifxx5g9lh.png)

This minimal code is missing some common user experience and feedback features, but should be enough to get us going!

```svelte
<script lang="ts">
</script>
 
<main>
	<h1>Contact form</h1>
	<form method="POST">
		<p>Leave a message</p>
		<label for="email">Email</label>
		<input
			id="email"
			type="email"
			required
			name="email"
			autocomplete="username"
			placeholder="trinidad@example.com"
		/>
		<label for="message">Message</label>
		<textarea id="message" required name="message" placeholder="Leave your message…" rows={4} />
		<button type="submit">Send</button>
	</form>
</main>
```

### `+page.svelte`: what we Have so Far
- basic form using web standards
- input fields have corresponding label elements, with `for` value matching the input `id`
- there is no `preventDefault`, because we are relying on browser primitives to submit the form

### Corresponding Server Code

```typescript
import { redirect } from '@sveltejs/kit';
import type { Actions } from './$types';

export const actions: Actions = {
	default: async ({ request }) => {
		const form = await request.formData();
		const email = form.get('email');
		const message = form.get('message');
 
		if (typeof email === 'string' && typeof message === 'string') {
			// Remember to obfuscate any real, personally identifiable information (PII) from logs,
			// which unauthorised personnel might have access to.
			console.log({ email, message });

			// Add logic here to process the contact form.
		}
 
		throw redirect(303, '/');
	}
};
```
### `+page.server.ts`: what we Have so Far
- The handler is named `default` in line `5`, because there is no `action` attribute on the `form` element in the Svelte markup.  We see how to use named handlers later.
- `email` & `message` in `form.get` method calls (lines `7` & `8`) match the `name` attribute value on the `input` and `textarea` elements in the Svelte markup.
- Working in TypeScript, form elements are of type `FormDataEntryValue`, and we should narrow the type to a more useful `string`, ahead of processing (line `10`).

### `+page.server.ts`: what is Missing
- We just `console.log()` the input email and message values. A typical workflow would involve emailing the message details to a team member who is authorized to handle personally identifiable information (**PII**).   See example code for sending this email using the Postmark REST API below.
- There is no check that the email is valid nor that the visitor left a message. We see how to use Zod for validation further down.
- There is no feedback.  We redirect the user to the home page, even if the data was not as expected.  We will return a SvelteKit `fail` object, below, to provide feedback in the markup.

#### Postmark Example: Emailing Form Details to a Team Member
```typescript
import { fail, redirect } from '@sveltejs/kit';
import type { Actions } from './$types';
import { POSTMARK_SERVER_TOKEN } from '$env/static/private';

export const actions: Actions = {
	default: async ({ request }) => {
		const form = await request.formData();
		const email = form.get('email');
		const message = form.get('message');
 
		if (typeof email === 'string' && typeof message === 'string') {
			await fetch('https://api.postmarkapp.com/email', {
				method: 'POST',
				headers: {
					Accept: 'application/json',
					'Content-Type': 'application/json',
					'X-Postmark-Server-Token': POSTMARK_SERVER_TOKEN
				},
				body: JSON.stringify({
					From: 'admin@example.com',
					To: email,
					Subject: 'Contact form message',
					TextBody: JSON.stringify({
						email,
						message
					}, null, 2),
					MessageStream: 'outbound'
				})
			});
		}
 
		throw redirect(303, '/');
	}
};
```
See <a aria-label="Postmark docs for more on the REST A P I" href="https://postmarkapp.com/developer/user-guide/send-email-with-api">Postmark docs for more on the REST API</a>.

## ⚠️ SvelteKit Forms: 10 Mistakes to Avoid

**Next, we can add some missing features, pointing out trip hazards along the way.**

1. ⛔️ **No SSG allowed**! In your `svelte.config.js` file, use `@sveltejs/adapter-auto` (the default) or another SSR compatible adapter, such as <a href="https://developers.cloudflare.com/pages/framework-guides/deploy-a-svelte-site">`@sveltejs/adapter-cloudflare`</a> or <a href="https://docs.netlify.com/integrations/frameworks/sveltekit/">`@sveltejs/adapter-netlify`</a>.  Also, make sure you do not have prerender directives in your server code for form handling routes.<br/><br/>You can build a completely static site with forms in SvelteKit, though you will probably want to <a href="https://rodneylab.com/sveltekit-local-edge-functions/">use edge functions</a> or <a href="using-netlify-functions-sveltekit/">serverless functions</a> to handle form submissions.  The techniques in this post will not work with prerendered pages.

2. 📮 **Most of the time you will want the <code>POST</code> form method**.  As best practice, when there is a side effect (e.g. you log a user in, add a row to the database etc.), use the `POST` method on the form element.  Use `GET` when <a href="https://developer.mozilla.org/en-US/docs/Learn/Forms/Sending_and_retrieving_form_data">the user just needs to request a document</a>. See <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods">MDN HTTP request method docs</a>

3. 🫱🏾‍🫲🏼 <strong>Add an <code>action</code> attribute to the form element for named handlers</strong>.  Above, we used the `default` handler in the server action code.  You might want to name your handlers, and if you do, you must include an `action` attribute on the form element in the markup.  Use this approach to make code intention clearer, or when you have more than one form on a single route.
    `src/routes/+page.svelte`:
    ```html
    <form action="?/contact" method="POST">
        <!-- TRUNCATED -->
    </form>
    ```
    `src/routes/+page.server.ts`:
    ```typescript
        export const actions: Actions = {
     	    contact: async ({ request }) => {
      		    const form = await request.formData();
       	    	/* TRUNCATED... */
           }
        }
    ```

4. 🫣 **Don’t trust users**! By all means, use client-side validation, to enhance user experience, but do not rely on it.  A savvy user might manipulate the form response, wreaking havoc in your server logic!  The **Zod** library is a common choice for validation, and we see how to integrate it further down.  Zod saves you having to work out your own Regex to validate email addresses and such like.

5. 💯 **Send the user helpful feedback when their input is invalid**. This goes hand-in-hand with validation, from the previous tip.  SvelteKit server actions can return an `ActionFailure` using the `fail` helper method.  This lets you send error messages to the client, as well as track valid inputs (saving the user having to re-enter them).

    ```typescript
    import { fail, redirect } from '@sveltejs/kit';
    import type { Actions } from './$types';

    export const actions: Actions = {
	    contact: async ({ request }) => {
		    const form = await request.formData();
	    	const email = form.get('email');
		    const message = form.get('message');

		    if (typeof email !== 'string') {
			    return fail(400, {
				    message: typeof message === 'string' ? message : '',
				    error: { field: 'email', message: 'Check your email address.' }
			    });
		    }

		    if (typeof message !== 'string') {
			    return fail(400, {
				    email,
				    error: { field: 'message', message: 'Don’t forget to leave a message!' }
			    });
		    }
		    /* TRUNCATED... */
	    }
    }
    ```
    Improving on the initial version, here, if the `email` is not a string (we introduce further checks when we look at Zod below), we can: (i) send back the existing input for the message field, (ii) return the custom `error` object which shows which field is awry and what the issue is.

6. 🤗 **Make sure feedback is accessible**.  We can make use of the new feedback from the previous tip to let the user know when the inputs were not as expected.  You can use `aria-invalid` and `aria-describedby` attributes to keep this feedback accessible.
    ```svelte
    <script lang="ts">
	    import type { ActionData } from './$types';
	    export let form: ActionData;

    	let { email, error, message } = form ?? {};
	    $: ({ email, error, message } = form ?? {
		    email: '',
		    error: { field: '', message: '' },
	    	message: ''
    	});
    </script>

    <main>
	    <h1>Contact form</h1>
	    <form action="?/contact" method="POST">
		    <p>Leave a message</p>
    		<label for="email">Email</label>
	    	<input
		    	id="email"
			    type="email"
			    required
    			name="email"
	        		autocomplete="username"
			    placeholder="trinidad@example.com"
    			aria-invalid={error?.field === 'email'}
	    		aria-describedby={error?.field === 'email' ? 'email-error' : undefined}
		    />
		    {#if error?.field === 'email'}
			    <small id="email-error">{error?.message}</small>
    		{/if}
	    	<!-- TRUNCATED... -->
    	</form>
    </main>
    ```
    SvelteKit plumbing makes the `email`, `error` and `message` variables returned from the action available on the frontend, via `export let form`.  See the <a href="https://rodneylab.com/sveltekit-forms/" >Grammar check post</a> for an explanation of why there is a double assignment for these identifiers.<br/><br/>Any email field error will be displayed by the template code in lines <code>29</code>&thinsp;&ndash;&thinsp;<code>31</code>.   Notice, we will display the exact message set on the server, saving us re-creating a message.  The <code>id</code> on the <code>small</code> element in line <code>30</code> is important to link the erroneous field, using an <code>aria-describedby</code> attribute in line <code>27</code>.  Code for the message input is similar.

7. 🙏🏽 **Avoid making the user re-enter details which were perfectly valid**.  An exception here is a password.  Typically, you will ask the user to re-enter a password when a form input was rejected.  For other fields, though, avoid winding up users; use a `value` attribute to repopulate the fields when a rejected form reloads:
    ```svelte
    <script lang="ts">
	    import type { ActionData } from './$types';
    	export let form: ActionData;

	    let { email, error, message } = form ?? {};
    	$: ({ email, error, message } = form ?? {
	    	email: '',
		    error: { field: '', message: '' },
    		message: ''
    	});
    </script>

    <main>
	    <h1>Contact form</h1>
    	<form action="?/contact" method="POST">
	    	<p>Leave a message</p>
		    <label for="email">Email</label>
    		<input
	    		value={email}
		    	id="email"
			    type="email"
    			required
	    		name="email"
		    	autocomplete="username"
			    placeholder="trinidad@example.com"
    			aria-invalid={error?.field === 'email'}
    			aria-describedby={error?.field === 'email' ? 'email-error' : undefined}
	    	/>
		    <!-- TRUNCATED... -->
    	</form>
    </main>
    ```

8. 🏋🏽 **Let Zod do the heavy lifting**.  Writing and testing validation code can eat up a lot of time.  Use a <a href="https://zod.dev/">validation library like Zod</a> to save your back!  There is minimal extra code for this example.  Add Zod as a dev dependency (`pnpm add -D zod`) then, define a schema for the form data:

    ```typescript
    import { z } from 'zod';

    export const contactFormSchema = z.object({
	email: z
		.string({ required_error: 'Don’t forget to enter your email address!' })
		.email('Check your email address.'),
	message: z
		.string({ required_error: 'Don’t forget to leave a message!' })
		.min(1, 'Don’t forget to leave a message!')
		.max(1024, 'That’s a long message, try getting to the point quicker!')
});
    ```
    Here, we specify two fields: `email` and `message`.  By default, with Zod, all fields are required, and you can add `.optional()` if the field is accepted, but not necessary.  `z.string()` details that we expect `email` to be a string, and `required_error` is the error message we want to emit, when the `email` field is missing.  `.email()` (line `6`) is all we need to ask Zod to check the field value is a valid email.  The string passed to `.email()` is the error message to emit when the value is not a valid email.  Similarly, for `message`, we set `min` and `max` lengths.<br/><br/>As a final step, we want to validate the form entries, on the server, using this new schema:
    - we validate the form entries in line `12`
    - note, we create a form entries plain JavaScript object in line `9`
    - if the validation fails, Zod throws an error, which we catch in the block starting at line `19`, and then return entered values and error messages, using the same format as earlier
    ```typescript
    import { fail, redirect } from '@sveltejs/kit';
    import type { Actions } from './$types';
    import { ZodError } from 'zod';
    import { contactFormSchema } from '$lib/schema';

    export const actions: Actions = {
	    contact: async ({ request }) => {
		    const form = await request.formData();
    		const formEntries = Object.fromEntries(form);

	    	try {
		    	const { email, message } = contactFormSchema.parse(formEntries);

			    // Remember to obfuscate any real, personally identifiable information (PII) from logs,
    			// which unauthorised personnel might have access to.
			console.log({ email, message });

	    		throw redirect(303, '/');
		    } catch (error: unknown) {
			    if (error instanceof ZodError) {
				    const errors = error.flatten();
				const { email, message } = formEntries;
				const { fieldErrors } = errors;
				return fail(400, {
					email: typeof email === 'string' ? email : '',
					message: typeof message === 'string' ? message : '',
					error: {
						    ...(fieldErrors?.email ? { field: 'email', message: fieldErrors.email[0] } : {}),
						    ...(fieldErrors?.message ? { field: 'message', message: fieldErrors.message[0] } : {})
					}
				});
			}
		}
	}
    };
    ```
    We need **zero** changes in the front end code, as the return value maintains the earlier format.
    ![SvelteKit Form Example: screen capture shows a contact form with email and message fields.  The email field contains the text "example.com" and has a feedback message below, which reads "Check your email address.".  The Message field contains the text "Just wanted to say you are doing a terrific job!" and has no feedback error message.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kujh3yqirtbnq7p8xg1f.png)

9. ⚾️ **Rethrow unhandled errors in SvelteKit handlers**.  This is possibly the easiest pitfall to fall into!  You might code everything up, test it, see the form submitting just fine, but then not get the expected redirect to the thank-you page!  This comes from the way SvelteKit manages redirects.  Like Remix, it throws redirects.  In the Zod code above, we had to catch an error.  The missing line, though, is a rethrow of any errors we did not handle.  Without adding that rethrow, we will catch the redirect in line `18` and just swallow it, preventing Svelte from redirecting.  An easy fix is to rethrow any errors which we do not handle:

    ```typescript
    import { fail, redirect } from '@sveltejs/kit';
    import type { Actions } from './$types';
    import { ZodError } from 'zod';
    import { contactFormSchema } from '$lib/schema';

    export const actions: Actions = {
	contact: async ({ request }) => {
		const form = await     request.formData();
		const formEntries =     Object.fromEntries(form);

		try {
			const { email, message } = contactFormSchema.parse(formEntries);

			// Remember to obfuscate any real, personally identifiable information (PII) from logs,
			// which unauthorised personnel might have access to.
			console.log({ email, message });

			throw redirect(303, '/');
		} catch (error: unknown) {
			if (error instanceof ZodError) {
				/* TRUNCATED... */
				return fail(400, { /* TRUNCATED... */ });
			}
			throw error;
		}
	}
    };
    ```

    If there is a Zod validation error, the code returns early (from line `22`), so the rethow in line `24` is not triggered.  Importantly, that rethrow will be invoked on the happy path, though!

10. 🔐 **Help users follow security best practices**.  For signup and login, consider implementing multifactor authentication (**MFA**), to help protect users from **credential stuffing** attacks.  Also encourage them to set strong and unique passwords.  See <a href="https://rodneylab.com/svelte-login-form-example/">more best-practices for SvelteKit login forms</a>.

## 🙌🏽 SvelteKit Form Example: Wrapping Up

In this post, we saw a concrete SvelteKit form example, as well as 10 mistakes to avoid.  More specifically, we saw:

- **how to pass form data** from the client to the server;
- how to **return feedback on invalid fields** from the server, **repopulating valid fields**; and
- debugging a common issue where **redirects do not work on SvelteKit server** handlers**.

Please <a aria-label="Open the Rodney Lab Git Hub repo" href="https://github.com/rodneylab/sveltekit-form-example">see the full repo code on the Rodney Lab GitHub repo</a>. The repo includes complete working examples from various stages in the journey above. I do hope you have found this post useful and can use the code in your own SvelteKit project.  Let me know if you have any suggestions for improvements.  Drop a comment below or <a href="https://rodneylab.com/contact/">reach out on other channels</a>.

## 🙏🏽 SvelteKit Form Example: Feedback

If you have found this post useful, see links below for further related content on this site. I do hope you learned one new thing from the video. Let me know if there are any ways I can improve on it. I hope you will use the code or starter in your own projects. Be sure to share your work on Twitter, giving me a mention, so I can see what you did. Finally, be sure to let me know ideas for other short videos you would like to see. Read on to find ways to get in touch, further below. If you have found this post useful, even though you can only afford even a tiny contribution, please <a aria-label="Support Rodney Lab via Buy me a Coffee" href="https://rodneylab.com/giving/">consider supporting me through Buy me a Coffee</a>.

Finally, feel free to share the post on your social media accounts for all your followers who will find it useful. As well as leaving a comment below, you can get in touch via <a href="https://twitter.com/messages/compose?recipient_id=1323579817258831875">@askRodney</a> on Twitter and also <a aria-label="Contact Rodney Lab via Telegram" href="https://t.me/askRodney">askRodney on Telegram</a>. Also, see <a aria-label="Get in touch with Rodney Lab" href="https://rodneylab.com/contact/">further ways to get in touch with Rodney Lab</a>. I post regularly on <a aria-label="See posts on svelte kit" href="https://rodneylab.com/tags/sveltekit/">SvelteKit</a> as well as <a href="https://rodneylab.com/tags/seo/" aria-label="See posts on search engine optimisation">Search Engine Optimization</a> among other topics. Also, <a href="https://newsletter.rodneylab.com/issue/latest-issue">subscribe to the newsletter to keep up-to-date</a> with our latest projects.