NgSysV2-2.3: Creating a simple Svelte Information System with Google's Firestore
MartinJ
Posted on November 26, 2024
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
Most webapps exist purely to create and access shared information. Consider, Amazon's https://www.amazon.co.uk/
website. The essential purpose of this system is to let you browse a central collection of product details, place orders and monitor progress on delivery. To make this work, Amazon must:
- Maintain this information somewhere accessible over the web
- Structure and manage it to ensure near-instant access and total integrity.
This post is all about the "database" technology used to achieve these aims.
Warning - this is a long post because database reads and writes in Svelte draw you remorselessly into using SvelteKit's client-server architecture. Previously, your code ran exclusively "client-side" in your web browser. Now you'll also be running code on the local server launched by npm run dev
. This has consequences...
I've looked at ways of splitting the post up but they don't work. To make matters worse, the Javascript you'll use contains many new features. So, I'm sorry - you're just going to have to suck it up.
But look on the bright side. Once you're through this, things will start to get easier. Take it slowly. Use chatGPT where you feel I've not explained something clearly. You'll find the bot particularly helpful when you need advice on JavaScript syntax. Relax. This is going to be fun!
2. Configuring your project to use Google's Firestore
There are innumerable ways of storing shared data on the web. This post series uses Google's Firestore system because it suits beginners. It requires minimal setup and fits comfortably within the structure of a Svelte webapp.
You'll need to perform four initial steps:
- Obtain a Google account
- Create a Firebase project under this account
- Register your "webapp"
- Initialise a Firestore Database for your Firebase project
Firebase is Google's umbrella term for many different services you might use to mount a simple project on the web. The services for a given account are managed via Google's "Firebase console" at https://console.firebase.google.com/
. They include both a "Storage" service that enables you to upload files into the Google Cloud and a "Firestore Database" service. A database differs from a file in that it possesses a configurable structure. It enables you to access and update discrete elements of the configured data set.
2.1 Obtaining a Google account
If you have a Gmail address, you're already covered because this automatically counts as a Google account. If you don't, follow the instructions at Create a Google Account to get one.
2.2 Creating a Firebase project for your code
Launch the Google Firebase console and log in with your Google Account (noting that, if you're logged into Gmail with this, you're already logged into the Firebase Console as well). Now click the "Create a Project" box to launch the process.
Google will want you to supply a name for your project (I suggest you use the project name you're using in VSCode), and will propose an extension that makes this a unique "Project Identifier" within the Firebase world. So for, example, my version of the "Svelte-dev" project used in this post series is known to Google as "Svelte-dev-afbaf".
As an aside, since the Project Identifier will ultimately form part of the default live URL for your webapp, and since Google lets you edit its initial "uniqueness extension" proposal, you might be tempted to try to change this to something meaningful. However, I suggest you forget this idea. Firstly, you'll find it difficult to pick an identifier that suits both parties. Secondly, in my experience, these "default URLs" aren't ever indexed by Google. A "custom URL" purchased at minimal cost and linked to your default URL when you go live is by far the best way to get a memorable URL.
Now click "Continue" to reveal a "Google Analytics" registration page. You can safely ignore this here as it is relevant only to performance issues on live apps. Use the slider bar to decline it and click the "Create Project" button to continue.
The lights now dim a little as Google registers your project. Finally, once you've clicked one more "Continue" button, you'll find yourself in your project's Firebase Console window. Here's a screenshot of the Firestore tab for a "svelte-dev" project:
It's worth giving yourself a moment to familiarise yourself with this page because it is a little complicated. The basic structure consists of a "tools menu" on the left that determines what gets displayed in the main panel on the right. The problem is that the menu is "adaptive" and maintains a "Project shortcuts" section that remembers where you've been. Consequently, the menu seems to look different every time you open the console! Once you've grasped this feature, however, things work well. Note that the complete set of tools is hidden within the "Build", "Run" and "Analytics" submenus of their parent "Product Categories" menu item. The "Build" set contains everything you need at present.
Before you proceed further, note the following:
- information at the top of the screen confirms that the svelte-test project is currently registered on the "Spark" plan. This means that everything you're doing at present is free. Eventually, in this post series, you will get to a point where you need to start to paying Google, and will need to upgrade your project to the "Blaze" tariff. But don't worry - this is a long way off yet, you won't pay much, and you can create a monthy budget to limit the amount Google might try to charge you.
- project details are revealed by clicking the "Project Overview" button at the top of the toolbar. Details available here include a reminder of the Project Identifier and a button to delete the project. If everything goes wrong, you can always use this to rub out the mess and start over again. This won't cost you anything
2.3 Registering your webapp
Firebase needs to know what your webapp will be called:
- Click the
</>
icon below the "Get started" message and supply a nickname when requested. I suggest you use your project name again here (eg "svelte-dev"). - Ignore the offer to "Set up Firebase Hosting for this app" because, when we finally get around to deployment, all your hosting needs will be handled by App Engine.
- Finally, click "Register" and "Continue to the console" to return to the initial console screen.
2.4 - Initialising a Firestore database
Select "Firestore Database" from the "Build" stack in the tools menu to obtain the Firebase console view shown below:
Once you've clicked the "Create Database" button, the console will want you to:
Set your database "Name and Location". "Name" is the identifier for the database and will only be relevant if you plan to create several different databases in your project. Leave this empty, for now, so that Google uses its "default" setting. "Location" specifies where your database will be physically located. The pull-down list of options available here is possibly your first sight of the scale of the Google Cloud service. Its server farms are available right around the globe. You will probably want to select a server close to your location. For example, I generally use "europe-west2 : Heathrow" since I am based in the UK. Pages elsewhere in the Google Cloud console enable you to specify performance and availability characteristics, but you don't need to look at these for now.
Secure your database with "Rules". The screen here offers you a choice between setting initial "production" and "test" "rules". This only makes sense, of course, if you know what "rules" are in the first place - and this isn't the right time for me to explain them. Unless you know better, I'd like you to check the "test mode" option here. Be assured, I'll come back to this later when I talk about "authorisation" (and, oh boy, is there a lot to talk about!).
Once you're through these two stages, the Cloud Firestore page opens in the Firebase Console. What now?
3. Working with a Firestore Database
This section aims to answer the following questions:
- What is a database?
- What does a Firestore database look like?
- How can I initialise a database in the Firestore console?
- How can I access a Firestore database in Javascript?
- How can I get a Svelte
+page.svelte
file to load data from a Firestore database? - How can I get a Svelte
+page.svelte
file to add data to a Firestore database?
3.1 What is a database?
For our immediate purposes, a database is a set of tables containing rows of values for named data "fields". So, for example, a Customer Order" database might contain
- a "Customers" table full of "Customer Id" and "Customer Address Details" field values
- a "Products" table full of "Product Number" and "Product Detail" fields
- a "Customer Orders" table with details of the orders for a "Product Number" placed by a "Customer Id"
The important thing is that such an arrangement is structured with consistent standards for the naming and formatting of data content
3.2 What does a Firestore database look like?
In Firestore, tables are called "collections" and rows within them are called "documents". Documents stored within a collection aren't all required to have the same fields, but field names and content are expected to follow consistent patterns throughout the collection.
An important feature of a Firestore document is that it should have a unique identifier or "key". Sometimes there will be a field such as "Email Address" within each document that you can use to provide a "natural" unique key. If not, Firestore can be asked to create an artificial key automatically.
Database design is probably the most challenging part of system development and, once again, I will duck this because the issues involved will only become clear when you've had some practical experience. However, when you have a moment, you will find it useful to check out the Cloud Firestore Data model page.
3.3 How can I initialise a database in the Firestore console?
In this post, I plan to show you how to create a single products
collection in your default Firestore database. This will contain simple documents containing a product number
field with a key automatically generated by Firestore.
On the Firestore page on the Firebase console, click the "Start collection" button and enter the name "products" in the "Collection ID" field of the popup that now appears.
Now use the data entry page to create a test products document containing a "productNumber" field with a numeric value of "1" and a "productDetails" field with a text value of "Product 1".
- Type "productNumber" in the "Field" entry box to set the field name, set the "Type" box to "number" and enter "1" (without the quote characters) in the "Value" box.
- Click "add field and type "productDetails" in the "Field" entry box to set the field name, leave the "Type" box set to its default "string" setting and enter "Product 1" (without the quote characters) in the "Value" box.
Now sign off the document by first clicking the "Auto Id" button and then "Saving" it Here's what the console should look like now:
If you wanted to add further documents, you would click "add document" at this point, but that's not necessary in this case - you only need a single document to demonstrate your webapp's ability to read it.
You're finished here for now, but note that the console's "panel view" lets you edit or delete the document you've just created. If you've got in a complete mess you can even delete the entire collection and start again.
3.4 How can I access a Firestore database in Javascript?
Here's where things start to get really interesting!
Google provides a library of Javascript functions to let you read and write Firestore documents. Such libraries are referred to as "APIs" (Application Program Interfaces). Have a look at the following code that shows how the firebase/firestore
library would be used to read all the documents in svelte-dev
's products
collection:
import { collection, query, getDocs, orderBy } from "firebase/firestore";
import { initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";
const firebaseConfig = {
apiKey: "AIzaSyCE933 ... klfhFdwQg1IF1pWaR1iE",
authDomain: "svelte-dev-afbaf.firebaseapp.com",
projectId: "svelte-dev-afbaf",
storageBucket: "svelte-devt-afbaf.appspot.com",
messagingSenderId: "1027 ... 85697",
appId: "1:1027546585697:web:27002bf ..... b0f088e820",
};
const firebaseApp = initializeApp(firebaseConfig);
const db = getFirestore(firebaseApp);
const productsCollRef = collection(db, "products");
const productsQuery = query(productsCollRef, orderBy("productNumber", "asc"));
const productsSnapshot = await getDocs(productsQuery);
let currentProducts = [];
productsSnapshot.forEach((product) => {
currentProducts.push({productNumber: product.data().productNumber});
});
return { products: currentProducts } // accessed in +page.svelte as data.products
Concentrate on the section that starts const productsCollRef = collection(db, "products");
. This uses Firestore API calls to load a sorted copy of all documents within the products
collection into the State currentProducts
variable.
First, the collection
and query
functions, drawn from the Firestore Client API library, are used to point Firebase at the products
collection and define a query to run on it. Then the query is launched by a getDocs
API call.
I won't attempt to describe the mechanics of this sequence of Firestore API calls. Treat these as a piece of "boiler-plate code" - code - the sort of thing that you write once and, ever afterwards, simply copy. Since you'll find you need a whole library of templates to cover the full array of Firestore "read", "update" and "delete" operations, you might find it useful to look at "Firestore CRUD command templates" in Post 10.1. If you'd like to check out Google's own description of the API, you'll find links to these at the end of Post 10.1.
"CRUD" here is short for "create", "read", "update" and "delete".
The getDocs
result is returned as an array of documents conventionally called a "snapshot". However, note that the getDocs function call is preceded by an await
keyword.
The await
keyword is needed here because, by default in Javascript, instructions referencing external data sources that may take an unpredictable time to complete are handled asynchronously. The "await" keyword essentially (though this is a gross simplification) enables you to override this arrangement. When you have more time, you might find it useful to have a look at A simple guide to the Javascript fetch() API and the "await" keyword
But right now, returning to our code snippet above, look at the section starting with the const firebaseConfig
statement.
The firebaseConfig
declaration initialises an object containing the configuration details needed to connect your web app to your specific Firebase project. It includes various keys and identifiers that Firebase uses to locate and authenticate your app. You'll find the settings for your particular webapp in "Project Overview/Project settings" on the Firebase console. The firebaseConfig
settings in the code samples below have been "obfuscated" because they are unique to my project and aren't to be passed around lightly (more about this shortly). When trying the sample code below, you'll need to copy in the firebaseConfig
settings from your own project.
With firebaseConfig
initialised, the webapp can initialise the db
variable required by the query's const productsCollRef = collection(db, "products");
statement:
const firebaseApp = initializeApp(firebaseConfig);
const db = getFirestore(firebaseApp);
Finally, you may be wondering where these API functions come from. The answer is that they are imported
from locations in your project by the three statements at the top of the code block:
import { collection, query, getDocs, orderBy } from "firebase/firestore";
import { initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";
Here "modular libraries" are being accessed to supply functions for your code. Named functions such as collection
are extracted from a parent module to fulfil the references required later in the code.
But then this leads to the question "And how do modular libraries get into my project in the first place?" The answer, of course, is that you have to put them there, and the tool you use to do this is faithful old npm
.
Back in a VSCode svelte-test terminal session (terminate the dev server if necessary with a couple of ctrl-C keystrokes) and run the following instruction'
npm install firebase
After a minute or two (the installation involves a sizeable download), you'll be poised to run code that downloads a Firestore database collection. But, you still don't know how to embed this in a Svelte webapp. So, on to the next question...
3.5 How can I get a Svelte +page.svelte
file to load data from a Firestore database?
This has been a long haul but, hang in there, you're nearly finished.
Currently, in the <script>
section of your src/routes/+page.svelte
file, you have the following statement:
let products = [];
As you know, this declares your products
field as a State variable and initialises it as an empty array. What you want to do now is replace "empty" with the content of the products
Firestore collection.
Unfortunately, as you've seen, this involves an asynchronous operation. This complicates things somewhat because Svelte doesn't want anything to slow down the initial load of a page - it's happy to see information added later but, the first user impression should be of an instantaneous response. Svelte has a standard arrangement for loading initial data into a +page.svelte
file. It goes like this:
First, you create a new src/routes/+page.server.js
file that wraps all your asynchronous code inside a load()
function (mandatory name) and returns its results as an object.
Here's the code
// src/routes/+page.server.js
import { collection, query, getDocs, orderBy } from "firebase/firestore";
import { initializeApp, getApps, getApp } from 'firebase/app';
import { getFirestore } from "firebase/firestore";
export async function load() {
const firebaseConfig = { // replace with settings from your own project!!
apiKey: "AIzaSyCE933v....vklfhFdwQg1IF1pWaR1iE", // obfuscated code
authDomain: "svelte-test-afbaf.firebaseapp.com",
projectId: "svelte-test-afbaf",
storageBucket: "svelte-test-afbaf.appspot.com",
messagingSenderId: "1027 ..c585697",
appId: "1:1027546585697:web:27002bf ...ccccb0f088e820", // obfuscated code
};
// see Post 3.3 for an explanation of the "ternary" expression used below
const firebaseApp = !getApps().length ? initializeApp(firebaseConfig) : getApp();
const db = getFirestore(firebaseApp);
const productsCollRef = collection(db, "products");
const productsQuery = query(productsCollRef, orderBy("productNumber", "asc"));
const productsSnapshot = await getDocs(productsQuery);
let currentProducts = [];
productsSnapshot.forEach((product) => {
currentProducts.push({productNumber: product.data().productNumber});
});
return { products: currentProducts }
}
The load
function above returns an object with a single products
property whose value is the currentProducts
array constructed by the Firestore API calls.
This is all very well, but how is this to be conveyed to the products
state variable in +page.svelte
?
The first step is to advertise a new data
(mandatory name) state variable as a prop (short for "property") of +page.svelte
You do this by declaring it with an export
keyword, thus:
export let data;
Props won't be covered in this series until you get to Post 3.1 in this series and learn about "components". For now, think of your +page.svelte
file as a function with data
as its parameter.
When you run your +page.svelte
file now, the SvelteKit framework sees the export let data
declaration with its reserved data
keyword and thinks, "ah, I need to run the load() function associated with this page". The products
data is duly returned into the data
prop of +page.svelte
and now, since this is a reactive variable, the page is refreshed.
All you need to do to make your existing "template" code work with the new arrangement is to replace products
references with data.products
The +page.server.js
file is your first sight of "server-side" code in Svelte - that is, code that runs in the server. All the +page.svelte
code you've seen so far runs "client-side" in the browser. A +page.server.js
file, by contrast, runs either in the local server launched by npm run dev
or, after deployment, in the Node.js environment of an App Engine server. Server-side code runs faster than client-side code and is secure. The only person who can view or change it is you - its owner.
Here's the complete code for a modified version of the +page.svelte
file from Post 2.2:
// src/routes/+page.svelte - remove before running
<script>
let popupVisible = false;
let newProductNumber;
export let data; // An array of data.product objects returned by +page.svelte.js
</script>
<div style="text-align: center">
<h1>Products Maintenance Page</h1>
{#if !popupVisible} <!-- the start position - popup not visible-->
<p>Currently-registered Product Numbers:</p>
{#each data.products as product} <!-- display the current list of products-->
<p style="margin: 0">{product.productNumber}</p>
{/each}
<button
style="margin-top: 1rem"
on:click={() => { // a function to toggle the popup on
popupVisible = true;
}}>Add Another Product
</button>
{:else} <!-- display the product registration form-->
<form
style="border:1px solid black; height: 6rem; width: 30rem; margin: auto;"
>
<p>Product Registration Form</p>
<label>
Product Number
<input bind:value={newProductNumber} type="text" />
</label>
<button
type = "button"
on:click={() => { // a function to add the product number to the array and hide the popup
data.products.push({productNumber: newProductNumber});
popupVisible = false;
newProductNumber = ""; // Reset the input field
}}>Register
</button>
</form>
{/if}
</div>
If you now replace the contents of your current src/routes/+page.svelte
file with the above code, update thefirebaseConfig
settings in +page.server.js
with parameters from your own project), and start your dev server as before, the page should initialise with the single "Product Number 1" record you created in the products
collection earlier. Adding new Products should also work exactly as before.
But there's a snag. When you restart the webapp you'll see that you're back to square one with a single "Product Number 1" record. Can you see the problem? Yes, the webapp is still updating the in-core list of products
(now located in the new data.products
variable). You need to find some way of replacing this with code that updates the products
database. Onward and upward!
3.6 How can I get a Svelte +page.svelte
file to add data to a Firestore database?
What you need is another piece of Firestore API boilerplate code - this time a sequence that creates a new {productNumber: newProductNumber}
document in a Firestore products
collection (to simplify things, I'm ditching the productDetails
property for now). Here it is:
const productsDocData = {productNumber: newProductNumber}
const productsCollRef = collection(db, "products");
const productsDocRef = doc(productsCollRef );
await setDoc(productsDocRef, productsDocData );
Where should this code be located? Currently, the code fired by the "Add Another Product" <form>
's on:click
button lives in your +page.svelte
file. But Svelte advises that, for security and efficiency reasons, the database update should be performed "server-side" in +page.server.js
in an actions()
function paralleling the load()
function you've already created here. The function is triggered by "posting" data from the <form>
.
Here's what the new <form>
arrangement in +page.svelte
should look like:
// src/routes/+page.svelte - fragment
<script>
let popupVisible = false;
export let form;
</script>
<form
method="POST"
style="border:1px solid black; height: 6rem; width: 30rem; margin: auto;">
<p>Products Registration Form</p>
<label>
Product Number
<input name="newProductNumber" />
</label>
<button type="submit">Register</button>
</form>
The most important thing here is the presence of the method=POST
qualifier on the <form>
specification. When the form is submitted, this makes SvelteKit think "ah, I now need to pass the data collected by this form to actions()
in a +page.server.js
file associated with this page. Because no explicit "action" is requested above, SvelteKit goes looking for a "default" actions()
function. Here is is
// src/routes/+page.server.js - fragment
export const actions = {
default: async (event) => {
const formData = await event.request.formData();
// retrieve the newProductNumber string from the form and convert it to a number
const productNumberAsText = formData.get('newProductNumber');
const newProductNumber = parseInt(productNumberAsText, 10)
console.log("newProducNumber : " + newProductNumber)
const productsDocData = { productNumber: newProductNumber }
const productsCollRef = collection(db, "products");
const productsDocRef = doc(productsCollRef);
await setDoc(productsDocRef, productsDocData);
return ({updateSucceeded: true})
}
};
The object returned by the "actions" function becomes available to the +page.svelte
file through the form
prop declared in its <script>
section. Here, an export let form;
statement parallels the earlier use of a data
prop to return the results of a load()
function.
Here are the complete versions of +page.svelte
and +page.server.js
:
// routes/+page.svelte - remove before running
<script>
let popupVisible = false;
export let data;
//The "data" State variable will be initialised by +page.server.js as an array of {productNumber : value}
// objects
export let form;
</script>
<div style="text-align: center">
<h1>Products Maintenance Page</h1>
{#if !popupVisible}
<!-- the start position - popup not visible-->
<p>Currently-registered product numbers:</p>
{#each data.products as product}
<!-- display the current list of products-->
<p style="margin: 0">{product.productNumber}</p>
{/each}
<button
style="display: inline; margin-top: 1rem"
on:click={() => {
popupVisible = true;
}}
>Add Another Product
</button>
{:else}
<!-- display the product registration form-->
<form method="POST"
style="border:1px solid black; height: 6rem; width: 30rem; margin: auto;"
>
<p>Product Registration Form</p>
<label>
Product Number
<input
name="newProductNumber" type="text"/>
</label>
<button type="submit">Register</button>
</form>
{/if}
</div>
// routes/+page.server.js
import { collection, query, getDocs, orderBy, doc, setDoc } from "firebase/firestore";
import { initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";
const firebaseConfig = {
apiKey: "AIzaSyDOVyss .... LsT6n-tWh0GZoPQhM",
authDomain: "svelte-dev-80286.firebaseapp.com",
projectId: "svelte-dev-80286",
storageBucket: "svelte-dev-80286.appspot.com",
messagingSenderId: "5855 ... 06025",
appId: "1:585552006025:web:e41 ..... fcc161e6f58"
};
const firebaseApp = initializeApp(firebaseConfig);
const db = getFirestore(firebaseApp);
export async function load() {
const productsCollRef = collection(db, "products");
const productsQuery = query(productsCollRef, orderBy("productNumber", "asc"));
// Because the productNumber field in the products collection is numeric rather than string, orderBy
// produces numeric sorted order. If the field were a string, the order would be "lexicographic" which
// means "12" sorts before "3". It's a nuisance, however, since later we will be inputting productNumber as
// a string in order to demonstrate validation t in productNumberIsNumeric and also using it
// to demonstrate dynamic routes, which also demand the use of strings. The .toString and parseInt
// javascript functions are used to perform conversions where required.
const productsSnapshot = await getDocs(productsQuery);
let currentProducts = [];
productsSnapshot.forEach((product) => {
currentProducts.push({ productNumber: product.data().productNumber });
});
return { products: currentProducts }
}
export const actions = {
default: async (event) => {
const formData = await event.request.formData();
// retrieve the newProductNumber string from the form and convert it to a number
const productNumberAsText = formData.get('newProductNumber');
const newProductNumber = parseInt(productNumberAsText, 10)
const productsDocData = { productNumber: newProductNumber }
const productsCollRef = collection(db, "products");
const productsDocRef = doc(productsCollRef);
await setDoc(productsDocRef, productsDocData);
return ({updateSucceeded: true})
}
};
If you copy this code, remember to reset the firestoreConfig data again. If you have problems getting it to work, remember also the advice in the previous post about debugging client-side code and look at the "Postscript sections" below for advice about fixing server-side problems. Good luck!
Note that the FireStore API imports and db
configuration statements in +page.server.js
have been given "file scope" by moving them out of the functions they serve and relocating them at the top of the file's <script>
section. In a more complex project, it would be common practice to configure db
in a separate lib
folder to enable it to be shared more widely as an import
.
You'll also note that the new code drops the resetting of the popupVisible
field that previously appeared in the old +page.svelte
file's on:click
function. Svelte's default action on form submission is to reload the page. Consequently, when the form has been processed, popupVisible
is re-initialised as false, the "Currently-registered products:" array is refreshed from the updated Firestore products
collection and the popup disappears. As a bonus, the new Product Number will be inserted in the correct sorted order, courtesy of the orderBy("productNumber", "asc")
qualifier in the products
getDocs
.
Caveat: code like this will only work while your Firestore database is public. When you add rules to restrict a collection's access to authorised users (ie users who have "logged in"), the Firestore templates you've been using here will fail. Posts in the next section of this series will explain why and Post 3.4 in particular will explain how you can fall back to code based purely on client-side +page.svelte
files. However, since this will be at the expense of security and performance, I hope you'll fight your way through these "minor irritations" and continue developing client-server code. For the present, make sure that your database rules look something like:
match /products/{document=**} {
// Allow both read and write access
allow read, write: if true;
}
4. Summary
I imagine that this post will have strained your patience to the limit. If you've reached this point with a working webapp and your sanity still intact, give yourself a gold star - you've covered most of the core Svelte topics and got a handle on the essential skills!
That said, there's still a lot to learn. For example, this post has ducked out of describing basic error handling, and form validation arrangements. Beyond that, I also want to introduce you to Svelte routes(ie pages), layouts (page headers and trailers), components, and the tricky issues surrounding security (login design and database rules). Finally, there's the interesting question of how you deploy your webapp for live operation on the web. I hope you'll read on!
Postcript: When things go wrong
The introduction of "server-side" processing in your webapp has fast-tracked you straight into the senior developer league. This is because the "Chrome Inspector" techniques that I hope you've enjoyed using on your +page.svelte
files don't work on +page.server.js
files. But all is not lost. Here's an introduction to the techniques that a senior developer would use:
Postscript (a): Debugging Server side code in the Terminal Session
Although the VSCode editor will do its best to help you produce sound code, some basic errors will only become apparent when the SvelteKit server tries to run your webapp. At this point, your screen may display a "500 - Internal Error" message (if it displays anything at all!). The browser can't help you much here because a +page.server.js
file is essentially invisible here. While the Source tab's Page/localhost storage hierarchy continues to show your +page.svelte
file, it has nothing to say about +page.server.js
.
But the Inspector knows that an error has occurred and can sometimes give you useful a clue as to its cause. The right-hand end of the menu bar will show a red icon with a cross in the middle. Click this and the Inspector's console will open and display summary error details. But if you need full details, you'll find these in the terminal session where you launched the server with npm run dev
.
The problem here is that you'll likely feel the level of detail is probably rather too full. Every error is reported with a "call stack" that details the full sequence of server function calls that preceded the failure point Here's a short one precipitated by an incorrect field name declaration (I deliberately mistype const db=
as const dba =
ReferenceError: db is not defined
at load (C:\Users\mjoyc\Desktop\GitProjects\svelte-test\src\routes\+page.server.js:18:38)
at Module.load_server_data (C:\Users\mjoyc\Desktop\GitProjects\svelte-test\node_modules\@sveltejs\kit\src\runtime\server\page\load_data.js:61:41)
at C:\Users\mjoyc\Desktop\GitProjects\svelte-test\node_modules\@sveltejs\kit\src\runtime\server\page\index.js:140:19
A terminal window isn't a good place to view information like this. Sometimes you have to scroll up or down to find what you want, and information may be additionally obscured by output from other activities. Nevertheless, it's all you've got so you need to make the best of it.
But note that, in the example shown above, the cause of the error is signalled very clearly - the server has encountered a reference to a db
variable in +page.server.js
at line 18 column 38 and db
hasn't been declared. That gives you everything you need, I think.
The terminal window can help you fix problems with your logic too. In the past, "debugging" was usually carried out by placing console.log
messages messages at strategic points in the code and it's easy enough to use this approach here. A typical log message would be:
console.log("Arrived at X with a= " + a);
A log message like this placed in a +page.server.js
file will be displayed in the VSCode terminal window (by contrast, log statements in a client-side +page.svelte
code will continue to be displayed in the browser's console window).
Postscript (b): Debugging Server side code in the VSCode debugger
Console.log messages are fine for fixing minor issues, but by now you expect to be able to use something with the sophistication of the browser "breakpoint" debugging tool. Fear not. You can now find this in the VSCode. editor. Here's what you do:
- Open the
+page.server.js
file you want to debug and set a breakpoint at a point you'd like to inspect. You do this by mousing over the source line you want to use as a breakpoint and clicking on the pale red dot that will appear at its left-hand side. The pale red dot now changes to bright red. - In the VSCode command palette (shortcut "ctrl shift p") choose
Debug: Attach to node process
and get a list of projects in your workspace. Select the one you want to debug. - Note that the terminal session that you've just launched is labelled as "Javascript Debug Terminal". Type 'npm run dev` into this terminal and note how the usual Vite output that appears includes an additional "Debugger attached" line. Note also how VSCode's activity bar has turned orange
- Now mouse over the
http://localhost
address displayed by Vite and launch the webapp with a "ctrl-click". Your webapp now opens in a popup window. - You'll now find that the editor page for your
+page.server.js
file has become an active debugging session with execution halted on the first breakpoint. A panel at the top of the editor window displays the familiar "advance to next breakpoint" etc buttons, and mousing over field names will reveal tooltips that display their values. As before, if a debug session is halted on a variable assignment statement, you'll only see the result of this when you advance to the next statement. - Terminate the debug session by mousing over the icon at the far end of the debug controls panel. This toggles between "disconnect" and "stop" actions when you press the "alt" key. Click this when it displays "disconnect" and the orange VSCode activity bar will turn blue again. If you want to resume debugging, "ctrl click" on the webapp URL in the debugging terminal window again (which will stay active until you explicitly "bin" it)
The screenshot below shows the +page.server.js
file for this post halted on the return
from the load()
function. A "mouseover" on the products
property for the return at this point displays the result of reading the Firestore products
collection.
For full details of the facilities available in the VSCode debugger, see the documentation at VSCode debugging
Posted on November 26, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 26, 2024
November 26, 2024