Opens an external site in a new window
Mental Health Awareness Month
“Community”
RODNEY LAB
  • Home
  • Plus +
  • Newsletter
  • Links
  • Profile
RODNEY LAB
  • Home
  • Plus +
  • Newsletter
  • Links

SvelteKit Form Example with 10 Mistakes to Avoid 🛟 # SvelteKit Form Example with 10 Mistakes to Avoid 🛟 #

blurry low resolution placeholder image SvelteKit Form Example
  1. Home Rodney Lab Home
  2. Blog Posts Rodney Lab Blog Posts
  3. SvelteKit SvelteKit Blog Posts
<PREVIOUS POST
NEXT POST >
LATEST POST >>

SvelteKit Form Example with 10 Mistakes to Avoid 🛟 #

Updated 4 months ago
10 minute read
Gunning Fog Index: 5.9
Content by Rodney
blurry low resolution placeholder image Author Image: Rodney from Rodney Lab
SHARE:

🤞🏽 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‑one.

Let’s start with that base example. We will later add feedback for missing fields, server-side validation using Zod and user experience enhancements.

🧱 Minimal SvelteKit Form Example #

blurry low resolution placeholder image SvelteKit Form Example: screen capture shows a contact form with email and message fields, and a send button.  All fields are blank.
SvelteKit Form Example: What we’re building

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

src/routes/basic-start/+page.svelte
svelte
    
1 <script lang="ts">
2 </script>
3
4 <main>
5 <h1>Contact form</h1>
6 <form method="POST">
7 <p>Leave a message</p>
8 <label for="email">Email</label>
9 <input
10 id="email"
11 type="email"
12 required
13 name="email"
14 autocomplete="username"
15 placeholder="[email protected]"
16 />
17 <label for="message">Message</label>
18 <textarea id="message" required name="message" placeholder="Leave your message…" rows={4}></textarea>
19 <button type="submit">Send</button>
20 </form>
21 </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 #

src/routes/basic-start/+page.server.ts
typescript
    
1 import { redirect } from '@sveltejs/kit';
2 import type { Actions } from './$types';
3
4 export const actions: Actions = {
5 default: async ({ request }) => {
6 const form = await request.formData();
7 const email = form.get('email');
8 const message = form.get('message');
9
10 if (typeof email === 'string' && typeof message === 'string') {
11 // Remember to obfuscate any real, personally identifiable information (PII) from logs,
12 // which unauthorised personnel might have access to.
13 console.log({ email, message });
14
15 // Add logic here to process the contact form.
16 }
17
18 redirect(303, '/');
19 }
20 };

+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 #

src/routes/basic-start/+page.server.ts — click to expand code.
src/routes/basic-start/+page.server.ts
typescript
    
1 import { fail, redirect } from '@sveltejs/kit';
2 import type { Actions } from './$types';
3 import { POSTMARK_SERVER_TOKEN } from '$env/static/private';
4
5 export const actions: Actions = {
6 default: async ({ request }) => {
7 const form = await request.formData();
8 const email = form.get('email');
9 const message = form.get('message');
10
11 if (typeof email === 'string' && typeof message === 'string') {
12 await fetch('https://api.postmarkapp.com/email', {
13 method: 'POST',
14 headers: {
15 Accept: 'application/json',
16 'Content-Type': 'application/json',
17 'X-Postmark-Server-Token': POSTMARK_SERVER_TOKEN
18 },
19 body: JSON.stringify({
20 From: '[email protected]',
21 To: email,
22 Subject: 'Contact form message',
23 TextBody: JSON.stringify({
24 email,
25 message
26 }, null, 2),
27 MessageStream: 'outbound'
28 })
29 });
30 }
31
32 redirect(303, '/');
33 }
34 };

See Postmark docs for more on the REST API .

🗳 Poll #

Do you use Zod for data validation?
Voting reveals latest results.

