Blog Posts

2023-12-03

My new web dev stack with Sveltekit

This blog is about the web development stack I have settled on for web applications. By this I mean sites which provide interactive functionality to the user, usually with database integration and interactive widgets. My dev stack for more static sites, such as the one you are reading, is on an entirely different stack and will be the topic of a future article.

I like Python. In the past, Django was my go-to framework for web development. When the status quo moved to JavaScript reactive applications, I switched to the Django REST Framework for the backend. However, I never really took to React, and asking more scientifically-minded developers to use it was always going to be tough.

Recently I started playing with Svelte. This is another reactive framework but, unlike React, contains JavaScript language extensions. In other words, it needs to be compiled to JavaScript before use.

React fans argue in favour of React's simplicity, boasting how small the library is. However, unless you are a strong JavaScript developer, the syntax is likely be overwhelming. When working with people whose speciality is in areas other than web development, some syntactic niceness goes a long way.

Reactive frameworks

For those who don't know what reactive frameworks are, they provide a way to bind JavaScript variables to components. Here's an example. Say you want to show the square of a number. In conventional HTML and JavaScript, you might do it like this:

<script>
function squareNumber() {
    let n = Number(document.getElementById('number-input').value);
    document.getElementById('number').innerHTML = n;
    document.getElementById('square').innerHTML = n*n;
}
</script>
<p>Enter a number: <input type="text" id="number-input" value="2" /></p>
<p><button onClick="squareNumber()">Calculate</button>
<p>The square of <span id="number">2</span> is <span id="square">4</span>

jQuery provides a more powerful and compact version of getElementById(), but conceptually it is the same.

Reactive frameworks let you bind an attribute of a component directly to a JavaScript variable. In Svelte, the above example could be written as:

<script>
    let number = 2;
</script>
<p>Enter a number: <input type="text" bind:value={number}" /></p>
<p>The square of {number} is {number*number}</p>

Whenever you update the number in the input field, Svelte automatically updates the text. We no longer even need the button.

For this trivial example, it might seem not worth effort. However, if you have many components with many interactions, your code becomes a lot cleaner, reducing the chance of errors.

Svelte also lets you define components that can be reused, for example:

<!-- NumberSpinner.svelte -->
<script>
    export let number;
</script>
<button on:click={number -= 1}>
<input type="text" pattern='[0-9]*'>
<button on:click={number += 1}>
<!-- App.svelte -->
<script>
    import NumberSpinner from './NumberSpinner.svelte';
    let myNumber = 2;
</script>
<NumberSpinner number={myNumber} />
<p>The square of {myNumber} is {myNumber*myNumber}</p>

So Svelte (and other reactive frameworks) make team work easier with self-contained, reusable components.

Svelte and Python

Svelte doesn't dictate your backend language. My first applications with Svelte had Django REST Framework for the backend. If your backend API is only being used by your frontend, you can keep Django's default authentication with session ID cookies. You can also keep Django's ORM to manage database queries and migrations.

This is how many people develop web applications - separation of front and backend. However, two limitations brought me to look for a more integrated solution:

To get the output from your backend API call into Svelte, you need a fetch() call, something like this:

export async function apiPostCall(baseurl, url, params, withCsrf) {
    var headers = {
        'Content-Type': 'application/json',
    }
    if (withCsrf) {
        await getCSRF(baseurl);
        headers['X-Csrftoken'] = localStorage.getItem("csrftoken");
    }

    const response = await fetch(baseurl + "/api/" + url + "/", {
        method: "POST",
        credentials: "include",
        headers: headers,
        body: JSON.stringify(params),

    });
    if (!response.ok) {
        throw new Error("Couldn't make request.  Our server may be down");
    }
    const data = await response.json();
    if (!data.success) {
        throw new Error(data.msg);
    }
    return data;
}

That feels very messy.

The problem for SEO is your page is a JavaScript application. Often, routing is managed in JavaScript too, rather than each route having a separate endpoint. This makes it harder for search engines to index your site. Added to that, while Svelte is pretty fast, having your browser render each page is a performance overhead.

