This post forms part of a series indexed at NgateSystems.com. You'll find a super-useful keyword search facility here too.
Last reviewed: Nov 24
1. Introduction
Thus far, this series has concentrated on basic functionality. Little attention has been given to the "edge cases" that can "crash" your webapp. This post considers the extra code you must write to make your webapp robust and fit to be launched into the noisy, dangerous world of a production environment.
An edge case is a situation or problem that occurs outside of expected parameters or procedures and at the extreme ends of a range of possible values.
For example, when an input form is used to create or update a Firestore document, data may be invalid or incompatible with existing records. The web and your Firestore server may not always be in a healthy state either. Your webapp will be judged harshly if it doesn't provide a cushion to handle or mitigate such issues. This post will describe how to handle both anticipated and unanticipated exceptions.
2. Form validation
Of all the tedious tasks that will fall your way as a developer, input validation is probably one of 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, and inadequate filtering of error conditions will upset your database.
When you are using +page.svelte
and +page.server.js
files in combination, your validation arrangements will be spread across both client and server. On the client, you can use the browser's interactive facilities to deliver excellent UX (user experience) and initial input validation. On the server, you can use its secure environment to assure yourself that the data really is valid and complete the database update in complete privacy. This might seem paranoid, but client code is not secure and a determined hacker could bypass validation procedures here.
Let's create a new version of the "New Product Registration" form you saw in Post 2.1 with input validation and the sort of UX that users expect.
A popular design arrangement would initially style both the "Product Number" input field and "Register" button, both with light borders and pale backgrounds indicating that they are waiting for user action.
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 might be coded in many different ways, but here's one solution that works well for me.
Use the code below to create a +page.svelte
file in a new src/routes/products-maintenance
route.
// 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}
on:input={() => {
if (productNumberIsNumeric(productNumber.value)) {
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.databaseUpdateSuccess}
<p>Form submitted successfully.</p>
{:else}
<p>Form submission failed! : Error is : {form.databaseError}</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>
Now use the code below to create an 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 required 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;
};
If you now start your dev
server 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:
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
import { productNumberIsNumeric } from "$lib/utilities/productNumberIsNumeric";
export const actions = {
default: async ({ request }) => {
const input = await request.formData();
const productNumber = input.get("productNumber");
const validationResult = productNumberIsNumeric(productNumber);
return { success : validationResult};
}
};
When you try the "submit" button now, the webapp should respond with a "Form submitted successfully." message.
This code introduces some new features so I'll quickly walk you through these.
The "code template" section of +page.server
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. In the 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}
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"}
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 for an invalid form if you'd somehow managed to hack the client-side code and bypass its checks. But this is certainly possible so, for critical operations, the client-side checks should be repeated server-side.
If the server-side validation checks did fail it's unlikely that you would want to give the hacker a courteous response, but in the next section of this post you'll be looking at other problems that might be encountered. So let's have a look at how +page.server.js
reports back on what it has found.
The export let form;
statement in +page.svelte
both declares form
as a variable and marks it as a "prop" or "property" of the code. This means it can be set outside the component - specifically by the "actions" function in the associated +page.server.js
file. The form
variable is thus how +page.server.js
returns its response. This is, of course, identical to the export let data
pattern used in post 2.3 to return data from a +page.server.js
load() function.
Let's look at situations where your +page.server.js
might have good reason to return a response to +page.svelte
.
3. Exception handling
If the +page.server.js
file has to update a database with the form data, it may encounter unforeseeable 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 all possible clean-up actions are taken.
Your questions 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 you used earlier to read and write documents to database collections signal that something has gone wrong by "throwing" an "exception" using a "throw" statement. It looks like this:
throw new Error("Structured error message");
Unless you've taken precautions, a "throw" statement terminates the program and displays a system error message. But a Javascript arrangement called a "catch .. try" block allows you to forestall this action and handle it gracefully. It looks like this:
try {
blockOfVulnerableCode;
} catch (error) {
handleErrorCode
}
This says "Try to run this "blockOfVulnerableCode" and pass control the catch 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 "handleErrorCode" code can inspect this and close the situation down appropriately.
Obviously, it 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 } 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.appspot.com",
messagingSenderId: "585552006025",
appId: "1:585552006025:web:e41b855f018fcc161e6f58"
};
const firebaseApp = initializeApp(firebaseConfig);
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 { validationSuccess: true, databaseUpdateSuccess: true };
} catch (error) {
return { validationSuccess: true, databaseUpdateSuccess: false, databaseError: error.message };
}
} else {
return { validationSuccess: true, databaseUpdateSuccess: false };
}
}
};
Since, at this point, you are working on the server, you can't communicate directly with the user. But once +page.svelte
regains control, you can use the form.databaseUpdateSuccess
value returned by +page.server.js
to trigger a popup and display a suitable message.
4. Summary
If you've managed to follow the "Form you now have most of what you need to know to write useful and reliable code. Good form design is appreciated highly by users and can make or break your system. Error-tolerant code will likewise win you many votes.
But you're not done yet. Your Firestore database is presently sitting on a publicly accessible server with access rules that permit anybody to read and write to it. All of your webapp pages are accessible to anyone who knows their URL. The next section of this post series tells you how to fix this. Specifically, it shows you how you can create a "login" page that restricts access to users who can authenticate themselves with a password. I hope you'll read on.
Top comments (0)