⚠️ 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 @sveltejs/adapter-cloudflare  or @sveltejs/adapter-netlify . Also, make sure you do not have prerender directives in your server code for form handling routes.

    You can build a completely static site with forms in SvelteKit, though you will probably want to use edge functions or serverless functions to handle form submissions. The techniques in this post will not work with prerendered pages.
  2. 📮 Most of the time you will want the POST 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 the user just needs to request a document . See MDN HTTP request method docs 
  3. 🫱🏾‍🫲🏼 Add an action attribute to the form element for named handlers. 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. This approach also helps avoid the SvelteKit “no action with name 'default' found” error.
    src/routes/+page.svelte
    svelte
        
    1 <form action="?/contact" method="POST">
    2 <!-- TRUNCATED -->
    3 </form>
    src/routes/+page.server.ts
    typescript
        
    1 export const actions: Actions = {
    2 contact: async ({ request }) => {
    3 const form = await request.formData();
    4 /* TRUNCATED... */
    5 }
    6 }
  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).
  6. src/routes/+page.server.ts
    typescript
        
    1 import { fail, redirect } from '@sveltejs/kit';
    2 import type { Actions } from './$types';
    3
    4 export const actions: Actions = {
    5 contact: async ({ request }) => {
    6 const form = await request.formData();
    7 const email = form.get('email');
    8 const message = form.get('message');
    9
    10 if (typeof email !== 'string') {
    11 return fail(400, {
    12 message: typeof message === 'string' ? message : '',
    13 error: { field: 'email', message: 'Check your email address.' }
    14 });
    15 }
    16
    17 if (typeof message !== 'string') {
    18 return fail(400, {
    19 email,
    20 error: { field: 'message', message: 'Don’t forget to leave a message!' }
    21 });
    22 }
    23 /* TRUNCATED... */
    24 }
    25 }

    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.

    10 SvelteKit Form Mistakes and how to Avoid Them (6 – 10) #

    1. 🤗 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.
      src/routes/+page.svelte
      svelte
          
      1 <script lang="ts">
      2 import type { ActionData } from './$types';
      3
      4 let { form }: { form: ActionData } = $props();
      5
      6 let { email, error, message } = $state(
      7 form ?? {
      8 email: '',
      9 error: { field: '', message: '' },
      10 message: '',
      11 },
      12 );
      13 </script>
      14
      15 <main>
      16 <h1>Contact form</h1>
      17 <form action="?/contact" method="POST">
      18 <p>Leave a message</p>
      19 <label for="email">Email</label>
      20 <input
      21 id="email"
      22 type="email"
      23 required
      24 name="email"
      25 autocomplete="username"
      26 placeholder="[email protected]"
      27 aria-invalid={error?.field === 'email'}
      28 aria-describedby={error?.field === 'email' ? 'email-error' : undefined}
      29 />
      30 {#if error?.field === 'email'}
      31 <small id="email-error">{error?.message}</small>
      32 {/if}
      33 <!-- TRUNCATED... -->
      34 </form>
      35 </main>

      SvelteKit plumbing makes the email, error and message variables returned from the action available on the frontend, via export let form. See the Grammar check post for an explanation of why there is a double assignment for these identifiers.

      Any email field error will be displayed by the template code in lines 30 – 32. Notice, we will display the exact message set on the server, saving us re-creating a message. The id on the small element in line 31 is important to link the erroneous field, using an aria-describedby attribute in line 28. Code for the message input is similar.

    2. 🙏🏽 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:
      src/routes/+page.svelte
      svelte
          
      1 <script lang="ts">
      2 import type { ActionData } from './$types';
      3
      4 let { form }: { form: ActionData } = $props();
      5
      6 let { email, error, message } = $state(
      7 form ?? {
      8 email: '',
      9 error: { field: '', message: '' },
      10 message: '',
      11 },
      12 );
      13 </script>
      14
      15 <main>
      16 <h1>Contact form</h1>
      17 <form action="?/contact" method="POST">
      18 <p>Leave a message</p>
      19 <label for="email">Email</label>
      20 <input
      21 value={email}
      22 id="email"
      23 type="email"
      24 required
      25 name="email"
      26 autocomplete="username"
      27 placeholder="[email protected]"
      28 aria-invalid={error?.field === 'email'}
      29 aria-describedby={error?.field === 'email' ? 'email-error' : undefined}
      30 />
      31 <!-- TRUNCATED... -->
      32 </form>
      33 </main>
    3. 🏋🏽 Let Zod do the heavy lifting. Writing and testing validation code can eat up a lot of time. Use a validation library like Zod  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:
      src/lib/schema.ts
      typescript
          
      1 import { z } from 'zod';
      2
      3 export const contactFormSchema = z.object({
      4 email: z
      5 .string({ required_error: 'Don’t forget to enter your email address!' })
      6 .email('Check your email address.'),
      7 message: z
      8 .string({ required_error: 'Don’t forget to leave a message!' })
      9 .min(1, 'Don’t forget to leave a message!')
      10 .max(1024, 'That’s a long message, try getting to the point quicker!')
      11 });

      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.

      As a final step, we want to validate the form entries, on the server, using this new schema:

      src/routes/with-zod-validation/+page.server.ts
      typescript
          
      1 import { fail, redirect } from '@sveltejs/kit';
      2 import type { Actions } from './$types';
      3 import { ZodError } from 'zod';
      4 import { contactFormSchema } from '$lib/schema';
      5
      6 export const actions: Actions = {
      7 contact: async ({ request }) => {
      8 const form = await request.formData();
      9 const formEntries = Object.fromEntries(form);
      10
      11 try {
      12 const { email, message } = contactFormSchema.parse(formEntries);
      13
      14 // Remember to obfuscate any real, personally identifiable information (PII) from logs,
      15 // which unauthorised personnel might have access to.
      16 console.log({ email, message });
      17
      18 redirect(303, '/');
      19 } catch (error: unknown) {
      20 if (error instanceof ZodError) {
      21 const errors = error.flatten();
      22 const { email, message } = formEntries;
      23 const { fieldErrors } = errors;
      24 return fail(400, {
      25 email: typeof email === 'string' ? email : '',
      26 message: typeof message === 'string' ? message : '',
      27 error: {
      28 ...(fieldErrors?.email ? { field: 'email', message: fieldErrors.email[0] } : {}),
      29 ...(fieldErrors?.message ? { field: 'message', message: fieldErrors.message[0] } : {})
      30 }
      31 });
      32 }
      33 }
      34 }
      35 };
      • 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
      • We need zero changes in the front end code, as the return value maintains the earlier format.

    4. blurry low resolution placeholder image 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.
      SvelteKit Form Example: Validation
      1. ⚾️ 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:
        src/routes/with-zod-validation/+page.server.ts
        typescript
            
        1 import { fail, redirect } from '@sveltejs/kit';
        2 import type { Actions } from './$types';
        3 import { ZodError } from 'zod';
        4 import { contactFormSchema } from '$lib/schema';
        5
        6 export const actions: Actions = {
        7 contact: async ({ request }) => {
        8 const form = await request.formData();
        9 const formEntries = Object.fromEntries(form);
        10
        11 try {
        12 const { email, message } = contactFormSchema.parse(formEntries);
        13
        14 // Remember to obfuscate any real, personally identifiable information (PII) from logs,
        15 // which unauthorised personnel might have access to.
        16 console.log({ email, message });
        17
        18 redirect(303, '/');
        19 } catch (error: unknown) {
        20 if (error instanceof ZodError) {
        21 /* TRUNCATED... */
        22 return fail(400, { /* TRUNCATED... */ });
        23 }
        24 throw error;
        25 }
        26 }
        27 };

        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!

      2. 🔐 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 more best-practices for SvelteKit login forms.
      3. 🙌🏽 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 see the full repo code on the Rodney Lab GitHub repo . 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 reach out on other channels.

        🏁 SvelteKit Form Example: Summary #

        How can I fix server redirects not working in SvelteKit? #

        A common cause of redirects inexplicably failing in SvelteKit server code is accidental consumption of SvelteKit thrown objects. SvelteKit uses the throw mechanism (usually used with errors) to handle redirects. Often, you will add a try-catch block in your server handler to manage any exceptions in your own logic. In doing so, it is easy unwittingly to consume SvelteKit thrown objects like redirects. Luckily, this is quick to remedy. Just make sure you rethrow any errors which are not handled by your code. That way, thrown redirects live long enough for SvelteKit to act on them!

        How can you do server-side form validation in SvelteKit? #

        Zod is a great fit for server-side form validation in SvelteKit. Zod works performs validation on plain JavaScript object. To get the ball rolling, convert your SvelteKit action form data by calling `Object.fromEntries(form)` on it. Next, you will need a schema. Remember, any fields in your schema are assumed required, by default, though you can chain on a .optional() method, where required. Finally, you will find the flatten() method on Zod errors useful when manipulating errors, to create feedback.

        How do SvelteKit form action names and default work? #

        Use default, for your action name, in the server code if you do not provide an action attribute value in the form element markup. However, to make your code easier to follow, you might want to name the handler more descriptively. Let’s say you want to name it `login`. In this case, add an `action` attribute to the form element tag in the markup: `<form … action="?/login">`

        🙏🏽 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 consider supporting me through Buy me a Coffee.

        blurry low resolution placeholder image ask Rodney X (formerly Twitter) avatar

        Rodney

        @askRodney

        Just dropped a new post looking at 10 SvelteKit form errors to avoid.

        Starting with a basic contact form, we add:
        - internal email alert with 📮

        @postmarkapp;
        - validation with Zod,
        - user feedback & a11y enhancements.

        Hope you find it useful!https://t.co/IYBhVsMd2A

        — Rodney (@askRodney) August 7, 2023

        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 @askRodney on Twitter and also askRodney on Telegram . Also, see further ways to get in touch with Rodney Lab. I post regularly on SvelteKit as well as Search Engine Optimization among other topics. Also, subscribe to the newsletter to keep up-to-date with our latest projects.

Thanks for reading this post. I hope you found it valuable. Please get in touch with your feedback and suggestions for posts you would like to see. Read more about me …

blurry low resolution placeholder image Rodney from Rodney Lab
TAGS:
SVELTEKIT

Likes:

Likes

  • Colin profile avatar
  • Geoff Rich profile avatar
Likes provided by Mastodon & X via Webmentions.

Related Posts

blurry low resolution placeholder image Rust Cloudflare Workers: Turnstile Example 🤖

Rust Cloudflare Workers: Turnstile Example 🤖

rust
serverless
<PREVIOUS POST
NEXT POST >
LATEST POST >>

Leave a comment …

Your information will be handled in line with our Privacy Policy .

Ask for more

1 Nov 2022 — Astro Server-Side Rendering: Edge Search Site
3 Oct 2022 — Svelte eCommerce Site: SvelteKit Snipcart Storefront
1 Sept 2022 — Get Started with SvelteKit Headless WordPress

Copyright © 2020 – 2025 Rodney Johnson. All Rights Reserved. Please read important copyright and intellectual property information.

  • Home
  • Profile
  • Plus +
  • Newsletter
  • Contact
  • Links
  • Terms of Use
  • Privacy Policy
We use cookies  to enhance visitors’ experience. Please click the “Options” button to make your choice.  Learn more here.