These are the issues reactive full stack frameworks are designed to address. React has Next.js. Vue has Nuxt.js. And Svelte has... Sveltekit.

Sveltekit gives you the following:

Each endpoint has its own directory, with a +page.svelte for the client-side code and optionally +page.server.js for server-side code (and some other optional files too). Here's an example showing how server-side data is passed to a client component:

// +page.server.js
export const load = async () => {
    
    const todos = await /* code to get todos from my database */;

    return {
        todos,
    }
}
<!-- +page.svelte -->
<script>
	export let data;

</script>
<h1>My ToDo's</h1>
<div id="main">
    <ul>
        {#each Object.entries(data.todos) as [id, item], index (id)}
            <li>{item.content}</li>
        {/each}
    </ul>    
</div>

The tradeoff for this simplicity? Abandoning Python for backend development and switching to Javascript instead.

Typescript

I'm also a recent convert to Typescript. Typescript adds strong typing to Javascript, and is fully supported by Svelte and Sveltekit. For simple types. you add the type to the variable, eg

let todo : string = "Do this important task";

For structured types, you can define an interface, for example:

interface ToDo {
  id : number;
  description : string;
}
let todo : ToDo = {
    id: 1,
    description: "Do this important task";
}

The advantage: compile-time errors, and syntax checking in supporting editors like Visual Studio Code.

Sveltekit comes with Typescript interfaces to automatically add typing between front- and backen:

import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async () => {
    const todos: ToDos = await /* code to get todos from my database */;

    return {
        todos,
    }
}
<!-- +page.svelte -->
<script>
    import type { PageData } from './$types';
	export let data : PageData;

</script>
<h1>My ToDo's</h1>
<div id="main">
    <ul>
        {#each Object.entries(data.todos) as [id, item], index (id)}
            <li>{item.content}</li>
        {/each}
    </ul>    
</div>

Harmonized front and backend

I found switching from Python to Typescript for backend development a pain, until I discovered that I only have one set of dependencies, and they work in both front- and backend. I can shift functionality between them without rewriting. All dependencies are managed by npm.

Prisma for ORM

I like ORMs (object relational mapping). These automate the mapping between database tables and objects in your chosen language. It's not to save me writing SQL. I worked for decades in banking and insurance - I know SQL fine. But ORMs have other advantages:

I'll come to these in a moment.

Django has its built-in ORM which is performant and easy to use. For other Python developers, there's SQLAlchemy. Luckily there are also some for JavaScript. My preferred one is Prisma

With Prisma, you define your database models in a schema.prisma file:

// schema.prisma
generator client {
    provider = "prisma-client-js"
}

datasource db {
    provider = "sqlite"
    url      = env("DATABASE_URL")
}

model ToDo {
    id              Int     @id @unique
    content         String
}

The database actual database select code from the +page.server.ts file above is

import type { PageServerLoad } from './$types';
import { PrismaClient } from '@prisma/client'
import { DATABASE_URL } from '$env/static/private';

export const load: PageServerLoad = async () => {
    const prisma = new PrismaClient({
        datasources: {
            db: {
                url: DATABASE_URL,
            },
        },
    });
    
    const todos = await prisma.toDo.findMany();

    return {
        todos,
    }
}

You can see from this that Prisma fetches columns from my ToDo table and puts them into an associative array. const todos. Sveltekit manages passing this from the server to client.

The other things ORMs (including Prisma) give you is database migrations. Instead of manually updating all development and production databases each time you change your schema, Prisma gives you a way to create these automatically from your schema.prisma, which each datbase instance remembering which changes have been applied.

Component library and styling

Highly interactive applications need things like buttons, dialogs, dropdowns, accordions. We want them to be stylistically harmonious.

Some developers are happy to code their components and style their pages manually. However, in the interests of rapid development, I like toolkits that provide a rich set of components I can use without thinking. Some of these toolkits come unstyled, others with more opiniated styling. I'm not much of a designer so I like mine styled nicely out of the box.

There are two that I like: Daisy UI and Carbon Components. I really like Daisy UI. It is based on TailwindCSS, which I'll come to later. It is easy to use, framework argnostic, and it does look good. However, and this is a personal opinion, it is not a style best suited for understated, corporate applications. It's fun, and colourful, but understated it is not.

Carbon Components is more corporate. It's still easy to use but is not based on Tailwind. It is themable but, to me, it looks fine out of the box. It is based on IBM's Carbon Design System - a bit similar to Google's Material. It has nice Svelte bindings, carbon-components-svelte. Documentation is not brilliant, but the documentation pages include a link to the code which is easy enough to read to serve well in place of actual documentation.

Here's an example sign-in form with Carbon Components:

<script lang="ts">
    import { Form, TextInput, PasswordInput, Button, Link } from 'carbon-components-svelte'
</script>

<h1>Sign in</h1>
<div id="inputs">
    <Form method="post">
        <TextInput name="username" labelText="User name" placeholder="Enter user name..." /><br>
        <PasswordInput name="password" labelText="Password" placeholder="Enter password..." /><br>
        <Button type="submit">Log In</Button>
    </Form>
</div><br>
<Link href="/signup">Create an account</Link>

<style>
    #inputs {
        margin-top: 1ex;
        max-width: 400px;
    }
</style>

It looks like this:

Svelte Carbon Components Example

Apart from the navigation bar and the small amount of styling you see at the bottom of the code, there is no additional styling.

TailwindCSS

I have become a fan of Tailwind, which Daisy UI is based on but sadly not Svelte Carbon Components. Tailwind calls itself a utility first CSS framework. Instead of writing CSS to apply styling based on an element class or ID, and putting them between <style> and </style> tags or a .css file, Tailwind provides classes encapsulating low-level style aspects.

For example, a paragraph with a background of 800 Zinc (this background), a foreground of Sky 300 (the first paragraph in this document) and a margin of 4rem (16px), you would just do

<p class="bg-zinc-800 text-sky-300 m-4">
    This is my paragraph
</p>

Tailwind relies on PostCSS to postprocess CSS definitions. Fortunately the Vite development framework, which Svelte and Sveltekit uses by default, manages all this postprocessing transparently (plus Svelte and Typescript compilation).

Although Tailwind looks like it results in obscene amounts of duplicated code, it does actually speed up development and makes maintenance easier. It's quite seductive. And you can define your own classes to group things together, which I'll talk about in another blog.

Authentication with Lucia

Authentication always messes up a nicely streamlined frontend/backend application. Auth.js has Sveltekit integration. Auth.js is a comprehensive and very generic framework for OAuth2 authentication and authorization. If you're using an external OpenID Connect provider for your authentication, such as Google or Github, Auth.js is pretty easy to use. If you want to provide your own user database, or use your corporate LDAP, things get more complicated.

I settled on Lucia Auth. It is simple, low-level enough to intuitively support LDAP or other OpenID Connect providers, while adding username and password authentication is surprisingly easy. It supports multiple database backends, including Prisma.

Lucia requires a certain mimimum when it comes to user and session ID tables but, beyond that, you are free to add whatever you want to them. Here is what I have:

generator client {
    provider = "prisma-client-js"
}

datasource db {
    provider = "sqlite"
    url      = env("DATABASE_URL")
}

// Lucia tables

model User {
    id                  String      @id @unique
    username            String      @unique

    auth_session Session[]
    key          Key[]

    @@index([id])
    @@index([username])
}

model Session {
    id             String @id @unique
    user_id        String
    active_expires BigInt
    idle_expires   BigInt
    user           User   @relation(references: [id], fields: [user_id], onDelete: Cascade)

    @@index([id])
    @@index([user_id])
}

model Key {
    id              String  @id @unique
    hashed_password String?
    user_id         String
    user            User    @relation(references: [id], fields: [user_id], onDelete: Cascade)

    @@index([id])
    @@index([user_id])
}

// My non-Lucia tables

model ToDo {
    id              Int     @id @unique
    content         String
}

Lucia provides a getting-started guide for Sveltekit (sadly, the author seems to have stopped halfway through, but the generic getting-started guide takes you from there). It works with locals, Sveltekit's way of transparently passing secure data between server and client.

Here is the +page.server.ts for the sign-in page I showed before:

// borrowed from https://lucia-auth.com/guidebook/sign-in-with-username-and-password/sveltekit/
import { auth } from "$lib/server/lucia";
import { LuciaError } from "lucia";
import { fail, redirect } from "@sveltejs/kit";

import type { PageServerLoad, Actions } from "./$types";

export const actions: Actions = {
	default: async ({ request, locals }) => {
		const session = await locals.auth.validate();
		console.log(session);
		if (session) throw redirect(302, "/");

		const formData = await request.formData();
		const username = formData.get("username");
		const password = formData.get("password");

		try {
			// find user by key and validate password
			const key = await auth.useKey(
				"username",
				username.toLowerCase(),
				password
			);
			const session = await auth.createSession({
				userId: key.userId,
				attributes: {}
			});
			locals.auth.setSession(session); // set session cookie
		} catch (e) {
			if (
				e instanceof LuciaError &&
				(e.message === "AUTH_INVALID_KEY_ID" ||
					e.message === "AUTH_INVALID_PASSWORD")
			) {
				// user does not exist or invalid password
				return fail(400, {
					message: "Incorrect username or password"
				});
			}
			return fail(500, {
				message: "An unknown error occurred"
			});
		}
		throw redirect(302, "/");
	}
};

The global +layout.server.ts, Sveltekit's file that is executed on the backend for all requests, passes the Lucia user object to front-end pages:

import type { LayoutServerLoad } from './$types';

export const load: LayoutServerLoad = async ({ locals }) => {
    return {
		user: locals.user,
	};
};

In the layout.svelte, the front-end code that is executed for all pages, I can check if the user is logged in and act accordingly:

// ...
{#if data.user == null}
    // do stuff for when no user is logged in
{:else}
    // do stuff for when a user is logged in
{/if}
// ...

Development and production

Vite provides a development server and watches for code changes. And naturally all the above tools integrate nicely in Visual Studio Code.

Unless you are producing static files only with Sveltekit, you will need to run Node on your server. My initial concern was that Node is single threaded. Apache and Nginx, for example, are not. Enter PM2.

PM2 provides the multitasking that Node doesn't (conveniently). Node has a cluster mode, which is leveraged by PM2. PM2 takes care of firing up your desired number of instances (by default, equal to the number of cores on your server). PM2 manages process monitoring, daemonization and integration into system startup (eg systemd).

The multithreading (well, multiprocess) is transparent. Each instance is on the same port. PM2 can scale to multiple servers and also integrates with Docker.

PM2 doesn't provide TLS/SSL. For this, use a reverse proxy like Nginx.

Adding Python

With Sveltekit, Prisma and Tailwind, I have come to like Javascript and Typescript, a lot. But they don't do everything. If you need to access GPUs, perform system operations, machine learning or anything else not supported by Javascript, you'll need to add Python or another language to your stack.

My preferred way of doing this is to keep my Sveltekit Typescript backend and run a separate API, served by Python over REST, and call this from the Sveltekit backend. This keeps the Python to only those parts of the application it is needed for, leaving the rest nicely integrated between front- and backend. If it's on the same host as the Node server, it is easier to hide it behind the firewall than to worry about authentication. If not, OAuth2 does a perfectly good job.

I've come to like FastAPI. Unlike Django, it is asynchronous out of the box (yes I know that Django does now support it). It is lightweight and well suited to situations where the bulk of your app is served by Node and you only need to supplement it with Python endpoints.

Summary

This blog described my web application development stack. It is modern and reactive but not so complex that you need to be a Javascript genius to work with it. The stack is:

In another blog I'll run through my static site stack.

Matt Baker - technology and scientific IT blog