Tutorials

Here you will find material from the different tutorials.

Tutorial 1: Client-side JS

Tutorial 2: Svelte

Covered the basics in Svelte.

Example of using components

<!-- lib/Counter.svelte -->
<script>
	
	export let startValue = 0
	export let incStep = 1
	
	let counter = startValue
	
	function handleClick(){
		counter += incStep
	}
	
</script>

<button on:click={handleClick}>
	{counter}
</button>

<style>
	button{
		color: blue;
	}
</style>
<!-- App.svelte -->
<script>
	
	import Counter from './lib/Counter.svelte'
	
</script>

<p>Click the buttons!</p>
<div>
	<Counter />
	<Counter startValue={5} incStep={2} />
	<Counter startValue={10} incStep={5} />
</div>

Example of using routing

First you need to install the npm package svelte-routingopen in new window (or use SvelteKit, if you prefer).

<!-- App.svelte -->
<script>
	
	import { Router, Link, Route } from 'svelte-routing'
	import Start from './lib/Start.svelte'
	import About from './lib/About.svelte'
	import Posts from './lib/Posts.svelte'
	
</script>

<Router>
	
	<header>
		Blogworld
	</header>
	
	<nav>
		<Link to="/">Start</Link>
		<Link to="/about">About</Link>
		<Link to="/posts">Posts</Link>
	</nav>
	
	<main>
		
		<Route path="/about" component={About}></Route>
		<Route path="/posts" component={Posts}></Route>
		<Route path="/" component={Start}></Route>
		
	</main>
	
	<footer>
		Copyright Peter L-G
	</footer>
	
</Router>
<!-- lib/Start.svelte -->
<h1>Welcome!</h1>
<p>Welcome to Blogworld! ...</p>
<!-- lib/About.svelte -->
<h1>About</h1>
<p>Blogworld is a website where...</p>
<!-- lib/Posts.svelte -->
<script>
	
	import { blogposts } from '../data.js'
	
</script>

<h1>Posts</h1>

