DEV Community

MartinJ
MartinJ

Posted on • Edited on

NgSysV2-3.2: Creating a Serious Svelte IS: Form validation and Exception handling

This post series is indexed at NgateSystems.com. You'll find a super-useful keyword search facility there too.

Last reviewed: Nov '24

1. Introduction

So far, this series has concentrated on the basic functionality of your webapp. Little attention has been given to the "edge cases" that can "crash" your code.

Your webapp will be judged harshly if it doesn't handle problems caused by faulty input, program errors and network issues. This post describes what you must do to make it operate reliably in the noisy, dangerous and "exciting" world of a production environment.

2. Input validation

Of all the tedious tasks that will fall your way as a developer, input validation is probably the most fiddly and annoying. But this is an area where you must be absolutely on top of your game. Poor design of the form and its error-signalling arrangements will upset your users. Inadequate filtering of bad data will upset your database.

In an ideal world, input validation would be performed entirely in +page.svelte files on the user's client device. Here you can use the browser's excellent interactive facilities to make the process a positive pleasure for your users. But, the client environment is insecure. A skilful and determined "malicious operator" could, in principle, bypass client-side validation checks.

Validation is so tightly bound up with the ergonomics of data capture that it's not sensible to relegate this entirely to the actions() function in the +page.server.js file. The answer is to continue to perform validation client-side in +page.svelte and duplicate these checks server-side in +page.server.js.

Let's create a new version of the "New Product Registration" form you saw in Post 2.3. The original version collected new "Product Numbers" but didn't take the trouble to check that the input was actually numeric. The new version will ensure that the <form> code in +page.svelte delivers only valid input to the database update code in +page.server.js. That's easy enough, but this version of the webapp will also aim to guide the user, helpfully, through the process.

A popular design arrangement will initially style both the "Product Number" input field and "Register" button with light borders and pale backgrounds. These signal that the fields are awaiting input.

As soon as valid input is detected, the fields will change their characteristics to indicate that the webapp is happy with what it sees - the borders will thicken and the submit button will acquire a healthy green background. But, if a non-numeric field is entered, the position will be reversed: the submit button and the offending input field will assume an error state with red text and an accompanying error message.

This arrangement ensures that the user is always clear about the state of their transaction, is informed about problems as soon as they are identified and is positioned to correct them immediately.

The proposed scheme might be coded in various ways, but here's a solution that works well for me.

