A Svelte application with authentication and storage

How to build a minimalist mood tracker with an API, secure SQL storage and authentication in Svelte / Sapper.

Andreas Ehrencrona
Oct 16, 2020 · 7 min read

Svelte has been seeing increasing amounts of traction lately but there are sometimes a bit of a dearth of examples of how to use it to build real-life applications. This tutorial illustrates how to build a minimalist application that stores its data in a database and requires login.

You can find the complete code on Github.

Project setup

We will use Sapper, the Svelte-powered server for easily building isomorphic applications. You can set up a new project with a single command:

npx degit “sveltejs/sapper-template#rollup” sapper-mood

“sapper-mood” is the name of the project you’re creating.

By default, the project is plain JavaScript. To switch to TypeScript, run node scripts/setupTypeScript followed by yarn to install dependencies and yarn run dev to start the development server.

Storage

We will store the data in a Postgres database. Rather than hand-coding SQL in the application code, we will use Knex to build SQL queries. Knex is not a fully-fledged ORM (Object–Relational Mapping), that are frequently more effort than they are worth; it’s just an API to build SQL queries and to manage migrations.

Make sure you have Postgres running locally, then create a new database and optionally a new user for your database:

> psql
# create database coaching;
CREATE DATABASE
# create user mooduser with password 'mysecretpassword';
CREATE ROLE

Add Knex and Postgres drivers using yarn add knex pg followed by npx knex init which will create Knex’ configuration file knexfile.js .

Edit the file to enter the credentials of your local Postgres, for example

development: {
client: 'postgresql',
connection: {
database: 'mood',
user: 'mooduser',
password: 'mysecretpassword'
}
}

We now need to set up a database schema. There will be a single table storing mood data by user and date. Run npx knex migrate:make create_schema to create a migration file (called create_schema). Knex will automatically run any migrations it hasn’t already run, thereby keeping them your database schema to date.

In the migrations directory you will now have a file called <timestamp>_create_schema.js . It should export an up function (that applies the migration) and a down function (that undoes up in case you need to roll back:

exports.up = (knex) =>
knex.schema.createTable('mood', (table) => {
table.increments();
table.string('user').notNullable();
table.date('date').notNullable();
table.integer('score').notNullable();
table.unique(['user', 'date']);
});
exports.down = (knex) => knex.schema.dropTable('mood');

Run the migration using npx knex migrate:up to create the schema.

We can now retrieve a connection to the database using

import * as Knex from 'knex';
import knexfile from '../knexfile';
let connection: Knex;export function connectToDb() {
connection = Knex.default(
process.env.NODE_ENV === 'production'
? knexfile.production
: knexfile.development
);
}

The connection instance has methods for performing all common SQL commands. We can for example retrieve the mood history for a user using

export async function getMoodHistory(user: string) {
return await connection
.select('date', 'score')
.from('mood')
.where('user', user)
.orderBy('date', 'desc')
}

We should add some types and a storeMood function for updating of the current mood for a user. You can find the complete code in src/db.ts.

Don’t forget to add a call to connectToDb in your server.ts.

That completes the storage layer. You can’t really test it at the moment, so next we’ll add an API for accessing it.

API

Sapper makes it very easy to create an API endpoint. Just create a file called src/routes/api/mood.ts with a get and a put method for handling those two HTTP verbs.

The get method should return the current users’ mood history. This is the basic code required:

import { getMoodHistory } from '../../db';
import { formatDate, getToday } from '../../date';
export function getUserId(req: IncomingMessage) {
return 'noidea';
}
export async function get(req, res) {
const user = getUserId(req);
let history = await getMoodHistory(user); res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ history });
}

The get method works just like a route in Express, except that we don’t need to do the routing; Sapper does that for us based on the file name.

If you open http://localhost:3000/api/mood in your browser you should get the result {history:[]} since – indeed – the database is empty.

There is some stuff left: we should distinguish between today’s mood and previous days’, add some types and implement the put method. You can find the result in src/routes/api/mood.ts.

Authentication

To build a simple authentication layer, we will use Auth0, which is a commercial service but has a free tier. It allows you to instantly add signup, login and authentication using external OAuth providers such as Google or Facebook.

First, you need to create an Auth0 account and create an application from your dashboard. The application should be of type “Simple Web Application.”

The only configuration required is to set the Allowed Callback URL to http://localhost:3000/callback, the Allowed Logout URL to http://localhost:3000 and the Allowed Web Origin to http://localhost:3000.