{#if blogposts.length == 0}
	<p>There are no blogposts.</p>
{:else}
	<p>Here are the blogposts:</p>
	<ul>
		{#each blogposts as blogpost (blogpost.id)}
			<li>{blogpost.title}</li>
		{/each}
	</ul>
{/if}
/* data.js */
export const blogposts = [
	{id: 1, title: "A new journey", content: "Some content..."},
	{id: 2, title: "Yes!", content: "Great news! ..."},
	// ...
]

Tutorial 3: REST API & Docker (DB)

/* project/database/init.sql */
CREATE TABLE humans (
	id INT PRIMARY KEY AUTO_INCREMENT,
	name VARCHAR(50)
);

INSERT INTO humans (name) VALUES ('Alice');
# project/database/Dockerfile
FROM mariadb:10.9.4

COPY ./init.sql /docker-entrypoint-initdb.d/
# project/compose.yaml
services:
  db:
    build: ./database/
    ports:
      - "5555:3306"
    environment:
      MARIADB_ROOT_PASSWORD: abc123
      MARIADB_DATABASE: abc

Tutorial 4: REST API & Docker (Backend)

Use the same database files as in Tutorial 3.

project/backend/package.json

{
  "name": "backend",
  "version": "1.0.0",
  "type": "module",
  "dependencies": {
    "express": "^4.18.2",
    "mariadb": "^3.1.0"
  }
}
// project/backend/src/app.js
import express from 'express'
import { createPool } from 'mariadb'

const pool = createPool({
	host: "db",
	port: 3306,
	user: "root",
	password: "abc123",
	database: "abc",
})

pool.on('error', function(error){
	console.log("Error from pool", error)
})

const app = express()

app.get("/humans", async function(request, response){
	
	console.log("Hello there hi")
	
	try{
		
		const connection = await pool.getConnection()
		
		const query = "SELECT * FROM humans ORDER BY name"
		
		const humans = await connection.query(query)
		
		response.status(200).json(humans)
		
	}catch(error){
		console.log(error)
		response.status(500).end()
	}
	
})

app.get("/", function(request, response){
	response.send("It works")
})

app.listen(8080)
# project/backend/Dockerfile
FROM node:18.14.1

WORKDIR /backend

COPY ./package*.json ./

RUN npm install

COPY ./src/ ./

CMD node --watch ./src/app.js
# project/compose.yaml
services:
  db:
    build: ./database/
    ports:
      - "5555:3306"
    environment:
      MARIADB_ROOT_PASSWORD: abc123
      MARIADB_DATABASE: abc
  backend:
    build: ./backend/
    ports:
      - "8080:8080"
    volumes:
      - ./backend/src/:/backend/src/

Tutorial 5: AJAX & OAuth 2.0

Shows how to use the backend from the frontend using the REST API, and how to add authorization to the REST API using access tokens.

Backend

app.js

const express = require('express')
const jwt = require('jsonwebtoken')

const MIN_NAME_LENGTH = 3
const MAX_NAME_LENGTH = 10
const MAX_AGE = 150

const ACCESS_TOKEN_SECRET = "ablkdjflksjdflsdjf"

const humans = [{
	id: 1,
	name: "Alice",
	age: 15,
}, {
	id: 2,
	name: "Bob",
	age: 20,
}]

const app = express()

app.use(express.json())
app.use(express.urlencoded())

app.use(function(request, response, next){
	
	response.set("Access-Control-Allow-Origin", "*")
	response.set("Access-Control-Allow-Methods", "*")
	response.set("Access-Control-Allow-Headers", "*")
	response.set("Access-Control-Expose-Headers", "*")
	
	next()
	
})

app.get('/humans', function(request, response){
	setTimeout(function(){
		response.status(200).json(humans)
	}, 1000)
})

app.get('/humans/:id', function(request, response){
	
	const id = request.params.id
	
	const human = humans.find(h => h.id == id)
	
	if(human){
		response.status(200).json(human)
	}else{
		response.status(404).end()
	}
	
})

app.post('/humans', function(request, response){
	
	// TODO: This code will crash if no Authorization header value is provided.
	const authorizationHeaderValue = request.get("Authorization")
	const accessToken = authorizationHeaderValue.substring(7)
	
	jwt.verify(accessToken, ACCESS_TOKEN_SECRET, function(error, payload){
		if(error){
			response.status(401).end()
		}else{
			
			const human = request.body
			
			const errorCodes = []
			
			if(typeof human?.name != "string"){
				errorCodes.push("nameIsMissing")
			}else if(human.name.length < MIN_NAME_LENGTH){
				errorCodes.push("nameIsTooShort")
			}else if(MAX_NAME_LENGTH < human.name.length){
				errorCodes.push("nameIsTooLong")
			}
			
			if(typeof human?.age != "number"){
				errorCodes.push("ageIsMissing")
			}else if(human.age < 0){
				errorCodes.push("ageIsNegative")
			}else if(MAX_AGE < human.age){
				errorCodes.push("ageIsTooHigh")
			}
			
			if(0 < errorCodes.length){
				response.status(400).json(errorCodes)
				return
			}
			
			human.id = humans.at(-1).id + 1
			
			humans.push(human)
			
			response.set('Location', `/humans/${human.id}`)
			response.status(201).end()
			
		}
		
	})
	
})

app.post('/tokens', function(request, response){
	
	const grantType = request.body.grant_type
	const username = request.body.username
	const password = request.body.password
	
	if(grantType != "password"){
		response.status(400).json({error: "unsupported_grant_type"})
		return
	}
	
	if(username == "abc" && password == "abc123"){
		
		const payload = {
			isLoggedIn: true,
		}
		
		jwt.sign(payload, ACCESS_TOKEN_SECRET, function(error, accessToken){
			
			if(error){
				response.status(500).end()
			}else{
				response.status(200).json({
					access_token: accessToken,
					type: "bearer",
				})
			}
			
		})
		
	}else{
		
		response.status(400).json({error: "invalid_grant"})
		
	}
	
})

app.listen(8080)

Frontend

user-store.js

import { writable } from "svelte/store"

export const user = writable({
	isLoggedIn: false,
	accessToken: "",
})

App.svelte

<script>
	
	import { Router, Link, Route } from "svelte-routing"
	
	import Home from './lib/Home.svelte'
	import Humans from "./lib/Humans.svelte"
	import Human from "./lib/Human.svelte"
	import CreateHuman from "./lib/CreateHuman.svelte"
	import Login from "./lib/Login.svelte"
	
	import { user } from "./user-store.js"
	
</script>

<div id="layout">
	
	<Router>
		
		<header>
			Human Site
		</header>
		
		<nav>
			<Link to="/">Home</Link>
			<Link to="/humans">Humans</Link>
			{#if $user.isLoggedIn}
				<Link to="/humans/create">Create Human</Link>
			{:else}
				<Link to="/login">Login</Link>
			{/if}
		</nav>
		
		<main>
			<Route path="/humans" component={Humans} />
			<Route path="/humans/create" component={CreateHuman} />
			<Route path="/humans/:id" component={Human} />
			<Route path="/login" component={Login} />
			<Route path="/" component={Home} />
		</main>
		
		<footer>
			Copyright Peter L-G 2023
		</footer>
		
	</Router>
	
</div>

<style>
	
	#layout{
		max-width: 600px;
		margin: 0 auto;
	}
	
	header{
		font-size: 2em;
		text-align: center;
	}
	
	nav{
		text-align: center;
	}
	
	footer{
		margin-top: 1em;
		text-align: center;
	}
	
</style>

Login.svelte

<script>
	
	import { user } from "../user-store.js"
	
	// TODO: Add loading indicator.
	let username = ""
	let password = ""
	
	async function login(){
		
		const response = await fetch("http://localhost:8080/tokens", {
			method: "POST",
			headers: {
				"Content-Type": "application/x-www-form-urlencoded"
			},
			body: `grant_type=password&username=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}`
		})
		
		// TODO: Need to check status code, etc.
		const body = await response.json()
		
		const accessToken = body.access_token
		
		$user = {
			isLoggedIn: true,
			accessToken,
		}
		
	}
	
</script>

<h1>Login</h1>
<form on:submit|preventDefault={login}>
	
	<div>
		Username:
		<input type="text" bind:value={username}>
	</div>
	
	<div>
		Password:
		<input type="password" bind:value={password}>
	</div>
	
	<input type="submit" value="Login">
	
</form>

Humans.svelte

<script>
	
	import { Link } from 'svelte-routing'
	
	const fetchHumansPromise = fetch("http://localhost:8080/humans")
	
</script>

<h1>Humans</h1>

{#await fetchHumansPromise}
	
	<p>Wait, I'm loading...</p>
	
{:then response}
	
	{#await response.json() then humans}
		
		<ul>
			{#each humans as human (human.id)}
				<li>
					<Link to="/humans/{human.id}">{human.name}</Link>
				</li>
			{/each}
		</ul>
		
	{/await}
	
{:catch error}
	
	<p>Something went wrong, try again later.</p>
	
{/await}

Human.svelte

<script>
	
	export let id
	
	let isFetchingHuman = true
	let failedToFetchHuman = false
	let human = null
	
	async function loadHuman(){
		
		try{
			
			const response = await fetch("http://localhost:8080/humans/"+id)
			
			switch(response.status){
				
				case 200:
					human = await response.json()
					isFetchingHuman = false
					break
				
			}
			
		}catch(error){
			
			isFetchingHuman = false
			failedToFetchHuman = true
			
		}
		
	}
	
	loadHuman()
	
</script>

<h1>Human</h1>
{#if isFetchingHuman}
	<p>Wait, I'm fetching data...</p>
{:else if failedToFetchHuman}
	<p>Couldn't fetch the human. Check your Internet connection.</p>
{:else if human}
	<div>Id: {human.id}</div>
	<div>Name: {human.name}</div>
	<div>Age: {human.age}</div>
{:else}
	<p>No human with the given id {id}.</p>
{/if}

CreateHuman.svelte

<script>
	
	import { user } from "../user-store.js"
	
	// TODO: Add loading indicator.
	let name = ""
	let age = 0
	let errorCodes = []
	let humanWasCreated = false
	
	async function createAccount(){
		
		const human = {
			name,
			age,
		}
		
		try{
			
			const response = await fetch("http://localhost:8080/humans", {
				method: "POST",
				headers: {
					"Content-Type": "application/json",
					"Authorization": "Bearer "+$user.accessToken,
				},
				body: JSON.stringify(human),
			})
			
			switch(response.status){
				
				case 201:
					humanWasCreated = true
				break
				
				case 400:
					errorCodes = await response.json()
				break;
				
				// TODO: Handle 401.
				
			}
			
		}catch(error){
			errorCodes.push("COMMUNICATION_ERROR")
			errorCodes = errorCodes
		}
		
	}
	
</script>

<h1>Create Human</h1>

{#if humanWasCreated}
	<p>Human created!</p>
{:else}
	
	<form on:submit|preventDefault={createAccount}>
		
		<div>
			Name:
			<input type="text" bind:value={name}>
		</div>
		
		<div>
			Age:
			<input type="number" bind:value={age}>
		</div>
		
		<input type="submit" value="Create Human">
		
	</form>
	
	<!-- TODO: Show error messages instead of error codes. -->
	{#if 0 < errorCodes.length}
		<p>We have errors!</p>
		<ul>
			{#each errorCodes as errorCode}
				<li>{errorCode}</li>
			{/each}
		</ul>
	{/if}
	
{/if}