Use the code below to create a new +page.svelte file in a new src/routes/products-maintenance route. (I'm creating a new route here in case you want to refer back to the old version in src/routes/+page.svelte)

// src/routes/products-maintenance/page.svelte - remove before running
<script>
    import { productNumberIsNumeric } from "$lib/utilities/productNumberIsNumeric";

    let productNumber;

    let productNumberClass = "productNumber";
    let submitButtonClass = "submitButton";

    export let form;
</script>

<!-- display the <form> only if it has not yet been submitted-->
{#if form === null}
    <form method="POST">
        <label>
            Product Number
            <input
                bind:value={productNumber}
                name="productNumber"
                class={productNumberClass}
                type=text
                on:input={() => {
                    if (productNumberIsNumeric(productNumber)) {
                        submitButtonClass = "submitButton validForm";
                        productNumberClass = "productNumber";
                    } else {
                        submitButtonClass = "submitButton error";
                        productNumberClass = "productNumber error";
                    }
                }}
            />
        </label>

        {#if productNumberClass === "productNumber error"}
            <span class="error">Invalid input. Please enter a number.</span>
        {/if}

        <button class={submitButtonClass}>Register</button>
    </form>
{:else if form.validationSucceeded && form.dbUpdateSucceeded}
    <p>Form submitted successfully.</p>
{:else}
    <p>Form submission failed! : Error is : {form.dbUpdateError}</p>
{/if}

<style>
    .productNumber {
        border: 1px solid black;
        height: 1rem;
        width: 5rem;
        margin: auto;
    }

    .submitButton {
        display: inline-block;
        margin-top: 1rem;
    }

    .error {
        color: red;
    }

    .validForm {
        background: palegreen;
    }
</style>
Enter fullscreen mode Exit fullscreen mode

Now use the code below to create a productNumberIsNumeric function in a src/lib/utilities/productNumberIsNumeric.js file. In other circumstances, you might have created this function within the <script> section of the +page.svelte file above. But, as you'll see in a moment, the code will also be referenced in an associated +page.server.js file. Deploying it as a module (ie with an export declaration) means that it can be shared.

// src/lib/utilities/productNumberIsNumeric.js
export function productNumberIsNumeric(valueString) {
    // returns true if "value" contains only numeric characters, false otherwise

    if (valueString.length === 0) return false;
    for (let char of valueString) {
        if (char < '0' || char > '9') {
            return false;
        }
    }
    return true;
};
Enter fullscreen mode Exit fullscreen mode

If you now start your devserver and enter the http://localhost:5173/products-maintenance route address in your browser, you can try the form out. Enter a number and the "submit" button should glow green. Add a letter and everything will turn red. Clear the error and things should perk up again. Here's a screenshot of the form-validation page in its error state:

form-validation screenshot

Don't press the "Submit" button though because you haven't got an actions function declared yet. Here it is. Save the following in a src/routes/form-validation/+page.server.js file,

// src/routes/products-maintenance/+page.server.js - fragment
import { productNumberIsNumeric } from "$lib/utilities/productNumberIsNumeric";

export const actions = {
  default: async ({ request }) => {
    const input = await request.formData();
    const productNumber = input.get("productNumber");

    if (productNumberIsNumeric(productNumber)) {

      // database update code goes here. Success is assumed

      return { validationSucceeded: true, dbUpdateSucceeded: true};
    } else {
      return { validationSucceeded: false };
    }
  }

};
Enter fullscreen mode Exit fullscreen mode

The database update code isn't included here because I'm trying to keep this short - you'll see a full version later. But at least you can see the code that creates the return object that communicates the outcome of the validation and update code back to the calling page. The mechanism uses the form variable exported from +page.svelte. When the action returns a { validationSucceeded: true } object, the value of the validationSucceeded property becomes available to +page.svelte as form.validationSucceeded.

The "template" section of +page.svelte introduces some new features, so I'll quickly walk you through these. The code needs to maintain a state variable to describe the valid/invalid condition of the productNumber variable. A flag field with values "valid" or "invalid" might have been used here, but a trick defining the state as display styles provides a neater solution.

When the field is invalid you want its <input> tag to turn red. Previously, you've seen this done by adding a style="color: red" qualifier to the tag. In this case, because the <input> needs several styles, I've used an error class qualifier declared in the <style> section of the template to provide the equivalent of style="color: red". Similarly, I've used a class called productNumber to deliver the styling of a valid <input> tag. A valid <input> can then be displayed as <input class="productNumber and an errored <input> field as <input class="productNumber error" statement.

When you add classes together like this they simply pool their component styles. Note, however, that the order in which you appear is important. When component styles conflict, classes to the right in the list take precedence over styles to the left. In this case, the productNumber class doesn't include a color style, but even if it did, the error class's color: red would win.

The trick now is to define the styling of productNumber with an productNumberClass variable that takes values of either "productNumber" or "productNumber error". You can then use this to control the styles of the <input> field display as follows:

            <input
                bind:value={productNumber}
                name="productNumber"
                class={productNumberClass}
Enter fullscreen mode Exit fullscreen mode

When you need to see if the "Numeric Input Field" is in an error state (to decide whether you need to display an error message) you simply check if it has been given an error style, as follows:

       {#if productNumberClass === "productNumber error"}
Enter fullscreen mode Exit fullscreen mode

As you've probably already realised, the server-side validation performed in +page.server.js here is rather stupid because the actions() function will only be called when you've got a valid form. It could only be called with invalid data if you'd somehow managed to hack the client-side code. But this is certainly possible so, where the stakes are high, it makes sense to repeat the checks.

If the server-side validation checks did fail it's also unlikely that you would want to return the hacker a courteous response (the code above doesn't). But, in the next section of this post, you'll see other problems that you may want to tell +page.svelte about.

3. Exception handling

If the +page.server.js file has to update a database, it may encounter hardware problems. While you can't stop these problems from happening, you can ensure that the consequences are managed gracefully - that users are informed of the situation and clean-up actions are taken, where possible.

Your question now should be "How do you know an error has occurred and how do you find out what type of error it is?"

Library functions like the Firestore API calls that you used earlier to read and write documents to database collections signal that something has gone wrong by "throwing" an "exception". This looks like this:

throw new Error("Structured error message");
Enter fullscreen mode Exit fullscreen mode

Unless you've taken precautions, a "throw" statement terminates the program and displays a system error message. But a Javascript "catch .. try" block allows you to forestall this action and handle it gracefully. It looks like this:

try {
  vulnerableCodeBlock;
} catch (error) {
  handleErrorCodeBlock
}
Enter fullscreen mode Exit fullscreen mode

This says "Try to run this vulnerableCodeBlock and pass control to the the handleErrorCodeBlock block if it throws an exception". If the catch block is called, its error parameter will receive the Structured error message registered by the exception. Then the handleErrorCodeBlock can inspect this and close the situation down appropriately.

It clearly only makes sense to use "try" bocks when there is serious concern that code may fail, but database i/o would certainly be a candidate. Just to be clear about the arrangement, here's the Firestore "register product" code from Post 2.3 rigged with a "try" block. With this in place, if any of the Firestore API calls fail, control will be passed to the catch block to engineer a soft landing.

// src/routes/products-maintenance/+page.server.js
import { productNumberIsNumeric } from "$lib/utilities/productNumberIsNumeric";

import { collection, doc, setDoc } from "firebase/firestore";
import { initializeApp, getApps, getApp } from 'firebase/app';
import { getFirestore } from "firebase/firestore";

const firebaseConfig = {
  apiKey: "AIzaSyDOVyss6etYIswpmVLsT6n-tWh0GZoPQhM",
  authDomain: "svelte-dev-80286.firebaseapp.com",
  projectId: "svelte-dev-80286",
  storageBucket: "svelte-dev-80286.firebasestorage.app",
  messagingSenderId: "585552006025",
  appId: "1:585552006025:web:e41b855f018fcc161e6f58"
};

// see Post 3.3 for an explanation of the "ternary" expression used below
const firebaseApp = !getApps().length ? initializeApp(firebaseConfig) : getApp();
const db = getFirestore(firebaseApp);

export const actions = {
  default: async ({ request }) => {
    const input = await request.formData();
    const productNumber = input.get("productNumber");

    const validationResult = productNumberIsNumeric(productNumber);

    if (validationResult) {
      try {
        const productsDocData = { productNumber: parseInt(productNumber, 10), productDescriptiom: "default" }
        const productsCollRef = collection(db, "products");
        const productsDocRef = doc(productsCollRef);
        await setDoc(productsDocRef, productsDocData);

        return { validationSucceeded: true, dbUpdateSucceeded: true };

      } catch (error) {

        return { validationSucceeded: true, dbUpdateSucceeded: false, dbUpdateError: error.message };
      }
    } else {

      return { validationSucceeded: false };
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

Now that you've got a full version of the actions() function, feel free to try the code out at http://localhost:5173/products-maintenance`

When you try the "submit" button now, the webapp should use itsactions() function's return mechanism to respond with a "Form submitted successfully." message.

You can use the original http://localhost:5173 version of the "Products Maintenance Page" to check that products are being correctly added to your database.

4. Summary

If you've managed to follow this post you should now have some idea of what you need to know to write useful and reliable code. Good form design is much appreciated by users and can make or break your system. Error-tolerant code will likewise win you many votes.

But you're not done yet. There are other ways in which your system might be compromised. Your Firestore database is presently sitting on a publicly accessible server with access rules that permit anybody to read and write to it. Likewise, all of your webapp pages are accessible to anyone who knows their URLs. The next section of this post series tells you how to create a "login" page that restricts database access to users who can authenticate themselves with a password. I hope you'll read on.

Top comments (0)