If you know where you will host your application later, you can add multiple domains separated by commas.

Auth0 provides middleware that make integration into Express applications simple, but the Sapper template uses a different server called Polka. Fortunately, Express also supported. So start by replacing Polka with Express in server.ts ; you can literally do a search and replace:

// instead of: import polka from 'polka';
import express from 'express';
// ...express().use(...) // instead of polka().use(...)

The Auth0 middleware is called express-openid-connect , so add it as a dependency using yarn add express-openid-connect .

Then, you need to add the middleware in server.ts:

import authConfig from './authConfig';express.use(
// ...
// the following replaces the sapper.middleware() line:
auth(authConfig),
(req, res, next) =>
sapper.middleware({
session: () => ({
user: req['oidc'].user
})
})(req, res, next)

auth() handles authentication and allows only authenticated users to access the following routes. We override session in the Sapper middleware to take the user from Auth0 and put it in the Sapper session. This makes it available to the preload function of each route, both on the client and server side.

Put your Auth0 configuration into src/authConfig.ts :

export default {
authRequired: false,
auth0Logout: true,
baseURL: 'http://localhost:3000',
clientID: '<long sequence of letters and numbers>',
// a.k.a. "domain" in the Auth0 admin; prepend "https://"
issuerBaseURL: 'https://<an ID>.<eu/us>.auth0.com',
// choose a long, random string.
secret: 'AyBYwCy73cBystBD6iZgYFNdquqqP'
};

You find the Client ID and the domain (issueBaseURL) in your application when you log in to Auth0. See src/authConfig.ts for how to adapt this configuration for a production environment.

Now we can provide a better implementation for the getUserId function we added above:

function getUserId(req) {
return req['oidc'].user;
}

Just one more thing: the configuration above says that authentication is not required. That prevents users from being immediately redirected to the login page. But we still want the API routes to always require login. We can fix this by adding some more middleware in server.ts after auth() :

(req, res, next) => {
if (req.path.startsWith('/api') && !req['oidc']?.user) {
notAuthorized(res);
} else {
next();
}
},

In English: if the request path starts with /api and no user is logged in, send a 401 response, otherwise all is fine. The 401 response can look something like this:

function notAuthorized(res: Response) {
res.setHeader('Content-Type', 'application/json');
res.status(401).send(JSON.stringify({ error: 'Not authorized' }));
}

This completes the authentication. If you load /api/mood now, you should get Not authorized . If you go to http://localhost:3000/login you should get a login/registration page that redirects you back to the application once completed.

Frontend

On the frontend, we want to fetch the mood history and render it. Sapper has the concept of a “preload” function that is responsible for fetching data.

Put the following in the index.svelte :

<script context="module" lang="ts">
export async function preload() {
let history;
const res = await this.fetch(`/api/mood`);
if (res.status == 200) {
({ history } = await res.json());
}
return { history }
}
</script>

The preload function will execute both on the server side and on the client side. If the user is authenticated (which happens through a cookie), the status will be 200 (“OK”) and we can retrieve the history.

Whatever preload returns will be available as a property for the current component. We need to declare that we want the history property and then we are ready to use it:

<script lang="ts">
export let history: Day[];
</script>
<ul>
{#each history as day}
<li>
<div class="date">{day.date}</div>
<div class="score">{day.score}</div>
</li>
{/each}
</ul>

To set your current mood, we need to call the PUT method of the API. Add the storeMood function inside the <script> with the history declaration (not the one with the preload function):

const storeMood = (score: number) => {
fetch("/api/mood", {
method: "PUT",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ today: score })
})
};

We can now call it from the template:

<h3>How do you feel?</h3><button on:click={{} => storeMood(1)}>Good</button><button on:click={{} => storeMood(0)}>Bad</button>

This leaves you with a fully functioning application: If you log in and click the buttons you should see your database being populated.

The beauty of this is that you automatically get hydration: the HTML you get from the server will already contain the mood history, resulting in a page the renders even without JavaScript and does not load progressively; all the data is there as soon as you open it.

To complete the UI you should add some CSS, error handling and break it down into components. You can see what this results in in src/routes/index.svelte and src/components.

The completed application looks as follows

Conclusion

I hope this shows how easy it is to build a complete application with Svelte and Sapper. We get a lot of things for free, such as a service worker that makes the application work offline, server-side rendering and hydration for amazing performance and SEO.

JavaScript In Plain English

New JavaScript + Web Development articles every day.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store