SvelteKit Turso Fly.io App Guide
I’ve been doing more stuff with Fly.io and Turso with SvelteKit. If you’re following my posts you’ll know I’ve got a real nerd boner for using Turso in anything I can cram it into at the moment!
Why Fly.io though? Just get a $5 VPS and use that! Skill issue! 😂 Well, sure, skill issue and time issue, insert “ain’t nobody got time for that” gif here! (I managed several company Linux boxes in the past). With Fly, I’m essentially shipping a Node container on their platform. This opens up the possibility of using Turso embedded replicas and multi-tenant apps! I’m not doing that now though!
What I want to go through here is the basics of what you’ll need to get a project scaffolded out to connect to a serverless database, display the data and deploy it to Fly.
The inspiration for doing this was a video from Philipp Hartenfeller that I caught on YouTube. You don’t want to use Turso? Check out his video!
This isn’t a CRUD app it’s an R app! 😂 This guide uses the chinook SQLite sample database. So there’s a load of tables and data to query. No need for user generated content and authentication, although this can be added in, it’s out of the scope of this and the time I can spend writing it.
Although I have used and like Drizzle ORM on other projects I really do prefer to just use SQL to query data.
So, no auth, no ORM, just raw dog SQL for getting data…
Still here? Good! So, this guide will go over the following:
- Actually getting a database you can use without having to make all the tables and add data!
- Use some Svelte 5 features, runes, yes
- Adding a local SQLite database to Turso
- Setting up queries to use in the project, including a full text search query
- Push the project to Fly.io
Setting up the project
So, usual SvelteKit setup from the terminal. But, as I’ll be using Fly to deploy the finished project I’ll be using Bun as the runtime and package manager. Why? Well, deploying a SvelteKit Node project to Fly can sometimes cause issues with CJS/ESM compatibility not being handled and I found using Bun sidestepped this whole issue.
I’ll spin up a new app using the create svelte command for Bun:
bun create svelte sveltekit-turso-flyio-app
I’ll pick the following options:
- Skeleton project
- using TypeScript syntax
- All the additional options
- Add ESLint for code linting
- Add Prettier for code formatting
- Add Playwright for browser testing
- Add Vitest for unit testing
- Try the Svelte 5 preview (unstable!)
For connecting to the Turso database I’ll need to install the Turso client:
bun i -D @libsql/client
Then because I’m using Bun I’ll want to uninstall the SvelteKit auto adapter and add in the Bun adapter:
bun uninstall @sveltejs/adapter-auto
bun i -D svelte-adapter-bun
Then configure the adapter in the svelte.config.js
file:
- import adapter from '@sveltejs/adapter-auto';
+ import adapter from 'svelte-adapter-bun';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = {
I’ll also need to adjust the scripts in the package.json
file to use
Bun:
"scripts": {
+ "start": "bun ./build/index.js",
- "dev": "vite dev",
+ "dev": "bun vite",
- "build": "vite build",
+ "build": "bunx --bun vite build",
- "preview": "vite preview",
+ "preview": "bun vite preview",
"test": "bun run test:integration && bun run test:unit",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
If you’re following along and you haven’t used Turso before, you’ll need to install the Turso CLI, it’s a one liner from the Turso quickstart page.
I’m using WSL so I’ll use the Linux install command:
curl -sSfL https://get.tur.so/install.sh | bash
I’ll be going over all the Turso CLI commands in a later section.
Aight! Time to scaffold out the files for the server stuff, from the terminal I’ll add in the folder/directory and files with the following commands:
mkdir src/lib/server
touch src/lib/server/{client,index,queries}.ts
I’ll get that set up in another section, what I will need though is a .env
file with the secrets for the Turso client to connect to the
database, a one liner to create the file with:
touch .env && echo -e "TURSO_DB_URL=\nTURSO_DB_AUTH_TOKEN=" >> .env
That’ll create a .env
file in the root of the project with the TURSO_DB_URL
and TURSO_DB_AUTH_TOKEN
secrets ready for populating
when I generate them.
Sweet! Now I’ll get the database set up on Turso!
Adding the database to Turso
So, the Turso CLI allows you to add a database via a file, so, I can download the chinook database zip from the SQLite tutorial site I linked earlier.
Then with the CLI using the --from-file
flag and pointing to the
extracted chinook.db
file I can create a new database on Turso:
turso db create sveltekit-turso-flyio-app --from-file /mnt/c/Users/scott/Downloads/chinook.db
I’m going to need the database URL to add to my project secrets
(.env
file), I can use the show
command to get this:
turso db show sveltekit-turso-flyio-app
Also, I’ll need to generate an auth token which I can do via the Turso CLI too:
turso db tokens create sveltekit-turso-flyio-app
Add them to the .env
file:
TURSO_DB_URL=libsql://sveltekit-turso-flyio-app.turso.io
TURSO_DB_AUTH_TOKEN=the-generated-auth-token
Right! I’m now ready to set up the Turso client so I can query data from the database!
Setting up the Turso client
In the src/lib/server/client.ts
file I created, I’ll export a client
function, essentially I could do this:
import { env } from '$env/dynamic/private'
import { createClient, type Client } from '@libsql/client'
const { TURSO_DB_URL, TURSO_DB_AUTH_TOKEN } = env
export const turso_client = (): Client => {
return createClient({
url: TURSO_DB_URL as string,
authToken: TURSO_DB_AUTH_TOKEN as string,
})
}
But, what I should do is the responsible thing and add in some error handling in there:
import { env } from '$env/dynamic/private'
import { createClient, type Client } from '@libsql/client'
const { TURSO_DB_URL, TURSO_DB_AUTH_TOKEN } = env
export const turso_client = (): Client => {
const url = TURSO_DB_URL?.trim()
if (url === undefined) {
throw new Error('TURSO_DB_URL is not defined')
}
const auth_token = TURSO_DB_AUTH_TOKEN?.trim()
if (auth_token === undefined) {
if (!url.includes('file:')) {
throw new Error('TURSO_DB_AUTH_TOKEN is not defined')
}
}
return createClient({
url: TURSO_DB_URL as string,
authToken: TURSO_DB_AUTH_TOKEN as string,
})
}
Ok, now, I’ll export this function from the src/lib/server/index.ts
file, I’ll also export the queries from here too, more on them in the
next section!
export * from './client'
export * from './queries'
Sweet! Now I can use the client to query some data! Now to get the data!
Setting up queries
I want to do a full text search query on the tracks table but also get information on the album, artist, genre and track.
First up though, I want to have some initial data to show on the index
page, so I’ll set up an initial query in the src/lib/server/queries.ts
file:
SELECT t.TrackId AS TrackId,
t.Name AS Name,
a.AlbumId AS AlbumId,
a.Title AS Title,
at.ArtistId AS ArtistId,
at.Name AS ArtistName,
g.Name AS Genre,
g.GenreId AS GenreId
FROM tracks t
JOIN albums a ON t.AlbumId = a.AlbumId
JOIN artists at ON a.ArtistId = at.ArtistId
JOIN genres g ON t.GenreId = g.GenreId
LIMIT 50;
So, loads of SQL joins and shiz! Right? I’m not going to go into a
relational database fundamentals talk here, so, if you’re not sure
what’s happening here essentially getting the names off of the related
tables that includes the names for the album, artist and genre, so,
let’s take a look at the data I retrieve if I did a straight up SELECT * FROM tracks LIMIT 3;
. It looks like this:
TRACKID NAME ALBUMID MEDIATYPEID GENREID COMPOSER MILLISECONDS BYTES UNITPRICE
1 For Those About To Rock (We Salute You) 1 1 1 Angus Young, Malcolm Young, Brian Johnson 343719 11170334 0.99
2 Balls to the Wall 2 2 1 NULL 342562 5510424 0.99
3 Fast As a Shark 3 2 1 F. Baltes, S. Kaufman, U. Dirkscneider & W. Hoffman 230619 3990994 0.99
Whereas I want something a bit more descriptive, so, running the big ass query with all the joins I get something like this:
TRACKID NAME ALBUMID TITLE ARTISTID ARTISTNAME GENRE GENREID
1 For Those About To Rock (We Salute You) 1 For Those About To Rock We Salute You 1 AC/DC Rock 1
6 Put The Finger On You 1 For Those About To Rock We Salute You 1 AC/DC Rock 1
7 Let's Get It Up 1 For Those About To Rock We Salute You 1 AC/DC Rock 1
I’m keeping the IDs as well for cross linking to other pages. More on that later!
Aight, so this is an SQL query, I’m not doing jack with this in a TypeScript file!
In the src/lib/server/queries.ts
file I’ll import the Turso client
and .execute
that query.
import { turso_client } from '.'
const client = turso_client()
export const get_initial_tracks = async (limit = 50) => {
const tracks = await client.execute({
sql: `SELECT t.TrackId AS TrackId,
t.Name AS Name,
a.AlbumId AS AlbumId,
a.Title AS Title,
at.ArtistId AS ArtistId,
at.Name AS ArtistName,
g.Name AS Genre,
g.GenreId AS GenreId
FROM tracks t
JOIN albums a ON t.AlbumId = a.AlbumId
JOIN artists at ON a.ArtistId = at.ArtistId
JOIN genres g ON t.GenreId = g.GenreId
LIMIT ?;`,
args: [limit],
})
return tracks.rows
}
Essentially the SQL is in backticks and given to the client as the SQL
to run, the args
are the arguments to pass to the SQL query, you may
have noticed the LIMIT ?;
at the end of the query, that ?
will get
substituted with the limit
argument.
Ok, so, this is going on a bit now, but, what I want to do here is have a way for the user to be able to enter some text into an input box and do a search against all the data in the database.
With that big boi query, I only get the first 50 rows, so, I’ll need to set up a virtual table that I can search against for anything that’s in the database.
So, now, time for some more SQL’ing! I’m going to shell into the Turso database for this:
turso db shell sveltekit-turso-flyio-app
Then from the shell I’ll create a virtual table that I can search against, I’ll do this in three steps.
- Create the virtual (FTS5) table
- Insert Data into the (FTS5) table (via big ass query)
- Fuzzy query the (FTS5) table
First up, create the virtual table:
CREATE VIRTUAL TABLE tracks_fts USING fts5 (
TrackId,
Name,
AlbumId,
Title,
ArtistId,
ArtistName,
Genre,
GenreId,
prefix = '2 3 4'
);
The prefix ‘2 3 4’ is so that it’s optimised for searches that are
that length, so searching for Jamiroquai
if I enter jam
I should
get a result matching that.
Then insert the data into the virtual table:
INSERT INTO
tracks_fts (
TrackId,
Name,
AlbumId,
Title,
ArtistId,
ArtistName,
Genre,
GenreId
)
SELECT
t.TrackId,
t.Name,
a.AlbumId,
a.Title,
at.ArtistId,
at.Name,
g.Name,
g.GenreId
FROM
tracks t
JOIN albums a ON t.AlbumId = a.AlbumId
JOIN artists at ON a.ArtistId = at.ArtistId
JOIN genres g ON t.GenreId = g.GenreId;
Then query the virtual table, so for Jamiroquai
try:
SELECT * FROM tracks_fts WHERE tracks_fts MATCH 'jam';
Success, ok now jag
for Jagged Little Pill
?? Nothing? Ok, so, I
need to add in a *
to the end of the search term to do a fuzzy
search:
SELECT * FROM tracks_fts WHERE tracks_fts MATCH 'jag*';
Then I get the result I’m looking for!
Cool! So, I just want to highlight that for now, as I’ll be coming back to that later!
For now I’ll concentrate on getting the data from Turso into the project!
Get the data from Turso
Cool! I’ll test out I’m getting data from the Turso database client
now. So, because this is a SvelteKit project I can use the load
function to go off and get data for the page on initial load.
Because the Turso client is server side I’ll need to create a +page.server.ts
file at the root of the routes directory:
touch src/routes/+page.server.ts
Then add in a load
function to get the initial tracks and return
them for use on the index page.
import { get_initial_tracks } from '$lib/server'
export const load = async () => {
const tracks = await get_initial_tracks()
return {
tracks,
}
}
In the src/routes/+page.svelte
file I can then get the props from
the load
function and display the data on the page.
As I’m just validating that I’m getting data through to the page I’ll
use my trusty debug tool, the <pre>{JSON.stringify(data, null, 2)}</pre>
of the data!
<script lang="ts">
let { data } = $props()
</script>
<pre>{JSON.stringify(data, null, 2)}</pre>
<h1>Welcome to SvelteKit</h1>
<p>
Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the
documentation
</p>
That gives me some output that looks like this:
{
"tracks": [
{
"TrackId": 1,
"Name": "For Those About To Rock (We Salute You)",
"AlbumId": 1,
"Title": "For Those About To Rock We Salute You",
"ArtistId": 1,
"ArtistName": "AC/DC",
"Genre": "Rock",
"GenreId": 1
},
{
"TrackId": 6,
"Name": "Put The Finger On You",
"AlbumId": 1,
"Title": "For Those About To Rock We Salute You",
"ArtistId": 1,
"ArtistName": "AC/DC",
"Genre": "Rock",
"GenreId": 1
},
Cool! So, when the page loads I’m getting the data from the database, so, what about searching for stuff? I’ll come onto that, soon, first better get this index page cleaned up a bit!
So, styling so far, I am a massive advocate for daisyUI however, so I’ll add that in:
npx svelte-add@latest tailwindcss --tailwindcss-typography --tailwindcss-daisyui
bun i
The bunx
command currently doesn’t work with the svelte-add
, so,
I’m using the npx
script then installing with bun.
That will configure Tailwind and the daisyUI plugin and add in the files needed for the project.
├── src
│ ├── routes
│ │ └── +layout.svelte
│ └── app.pcss
├── .prettierrc
├── package.json
├── postcss.config.cjs
├── svelte.config.js
└── tailwind.config.cjs
Added files, +layout.svelte
, app.pcss
, postcss.config.cjs
and tailwind.config.cjs
are added to the project, with the other files
configured for Tailwind.
Serious, if you’re following along and you have a stick up your butt about Tailwind, that’s cool! You can spend all the extra time you must have on your hands hand writing the CSS, I’m not about that life for an example app! 😂
Aight, I’ll get the initial page layout going on using a table and the handy styling utils from daisyUI:
<script lang="ts">
let { data } = $props()
</script>
<svelte:head>
<title>Music Search - Chinook SvelteKit</title>
</svelte:head>
<p class="mb-2 text-xl font-light">
This is the initial 50 tracks from the Chinook database
</p>
<div class="overflow-x-auto">
<table
class="table table-pin-rows table-pin-cols table-xs md:table-lg"
>
<thead>
<tr class="text-xl">
<th>Track</th>
<th>Artist</th>
<th>Album</th>
<th>Genre</th>
</tr>
</thead>
<tbody>
{#each data.tracks as track (track.TrackId)}
<tr class:hover={'bg-base-200'}>
<td>{track.Name}</td>
<td>{track.ArtistName}</td>
<td>{track.Title}</td>
<td>{track.Genre}</td>
</tr>
{/each}
</tbody>
</table>
</div>
The data from the load
function in the +page.server.ts
file is
received into the +page.svelte
file via the Svelte 5 props
rune
and then I’m looping through that with an each block to render out the
table.
Actually whilst I’m on the subject of runes, I’ll swap out the slot in
the +layout.svelte
file for the props
rune, so, from this:
<script>
import '../app.pcss'
</script>
<slot />
To this:
<script lang="ts">
let { children } = $props()
import '../app.pcss'
</script>
<main class="container mx-auto max-w-6xl flex-grow px-4">
<h1 class="mb-2 mt-4 text-5xl font-bold text-primary">
<a href="/">Chinook SQLite database</a>
</h1>
<ul class="mb-10 flex space-x-4 text-xl font-bold">
<li><a href="/genre" class="link link-primary">Genres</a></li>
<li><a href="/" class="link link-primary">Home</a></li>
</ul>
{@render children()}
</main>
Adding in some nav links and a title to the layout file along with
some tailwind container classes. The /genre
link is a placeholder
for a genre page I’ll add in later.
What I’ve done here is, instead of the slot
I’m using the children
prop to render out the children of the layout file like a snippet.
Cool! A page with 50 tracks on it from the database! Bit pants right! 😂
Ok, I’ll add in a search input now to use the full text search query.
Adding a search input
Now I want to be able to utilise that full text search table I created, so, I’m going to need to first make that query to the database form the Turso client.
Essentially that select query I made earlier validating the search on
the search_track table, I’m going to group all the queries being used
in the project in the src/lib/server/queries.ts
file.
This is going to be the same setup, passing the SQL to the client with the argument for what is being searched.
Remember the *
at the end of the search earlier, I’ll add that in
now with some regex to escape any double quotes in the search term:
export const search_tracks = async (search_term: string) => {
const escaped_search_term = `"${String(search_term).replace(/"/g, '""')}"*`
const tracks = await client.execute({
sql: `SELECT * FROM tracks_fts WHERE tracks_fts MATCH ?;`,
args: [escaped_search_term],
})
return tracks.rows
}
So, now notice that the get_initial_tracks
and the search_tracks
return the same variable name?
This is so that they can be swapped interchangeably.
More on that in a bit, for now though I want to way to get that data from the server, so, I’ll add in a new route for the search query.
The convention is to stick API call in a src/routes/api
directory,
so I’ll add a new search folder with a +server.ts
file in there:
mkdir -p src/routes/api/search
touch src/routes/api/search/+server.ts
Then bang out a get to run the search_tracks
query:
import { get_initial_tracks, search_tracks } from '$lib/server'
import type { Row } from '@libsql/client'
import { json } from '@sveltejs/kit'
export const GET = async ({ url }) => {
const search_term = url.searchParams.get('search_term')?.toString()
let tracks: Row[] = []
if (!search_term) {
tracks = await get_initial_tracks()
} else {
tracks = (await search_tracks(search_term)) ?? []
}
return json(tracks)
}
Now I’m going to need to call this from the index page via a client side fetch, I’m going to need to set up some state for the search term and the results.
So, I’ll add the data.tracks
that comes in from the load
function
as props then add that to state along with the search_term
:
let tracks = $state(data.tracks)
let search_term = $state('')
Then to fetch the data from the server I’ll add in a function to do that:
const fetch_tracks = async () => {
const res = await fetch(`/api/search?search_term=${search_term}`)
const data = await res.json()
tracks = data
}
Because I’ve added the tracks to state I’ll also need to swap out the data.tracks
from the each
loop in the table:
- {#each data.tracks as track (track.TrackId)}
+ {#each tracks as track (track.TrackId)}
Now whatever is is state is what will be rendered on the table.
Back to fetching the data now, so I’m going to be hitting that fetch_tracks
function to get the data from the server, so I’ll want
to limit the amount of calls to the endpoint, so, I’ll add in a
debounce function with a 300 millisecond delay to limit the amount of
calls to the server.
I’ll need to add a timer
variable to state as well, so I’ll update
my state to accommodate that as well:
let tracks = $state(data.tracks)
let search_term = $state('')
let timer: string | number | NodeJS.Timeout | undefined = $state(300)
Then I’ll add in the debounce function:
const handle_search = (e: Event) => {
clearTimeout(timer)
timer = setTimeout(() => {
const target = e.target as HTMLInputElement
search_term = target.value
fetch_tracks()
}, 300)
}
Ok, last up for the script stuff I’ll want to add something in to handle the input from the input box (which doesn’t exist yet! 😅):
const handle_input = (e: Event) => {
const target = e.target as HTMLInputElement
if (target.value === '') {
search_term = ''
tracks = data.tracks
}
}
Now for the markup, for the value of the input I’ll not bind that to
state as it need to go through the handle_search
debounce function
first, so I’ll add in the on:input
to update the state and on:keyup
to run the search:
<input
type="search"
placeholder="Search tracks, titles, albums, artists, genres..."
class="input input-bordered input-primary mb-10 w-full"
value={search_term}
on:keyup={handle_search}
on:input={handle_input}
/>
Code wall incoming!
Here’s the full file:
<script lang="ts">
let { data } = $props()
let tracks = $state(data.tracks)
let search_term = $state('')
let timer: string | number | NodeJS.Timeout | undefined =
$state(300)
const fetch_tracks = async () => {
const res = await fetch(`/api/search?search_term=${search_term}`)
const data = await res.json()
tracks = data
}
const handle_search = (e: Event) => {
clearTimeout(timer)
timer = setTimeout(() => {
const target = e.target as HTMLInputElement
search_term = target.value
fetch_tracks()
}, 300)
}
const handle_input = (e: Event) => {
const target = e.target as HTMLInputElement
if (target.value === '') {
search_term = ''
tracks = data.tracks
}
}
</script>
<svelte:head>
<title>Music Search - Chinook SvelteKit</title>
</svelte:head>
<input
type="search"
placeholder="Search tracks, titles, albums, artists, genres..."
class="input input-bordered input-primary mb-10 w-full"
value={search_term}
on:keyup={handle_search}
on:input={handle_input}
/>
<p class="mb-2 text-xl font-light">
This is the initial 50 tracks from the Chinook database
</p>
<div class="overflow-x-auto">
<table
class="table table-pin-rows table-pin-cols table-xs md:table-lg"
>
<thead>
<tr class="text-xl">
<th>Track</th>
<th>Artist</th>
<th>Album</th>
<th>Genre</th>
</tr>
</thead>
<tbody>
{#each data.tracks as track (track.TrackId)}
<tr class:hover={'bg-base-200'}>
<td>{track.Name}</td>
<td>{track.ArtistName}</td>
<td>{track.Title}</td>
<td>{track.Genre}</td>
</tr>
{/each}
</tbody>
</table>
</div>
Cool! Now I have a nice little search bar from the index page to find, title, tracks, artist, genres and albums!
Wiring up the rest of the project
I’ve gone over the basic pattern I’ll be using for the rest of the routes now. The pattern is, make a query to get the data, add the data to state, then render the data on the page via a page load.
This is quite a meaty section with a lot of code, so, only interested in deploying to Fly.io? Skip to the next section!
So, remember the big ass query and all the fields it returned? Currently I’m only using the names but I have the IDs for Track, Artist, Album and Genre too. So, this means that I can start listing more related stuff out from the initial search.
So, that’s going to be a route with a parameter passed to it so that can be used as an argument for a query, I’ll get the routes and files for that scaffolded out now with a load of terminal commands:
mkdir -p "src/routes/album/[album_id]" "src/routes/artist/[artist_id]" "src/routes/genre/[genre_id]" "src/routes/track/[track_id]"
touch src/routes/album/'[album_id]'/+page.{server.ts,svelte}
touch src/routes/artist/'[artist_id]'/+page.{server.ts,svelte}
touch src/routes/genre/'[genre_id]'/+page.{server.ts,svelte}
touch src/routes/genre/+page.{server.ts,svelte}
touch src/routes/track/'[track_id]'/+page.{server.ts,svelte}
I’ll also want to link to each of these routes, so, in the each
block on the src/routes/+page.svelte
page I’ll add in links for each
one:
{#each tracks as track (track.TrackId)}
<tr class:hover={'bg-base-200'}>
<td>
<a href={`/track/${track.TrackId}`} class="link link-primary">
{track.Name}
</a>
</td>
<td>
<a href={`/artist/${track.ArtistId}`} class="link link-primary">
{track.ArtistName}
</a>
</td>
<td>
<a href={`/album/${track.AlbumId}`} class="link link-primary">
{track.Title}
</a>
</td>
<td>
<a href={`/genre/${track.GenreId}`} class="link link-primary">
{track.Genre}
</a>
</td>
</tr>
{/each}
At the moment clicking on these isn’t going to go anywhere so I’ll start adding in the content, as stated, these patterns have already been detailed, now it’s a case of repeating them for each route.
In the src/lib/server/queries.ts
file I’ll add in the query that’s
going to give me some detail on the album with some transformation on
the milliseconds field to express the time in minutes and seconds:
export const get_album_by_id = async (album_id: number) => {
const album = await client.execute({
sql: `SELECT
a.Title AS AlbumTitle,
t.TrackId,
t.Name AS TrackName,
at.Name AS ArtistName,
(t.Milliseconds / 60000) || 'm ' || ((t.Milliseconds % 60000) / 1000) || 's' AS Duration
FROM
albums a
JOIN
tracks t ON a.AlbumId = t.AlbumId
JOIN
artists at ON a.ArtistId = at.ArtistId
WHERE
a.AlbumId = ?;`,
args: [album_id],
})
return {
artist: album.rows[0].ArtistName,
album: album.rows[0].AlbumTitle,
tracks: album.rows,
}
}
I’m also going to return the artist name and album title as well as
the tracks for the album, so in the src/routes/album/[album_id]/+page.server.ts
file:
import { get_album_by_id } from '$lib/server/queries'
export const load = async ({ params }) => {
const album_id = parseInt(params.album_id)
const { artist, album, tracks } = await get_album_by_id(album_id)
return {
artist,
album,
tracks,
}
}
Then for the src/routes/album/[album_id]/+page.svelte
file I’ll add
in the props and render out the data, much like the src/routes/+page.svelte
pretty much copy paste changing some of the
details:
<script lang="ts">
let { data } = $props()
const { artist, album, tracks } = data
</script>
<svelte:head>
<title>{album} - Chinook SvelteKit</title>
</svelte:head>
<h1 class="mb-5 text-4xl font-bold text-primary">{album}</h1>
<p class="mb-10 text-3xl font-bold tracking-widest text-secondary">
By {artist}
</p>
<div class="overflow-x-auto">
<table
class="table table-pin-rows table-pin-cols table-xs md:table-lg"
>
<thead>
<tr class="text-xl">
<th>#</th>
<th>Track</th>
<th>Duration</th>
</tr>
</thead>
<tbody>
{#each tracks as track, i}
<tr class:hover={'bg-base-200'}>
<td>{i + 1}</td>
<td>
<a
href={`/track/${track.TrackId}`}
class="link link-primary"
>
{track.TrackName}
</a>
</td>
<td>{track.Duration}</td>
</tr>
{/each}
</tbody>
</table>
</div>
Notice that I’ve liked the tracks as well, once that route is set up I’ll be able to click on the track and get more information on that…
Next up though, I’ll do the artist by artist ID, so, in the src/lib/server/queries.ts
file I’ll add in the query to get the
artist by ID, also add in a subquery to get the number of tracks on
the album:
export const get_albums_by_artist_id = async (artist_id: number) => {
const albums = await client.execute({
sql: `SELECT
a.AlbumId,
a.Title AS AlbumTitle,
at.Name AS ArtistName,
(SELECT COUNT(*) FROM tracks t WHERE t.AlbumId = a.AlbumId) AS TrackCount
FROM
albums a
JOIN
artists at ON a.ArtistId = at.ArtistId
WHERE
a.ArtistId = ?;`,
args: [artist_id],
})
return {
artist: albums.rows[0].ArtistName,
albums: albums.rows,
}
}
Same pattern for the src/routes/artist/[artist_id]/+page.server.ts
file, call the query and return the data:
import { get_albums_by_artist_id } from '$lib/server/queries'
export const load = async ({ params }) => {
const artist_id = parseInt(params.artist_id)
const { artist, albums } = await get_albums_by_artist_id(artist_id)
return {
artist,
albums,
}
}
Then for the src/routes/artist/[artist_id]/+page.svelte
file I’ll
add in the props and render out the data:
<script lang="ts">
let { data } = $props()
const { artist, albums } = data
</script>
<svelte:head>
<title>{artist} - Chinook SvelteKit</title>
</svelte:head>
<h1 class="mb-5 text-4xl font-bold text-primary">{artist}</h1>
<p class="mb-10 text-3xl font-bold tracking-widest text-secondary">
Albums
</p>
<div class="overflow-x-auto">
<table
class="table table-pin-rows table-pin-cols table-xs md:table-lg"
>
<thead>
<tr class="text-xl">
<th>Title</th>
<th>Tracks</th>
</tr>
</thead>
<tbody>
{#each albums as album}
<tr class:hover={'bg-base-200'}>
<td>
<a
href={`/album/${album.AlbumId}`}
class="link link-primary"
>
{album.AlbumTitle}
</a>
</td>
<td>{album.TrackCount}</td>
</tr>
{/each}
</tbody>
</table>
</div>
Again adding a link this time to the album page, so, I can click on the album and get more information on that.
Next up, the genre by genre ID, so, in the src/lib/server/queries.ts
another query to get the albums by passing in the genre ID:
export const get_albums_by_genre = async (genre_id: number) => {
const albums = await client.execute({
sql: `SELECT
a.AlbumId,
a.Title AS AlbumTitle,
g.GenreId,
g.Name AS GenreName,
at.ArtistId,
at.Name AS ArtistName
FROM
albums a
JOIN
tracks t ON a.AlbumId = t.AlbumId
JOIN
genres g ON t.GenreId = g.GenreId
JOIN
artists at ON a.ArtistId = at.ArtistId
WHERE
g.GenreId = ?
GROUP BY
a.AlbumId, a.Title, g.GenreId, g.Name, at.ArtistId, at.Name;`,
args: [genre_id],
})
return {
genre: albums.rows[0].GenreName,
albums: albums.rows,
}
}
In the src/routes/genre/[genre_id]/+page.server.ts
file, call the
query and return the data:
import { get_albums_by_genre } from '$lib/server/queries.js'
export const load = async ({ params }) => {
const genre_id = parseInt(params.genre_id)
const { albums, genre } = await get_albums_by_genre(genre_id)
return {
albums,
genre,
}
}
Then the markup for the src/routes/genre/[genre_id]/+page.svelte
file to render out the data:
<script lang="ts">
let { data } = $props()
const { albums, genre } = data
</script>
<svelte:head>
<title>{genre} - Chinook SvelteKit</title>
</svelte:head>
<h1 class="mb-5 text-4xl font-bold text-primary">
<a href="/genre">
{genre}
</a>
</h1>
<div class="overflow-x-auto">
<table
class="table table-pin-rows table-pin-cols table-xs md:table-lg"
>
<thead>
<tr class="text-xl">
<th>Album</th>
<th>Artist</th>
</tr>
</thead>
<tbody>
{#each albums as album}
<tr class:hover={'bg-base-200'}>
<td>
<a
href={`/album/${album.AlbumId}`}
class="link link-primary"
>
{album.AlbumTitle}
</a>
</td>
<td>
<a
href={`/artist/${album.ArtistId}`}
class="link link-primary"
>
{album.ArtistName}
</a>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
More links to the album and artist pages, so, I can click on the album and get more information on that and the artist.
Also I’ll add in an index for the genre, so, in the src/lib/server/queries.ts
file a simple query to get all the genres
with no arguments passed to it:
export const get_genres = async () => {
const genres = await client.execute(
`SELECT GenreId, Name AS GenreName FROM genres ORDER BY Name;`,
)
return {
genres: genres.rows,
}
}
Then another server side route in the src/routes/genre/+page.server.ts
to call the query and return the
data:
import { get_genres } from '$lib/server/queries.js'
export const load = async () => {
const { genres } = await get_genres()
return {
genres,
}
}
Then the markup for the src/routes/genre/+page.svelte
file to render
out the data:
<script lang="ts">
let { data } = $props()
const { genres } = data
</script>
<svelte:head>
<title>Genres - Chinook SvelteKit</title>
</svelte:head>
<ul class="list-disc pl-10 text-xl">
{#each genres as { GenreName, GenreId }}
<li>
<a href={`/genre/${GenreId}`} class="link link-primary">
{GenreName}
</a>
</li>
{/each}
</ul>
Just a simple list of the genres that links back to the /genre/${GenreId}
route.
Finally the track by track ID, so, in the src/lib/server/queries.ts
file a query to get the track by ID:
export const get_track_by_track_id = async (track_id: number) => {
const track = await client.execute({
sql: `SELECT
t.TrackId,
t.Name AS TrackName,
a.AlbumId,
a.Title AS AlbumTitle,
at.ArtistId,
at.Name AS ArtistName,
g.GenreId,
g.Name AS GenreName,
(t.Milliseconds / 60000) || 'm ' || ((t.Milliseconds % 60000) / 1000) || 's' AS Duration,
mt.Name AS MediaType,
t.UnitPrice AS Price
FROM
tracks t
JOIN
albums a ON t.AlbumId = a.AlbumId
JOIN
artists at ON a.ArtistId = at.ArtistId
JOIN
genres g ON t.GenreId = g.GenreId
JOIN
media_types mt ON t.MediaTypeId = mt.MediaTypeId
WHERE
t.TrackId = ?;`,
args: [track_id],
})
return {
artist: track.rows[0].ArtistName,
track: track.rows,
track_name: track.rows[0].TrackName,
}
}
Then the server side route in the src/routes/track/[track_id]/+page.server.ts
file to call the query
and return the data:
import { get_track_by_track_id } from '$lib/server/queries.js'
export const load = async ({ params }) => {
const track_id = parseInt(params.track_id)
const { artist, track, track_name } =
await get_track_by_track_id(track_id)
return {
track_name,
artist,
track,
}
}
Then the markup for the src/routes/track/[track_id]/+page.svelte
file to render out the data:
<script lang="ts">
let { data } = $props()
const { artist, track, track_name } = data
</script>
<h1 class="mb-5 text-4xl font-bold text-primary">{track_name}</h1>
<p class="mb-10 text-3xl font-bold tracking-widest text-secondary">
<a href={`/artist/${track[0].ArtistId}`}>
{artist}
</a>
</p>
<svelte:head>
<title>{track_name} - Chinook SvelteKit</title>
</svelte:head>
<div class="text-xl">
<p><strong>Album Title:</strong> {track[0].AlbumTitle}</p>
<p><strong>Genre Name:</strong> {track[0].GenreName}</p>
<p><strong>Duration:</strong> {track[0].Duration}</p>
<p><strong>Media Type:</strong> {track[0].MediaType}</p>
<p><strong>Price:</strong> {track[0].Price}</p>
</div>
Deploying to Fly.io
Ok, I’ve got a nice little example project I can share with the world now!
I’ve already installed the Fly CLI from following the instructions from the Fly.io docs.
Fly makes getting a project set up straightforward with the fly launch
command:
fly launch
This configures the project, installs the @flydotio/dockerfile
package and creates several files, ones to note are the fly.toml
and
the Dockerfile
.
I’m asked set up options for the project and I leave most of them as
default apart from the primary region. It defaults to lhr
(as I’m in
the UK) but I change it to iad
as I’ve had issues in the past with lhr
.
Add in the Turso secrets at the top of the Dockerfile:
ARG BUN_VERSION=1.1.0
FROM oven/bun:${BUN_VERSION}-slim as base
# Declare build arguments for secrets
ARG TURSO_DB_URL
ARG TURSO_DB_AUTH_TOKEN
LABEL fly_launch_runtime="Bun"
Then after the COPY
commands add in the secrets for building the
app:
# Copy application code
COPY --link . .
# Build application using build arguments
RUN TURSO_DB_URL=$TURSO_DB_URL TURSO_DB_AUTH_TOKEN=$TURSO_DB_AUTH_TOKEN bun run build
# Remove development dependencies
RUN rm -rf node_modules &&
bun install --ci
I’m now ready to deploy the app, I’ll need to pass the build arguments in the terminal, I’ve got into the habit now of exporting the secrets to my terminal now so I can just use the variables in the command:
export TURSO_DB_URL=libsql://sveltekit-turso-flyio-app.turso.io
export TURSO_DB_AUTH_TOKEN=the-generated-auth-token
Then I can reference the variables from the terminal, so in the terminal I do:
echo $TURSO_DB_URL
I’ll get back the secret:
the-generated-auth-token
So with the fly command I can use the variables for the deploy command:
fly deploy --build-arg TURSO_DB_URL=$TURSO_DB_URL --build-arg TURSO_DB_AUTH_TOKEN=$TURSO_DB_AUTH_TOKEN
The CLI gives me links to the Fly.io dashboard to inspect the build!
Done! 🎉
I’m not a Docker or Fly.io expert, this is from my trail and error dicking around with multiple configurations and endless docs and community posts searching.
If this is wrong or can be done better please get in touch I’d love to make things right.
Bonus! You want that CRUD?
Well, I’m not going to go into that here, but, I’ll give you a head-start!
Essentially all the tools you need to do that are in the project they’re just a form action away!
I’m not going down that path though as, like I stated at the beginning if you’re going to let any random person on the internet create and delete data on the database then you’re going to need to do a bit more than the basics I have gone over here 😅
Things like authentication, possibly data scoped per user or a database per user with something like the multi tennant approach in the Creating a multitenant SaaS service with Turso, Remix, and Drizzle on the Turso blog!
Have fun!
There's a reactions leaderboard you can check out too.