SvelteKit 1 with SQLite: Displaying, Changing and Searching Data
A perfect match#
SvelteKit 1.0 released in December 2022. As it now has a stable API I took a peek at how data-driven applications can be built with it. I am impressed by its simplicity and how fast you can achieve great results.
Together, SvelteKit and SQLite make a great combo because they both have a small footprint and are easy to set up and use. SQLite can be easily integrated into a SvelteKit application, allowing developers to easily store and retrieve data in a structured way, making it easy to build CRUD web applications.
SQLite is serverless (not the new cloud buzzword) meaning that it runs within the same process from the consuming application. Traditional relational client/server databases like MySQL or Postgres run in their own process and lead to the burden of having to maintain them. This approach can have big advantages if you have a high amount clients directly connecting to the database. In our SvelteKit app approach, this is not the case, as only the SvelteKit backend is directly accessing the database.
I use SvelteKit in combination with TypeScript and highly recommend you do this as well. We can type our data shapes in the backend and later reuse them in the frontend to safely access the data in the UI and make our lives easier in case of type changes.
I will use better-sqlite3 as the Node.js SQLite driver as I like its synchronous API.
Instead of setting up a schema with data myself, I used the chinook sample database that includes music data with tables like artists, albums, and tracks.
Display all tracks#
The example database has a table called
tracks. We will query this table to display the first 50 tracks on our home page.
Setting up the database#
As the database file only sits on the server, we can create the main DB file in
lib is the place to put your code that does not strictly belong to any route and can be imported with the
Any code in the
server subfolder cannot be imported in the frontend. So any code in there is hidden from the eyes of the users. You can read more about the project structure in the docs.
I created another subfolder
db with an
index.ts. We can import
better-sqlite3 and use its constructor to start our database. Afterward, we can outsource the path to my database file in a
.env file with the content
We start with the function
getInitialTracks that just returns the first 50 tracks from the corresponding table. I store the query in a variable and later call
db.prepare with it. The prepared statement can then be executed with the
.all function as we want to get multiple rows. This function also receives an object of the parameters we want to bind to the query. In our case, this is the limit amount we can now provide as a function parameter. We don’t want to manually concatenate that input into the SQL-Query as this can lead to nasty SQL-Injection vulnerabilities. The whole file now looks like this:
As I used TypeScript we also need to add a definition for our
Track type. I created a
types.ts next to the database
index.ts with the following content:
Enriching the page with the data#
To show the data on our home page we need to create a
src/routes. In there we can add a load function that simply returns the data of our function:
These few lines are enough for us to consume the data in our frontend svelte file. In the neighboring
+page.svelte we can consume and display the data:
As I included bulma to get some modern initial styles the result looks the following:
A neat addition to the app is an album details page that shows some basic information about any album with a tracklist.
Creating the route#
As SvelteKit uses a filesystem-based router we can create new folders to allow for an album detail route. Inside
src/routes we can create
album/[albumId]. We put the
albumId in squared brackets so that SvelteKit treats this as a parameter so that different content is loaded for different IDs.
In the newly created folders, we can add empty
Back in our database
index.ts we can add following two functions for the new page:
types.ts file, we then add the newly used types:
Page load function#
In the new
+page.server.ts file, we can then define our load function. We can receive the
albumId of the URL from the
params object. The value is named the same as we defined in the folder name (
It is now trivial to display the loaded data on the page:
We call our album route with any non-existing album ID we get a default unstyled error page that says “500 Internal Error”.
The 500 error code is not great as 404 “not found” would be better as the error was caused by an unknown album ID. We can manually fix this by adding error handling mechanisms in our
+page.server.ts. We can check after parsing the parameter as an integer if we have any value left as parsing only characters would result in no value. If this is the case we can throw the error function provided by SvelteKit. We can then define our wanted HTTP code and the message. We do the same when our database function still returns without a value:
We may want this error page to match our page style or add more information. We can achieve this by adding a
+error.svelte file next to the page file. Note that we could also create a global error file as SvelteKit will use the closest file while traversing up the file tree.
For a really simple page, we can just display the HTTP status code and a message by importing both from the
The result looks the following:
Update an album name#
To cover sending data to the backend to write data back to the database I decided to allow updating the album name.
We have to extend our database functions again with a new one that runs the update statement. We only need our primary key column, the
AlbumId, and the new value for the
Title column. We can add a prepared statement as before and pass the values as an object:
The easiest way to send data to the backend and start an operation is by utilizing forms. We can add one to the album page and set the HTTP method to
POST instead of
GET as this makes sense for writing operations.
We add a hidden input field for the album ID that is prefilled by our load data. Additionally, we create a text input field for the new album title. Both items need the name property so we can access the values later on.
We can then add a button that submits the form. We can add the
formaction property and set a function name that is then triggered in the
+page.server.ts file. This way we can do different operations based on a single form (e. g. Create, Update, Delete). Note that we prefix the function name with a question mark and slash to make it a relative path.
Handle form submission#
We can now handle the form submission in our
+page.server.ts file. For that, we create a const called
actions. In there we define a function called the same as our
formaction attribute on our button.
We can access the form data by calling
request.formData() and then grab the values by their name. Now we call our database function and pass the values:
Make tracks searchable#
Back on our home page, we want to make the tracks searchable. I think the most user-convenient way is to implement a search while you type. So this time we can not use any load function as we have to send a request to our backend while the user types and not on the initial page load.
We have to create a new DB function that takes a search term. We can then use the
like operator to search for the term in the track name. By using the
lower function to make the search case insensitive:
So instead of calling the new function in our load, we can make use of API routes. These endpoints don’t support any page and thus need to be called
+server.ts (instead of
+page.server.ts). I think it makes sense to create an
api folder so my API route sits in
We can create a function
GET as we want it to be triggered by the HTTP GET method. The search term should be passed as a query parameter. We can access it by calling
url.searchParams.get('searchTerm')?.toString();. If there is no search term we can return the initial tracks. Otherwise, we can pass our array to the
json function and return it.
We add an input of type
search to our home page. We can then add an
on:search event listener. The
on:keyup event is triggered on every key press. The
on:search event is triggered when the user presses the enter key or clicks the ‘x’ to reset the input:
handleSearch function receives the event and we can access the input value by calling
e.target.value. I recommend to debounce the search to throttle the number of requests send to the server. So we don’t actually filter while the user types, only after he has not typed in 300 ms. We can do this by using timeouts.
The real request is done in the
fetchTracks function. It uses the
fetch API to make a GET request to our API route. We pass the search term as a query parameter. After the request is done we can parse the response as JSON and set the result to the new
We still need to change the table loop to accessing the
tracks variable instead of
data.tracks so that it updates after a search:
Now we are done and can search for tracks. You can find the final code on GitHub. Let me know in the comments if you build something with SvelteKit and SQLite and if you like the combination,