Nils Röhrig | JavaScript & Angular Days Berlin 2024

Full-Stack Development with

Svelte & SvelteKit (CF Edition)

The Trainer

 

  • Nils Röhrig, 38

 

  • Software Engineer @ Loql

 

  • Frontend focus, full-stack profile

 

  • Svelte user since 2019

 

  • Speaker at conferences and meetups

Agenda

Coffee Break
10:30 - 11:00

Lunch Break
12:30- 13:30

Coffee Break
15:00 - 15:30

Begin
09:00

End
17:00

Part I

  • Svelte Components
  • Runes
  • Snippets
  • Event Handling
  • Animation & Transition

Part II

  • Cloudflare Dev Platform
  • Prisma ORM
  • SvelteKit Routing
  • SvelteKit Data Loading
  • SvelteKit Form Actions

Part III

  • Authentification
  • Authorisation
  • SvelteKit Layouts
  • ...

Part I

What is Svelte?

Svelte is a UI framework that uses a compiler to let you write breathtakingly concise components that do minimal work in the browser, using languages you already know — HTML, CSS and JavaScript. It’s a love letter to web development.

Svelte is a component framework

Svelte is a component framework

Logo
Register
Home
E-Mail
Submit

etc. pp...

Component

Component

Component

Svelte is a compiler

Svelte is a compiler

<h1>Simple Component</h1>

Component.svelte

Svelte is a compiler

<h1>Simple Component</h1>

Svelte is a compiler

<h1>Simple Component</h1>

Svelte is a compiler

<h1>Simple Component</h1>
import { SvelteComponent, detach, element, init,
         insert, noop, safe_not_equal } from "svelte/internal";

function create_fragment(ctx) {
  let h1;

  return {
    c() {
      h1 = element("h1");
      h1.textContent = "Simple Component";
    },
    m(target, anchor) {
      insert(target, h1, anchor);
    },
    p: noop,
    i: noop,
    o: noop,
    d(detaching) {
      if (detaching) detach(h1);
    },
  };
}

class SimpleComponent extends SvelteComponent {
  constructor(options) {
    super();
    init(this, options, null, create_fragment, safe_not_equal, {});
  }
}

export default SimpleComponent;

Svelte is a compiler

import * as $ from "svelte/internal/client";

var root = $.template(`<h1>Simple Component</h1>`);

export default function Component($$anchor) {
  var h1 = root();
  $.append($$anchor, h1);
}

Component.js

Svelte is a compiler

<h1>Simple Component</h1>
import { mount } from "svelte";
import Component from "./Component.js";

mount(Component, {
  target: document.querySelector("#root"),
  props: { some: "prop" }
});

main.js

Part I

Svelte Component Basics

Anatomy of a component

Anatomy of a component

<script>
  // STATE & BEHAVIOR
</script>

<!-- DECLARATIVE MARKUP -->
<h1>Hello Workshop People!</h1>
<p>{"Any expression works here!"}</p>

<style>
  /* PRESENTATION */
</style>

Component.svelte

Runes

Runes are symbols [...] to control the Svelte compiler. If you think of Svelte as a language, runes are part of the syntax — they are keywords.

$state()

$state()

Reactive State:
$state() creates a reactive state variable that automatically updates the UI upon changes.
 

Deep Reactivity:
Arrays and objects are deeply reactive, triggering updates on property modifications.
 

Usage in Classes:
Class fields can become reactive state properties with $state().

$state()

<script>
	let counter = $state(0);

	const inc = () => counter++;
	const dec = () => counter--;
</script>

<button onclick={dec}>-</button>
<output>{counter}</output>
<button onclick={inc}>+</button>

Counter.svelte

$derived()

$derived()

Reactive Derivation:
$derived allows creating reactive variables based on other state variables, updating automatically when dependencies change.
 

Purity:
Derivations must be pure, with no side effects, ensuring predictable behavior. Mutations are disallowed in derivations.

$derived()

<script>
	let counter = $state(0);
	
	let doubled = $derived(counter * 2);

	const inc = () => counter++;
	const dec = () => counter--;
</script>

<button onclick={dec}>-</button>
<output>{doubled}</output>
<button onclick={inc}>+</button>

DoubledCounter.svelte

$props()

$props()

Declaration of Component Props:
$props()is used to declare component properties, that can be provided from the outside of the component.


Type Support:

Component props can be typed by either using TypeScript or JSDoc comments.

 

Read-only by Default: Props are read-only unless declared with $bindable(), preventing unintended mutations and ensuring predictable behavior.

$props()

<script lang="ts">
	type CounterProps = {
		initialValue?: number;
	}
	
	let { initialValue = 0 } = $props();
	let value = $state(initialValue);

	const inc = () => value++;
	const dec = () => value--;
</script>

<button onclick={dec}>-</button>
<output>{value}</output>
<button onclick={inc}>+</button>

Counter.svelte

<script>
	import Counter from './Counter.svelte'
</script>

<Counter initialValue={10} />

Component.svelte

Logic Blocks

Logic Blocks

Logic Blocks in Svelte:

Logic blocks are a core feature in Svelte that allow you to control the flow within your markup.

 

Familiar Control Structures:

Just like in other programming languages, Svelte offers various control structures through logic blocks. You can use {#if} and {:else} for conditional rendering, and {#each} to create loops.

Logic Blocks

<script>
  let condition = false
</script>

<p> <input type="checkbox" bind:checked={condition} /> </p>

{#if condition}
	<p>The condition is true!</p>
{:else}
	<p>The condition is false!</p>
{/if}

IfElse.svelte

Logic Blocks

<script>
  let someIterable = [1, 2, 3, 4, 5]
</script>

<ul>
  {#each someIterable as item}
	<li>{item}</li>
  {:else value}
      <li>No items found</li>
  {/each}
</ul>

Each.svelte

Keyed Each Blocks

Handling Changes in {#each} Blocks in Svelte:

When the value of an {#each} block changes, Svelte adds or removes elements only at the end of the block, while updating existing elements at their current positions in the DOM.

 

Avoiding Unintended Effects:

To prevent unwanted behavior, especially if elements don’t directly or reactively depend on the change, using unique IDs for each item can help keep things in order.

Keyed Each Blocks

Snippets and Render Tags

Snippets, and render tags, are a way to create reusable chunks of markup inside your components.”

Snippets and Render Tags

Reusable Markup:
Snippets enable the creation of reusable markup within components, reducing code duplication.

 

Destructuring Parameters:
Snippet parameters can be destructured for convenience and can have default values.

 

Scope and Visibility:
Snippets can access values from their lexical scope, including sibling components.

Snippets and Render Tags

<script>
	let cards = [{ title: "A Card", content: "This is a card."},
    {
		title: "Another Card",
		content: "This is another card.",
		href: "http://somewhere-on-the-inter.net"
	}]
</script>

{#snippet cardSnippet(title, content)}
  <article class="card">
		<header>{title}</header>
		<p>{content}</p>
	</article>
{/snippet}

{#each cards as card}
	{#if card.href}
		<a href={card.href}>{@render cardSnippet(card.title, card.content)}</a>
	{:else}
		{@render cardSnippet(card.title, card.content)}
	{/if}
{/each}

Cards.svelte

Event Handlers

Event Handlers

React to User Input:
Event handlers react allow reacting to user input. They work pretty much the same as in regular DOM-based applications.

 

Declarative Props:
Event handlers are just component props and can be used as such on components and HTML elements alike.

 

Events as Callbacks:
Components, that trigger events, do so by accepting and calling a callback property.

<script>
	function onsubmit(event) {
		event.preventDefault();
		const formData = new FormData(event.target);
		console.log(formData.get("name"));
	}

	const handleSubmitButtonClick = () => console.log("submit button used")
</script>

<form {onsubmit}>
	<input name="name" />
	<button onclick={handleSubmitButtonClick}>Submit</button>
</form>

EventHandlers.svelte

Event Handlers

Animations & Transitions

Animations & Transitions

Transitioning Elements:

When elements enter or leave the DOM, Svelte enables smooth visual transitions between the states with the transition: directive.


With the :in and :out directives, transition can be run only when an element enters or leaves respectively.

 

Animate Elements:
When elements within keyed each blocks are re-ordered, they can be animated with the animate: directive.

<script>
	import { slide, fade, fly, scale } from 'svelte/transition';
	let showBoxes = $state(true);
	let duration = $state(2000);
</script>


<label>Transition duration: <input type="number" bind:value={duration} /></label>
<label><input type="checkbox" bind:checked={showBoxes} /> Show Boxes</label>
	
<div class="boxes">
	{#if showBoxes}
		<div class="box cyan" transition:slide={{duration}}></div>
		<div class="box magenta" in:fade={{duration}}></div>
		<div class="box yellow" out:fly={{y: 200, duration}}></div>
		<div class="box key" transition:scale={{duration}}></div>
	{/if}
</div>

Transitions.svelte

Animations & Transitions

<script>
	import { flip } from "svelte/animate";

	let fruits = $state([
		"banana",
		"apple",
		"strawberry"
	])

	const remove = (fruit) => (fruits = fruits.filter(f => f !== fruit));
</script>

<ul>
	{#each fruits as fruit (fruit)}
		<li>{fruit} <button onclick={() => remove(fruit)}>remove</button></li>
	{/each}
</ul>

Animations.svelte

Animations & Transitions

Live Coding

# https://github.com/nilsroehrig/taskpal

git checkout p1_exercise -b <new-branch-name>

Part I Exercise

Part II

Going Full-Stack

Cloudflare Workers®

Server by Adrien Coquet from Noun Project (CC BY 3.0)

world by Adrien Coquet from Noun Project (CC BY 3.0)

script by Adrien Coquet from Noun Project (CC BY 3.0)

User by Adrien Coquet from Noun Project (CC BY 3.0)

GitHub

Gitlab

Wrangler CLI

deploy to

Edge Location

Nearby User

Cloudflare Workers®

Cloudflare D1

world by Adrien Coquet from Noun Project (CC BY 3.0)

Data by Adrien Coquet from Noun Project (CC BY 3.0)

script by Adrien Coquet from Noun Project (CC BY 3.0)

Server by Adrien Coquet from Noun Project (CC BY 3.0)

replicate

replicate

replicate

replicate

replicate

replicate

Edge Location

Regional Data Centre

Cloudflare D1

Prisma ORM

Prisma ORM

Object Relational Mapping:

Prisma maps relational databases to type-safe TypeScript or JavaScript objects.


Useful Features:

Prisma comes with a Query Builder and a Migration system, as well as a GUI for managing the database manually.

 

Compatibility:
Prisma is compatible with most popular relational database system, including PostgreSQL, MySQL and SQLite. It also supports MongoDB and Cloudflare D1.

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

generator client {
  provider        = "prisma-client-js"
  previewFeatures = ["driverAdapters"]
}

model User {
  id           String    @id @default(cuid())
  username     String    @unique
  passwordHash String
}

schema.prisma

Prisma ORM

export function getUserByName(username: string) {
  return prisma.user.findFirst({
    where: {
      username
    }
  });
}

export function createUser(userInput: UserInput) {
  return prisma.user.create({
    data: {
      username: userInput.username,
      passwordHash: await hash(userInput.password)
    }
  });
}

user-query.ts

Prisma ORM

What is SvelteKit?

SvelteKit is a framework for rapidly developing robust, performant web applications using Svelte. If you're coming from React, SvelteKit is similar to Next. If you're coming from Vue, SvelteKit is similar to Nuxt.

  • Components
  • Rendering
  • Interactivity
  • ‘Browser Stuff‘
  • Routing
  • Data Loading
  • Form Processing
  • ‘Server Stuff‘

What is SvelteKit?

SvelteKit Routing

SvelteKit Routing

File-Based Routing:

The path of a route is defined by it's respective location in the file system. The folder src/routes/login, for example, points to https://your.host/login.

 

By default, the src/routes folder points to the root path of the site.

 

Dynamic Path Parameters:

Path parameters can be used by putting their names in brackets. In consequence, src/routes/posts/[id] points to a post with the id passed in place of [id].

SvelteKit Routing

Folder by Adrien Coquet from Noun Project (CC BY 3.0)

page by Adrien Coquet from Noun Project (CC BY 3.0)

+page.svelte

The pages markup, styles and behavior. It may receive a data or a form prop.

Loads data on the client and on the server. Returns data available within the data prop.

+page.js

Loads data and processes forms on the server. Returns data available within data and form.

+page.server.js

SvelteKit Routing

export function load () {
  // any data loading logic
  // that is server-only
 
  return {
    // any loaded data
  }
}

export const actions = {
  default() {
    // default form action
  },
  
  namedAction() {
    // named form action
  }
}

+page.server.js

export function load () {
  // any data loading logic
  // that can run on client
  // and server
 
  return {
    // any loaded data
  }
}

+page.js

<script>  
  let {    
    // data loaded in 
  	// load functions
    data,
    
    // data returned from 
  	// form actions
    form
  } = $props(); 
  
</script>

<h1>Route Headline</h1>

+page.svelte

SvelteKit Routing

API routes:

In addition to regular pages that render HTML, SvelteKit also allows for API routes defined in +server.js files.

 

HTTP Method Functions:

The file path still dictates the URL path, but +server.js files export functions named after HTTP methods: e.g. GET, POST, PUT, DELETE. They take RequestEvents and return standard Response objects.

export function GET() {
	const randomQuote = getRandomQuote();
    return new Response(randomQuote);
}

export async function POST({request}) {
  const { quote } = await request.json();
  const addedQuote = addRandomQuoteToPool(quote);
  return Response(JSON.stringify(addedQuote));
}

/api/random-quote/+server.js

SvelteKit Routing

SvelteKit Data Loading

SvelteKit Data Loading

Load Functions:

In SvelteKit, data is loaded via load functions. These are responsible to gather all data a page needs, in order to be rendered properly.

 

Universal Load Functions:

Defined in +page.js, universal load functions can run on the server and the client. They should not handle any sensitive or private data.

 

Server Load Functions:

In contrast, server load functions only run on the server, making them ideal for e.g. database access or usage of sensitive information. They are defined in +page.server.js.

SvelteKit Data Loading

src/resource/[id]/+page.server.js

export function load ({ params, locals }) {  
  const resource = db.getResourceById(params.id);
  
  if(!resource) {
    error(404);
  }
  
  if(!canAccessResource(resource, locals.user)) {
    error(403);
  }
  
  return {
    resource
  }
}

SvelteKit Form Actions

SvelteKit Form Actions

HTML Form Processing:

Form actions in SvelteKit serve to process standard HTML forms. They can work without client-side JavaScript but can be progressively enhanced.

 

Default vs. Named Actions:

Form actions are defined in +page.server.js files alongside load functions. It is possible to either have one single action called default, or an arbitrary number of named actions, not both at the same time.

 

The form Property:

On a page with a form action, an optional form property is available. It is populated by returning data from an action.

SvelteKit Form Actions

<script>
  import { enhance } from '$app/forms';
  
  const { form } = $props();
</script>

<form method="POST" use:enhance>
  {#if !form.success}
  	An error occurred!
  {/if}
  <input type="email" name="email">
  <input type="password" name="password">
  <button>Submit</button>
</form>

+page.svelte

export const actions = {
  default({ request }) {
    const formData = await request.formData();
    const success = validateCredentials(
      formData.get("email"),
      formData.get("password")
    )
    
    if(!success) {
      return fail(400, { success: false });
    }
    
    return { success: true };
  }
}

+page.server.js

ZOD

Zod

Runtime Validation:

To validate data at runtime, Zod can be used. It is a 3rd-party library an not part of SvelteKit.

 

Zod Schemas:
At the heart of Zod are schemas. These are type parsers defined programmatically by the application. They allow to parse and validate arbitrary data, using the Schema.parse() method.

 

Type Safety:

When using TypeScript, proper types can be easily inferred from Zod Schemas.

Zod

import { z } from 'zod';

export const UserSchema = z.object({
  name: z.string(),
  age: z.number().optional(),
  dateOfBirth: z.coerce.date()
});

export type User = z.infer<typeof UserSchema>;

// passes
let user = UserSchema.parse({name: "Karl", dateOfBirth: new Date()});

// throws
let user = UserSchema.parse({name: 3537, dateOfBirth: new Date() });

// throws
let user = UserSchema.parse({name: "Marlene", dateOfBirth: "not-a-date" });

User.js

zod-form-data

zod-form-data

Easy FormData Validation:

When using Zod with FormData objects, some transformation is needed. The library zod-form-data helps with this.

 

Good Pairing with Form Actions:

Since SvelteKit form actions are using FormData, they pair exceptionally well with zod-form-data.

 

Parse FormData with Zod Schemas:

zod-form-data allows using either built-in validators or passing a Zod Schema which is then applied to FormData.

import { zfd } from 'zod-form-data';

const Credentials = zfd.formData({
  email: zfd.text(),
  password: zfd.text()
});

export const actions = {
  async login({request}) {
    const credentials = Credentials.parse(
      await request.formData()
    );
    
    const user = await loginWithCredentials(credentials);
    
    return {
      user
    }
  }
}

+page.server.ts

zod-form-data

Live Coding

# https://github.com/nilsroehrig/taskpal

git checkout p2_exercise -b <new-branch-name>

Part II Exercise

Part III

Authenticate & Authorize

Session-Based Authentication

alt

Client

Server

Database

sends credentials

validates
credentials

[credentials valid]

create

session

stores session

returns session cookie

[credentials invalid]

returns auth error

requests data with session cookie

queries session data

return session data

validates
session

alt

[session is present]

returns requested data

returns auth error

[session is missing]

SvelteKit Hooks

SvelteKit Hooks

Hooks in SvelteKit are powerful functions that let you control different behaviors across your entire app.

 

Types of Hooks:

SvelteKit provides three types of hooks: server hooks, shared hooks, and universal hooks.

 

Server Hooks:

These run exclusively on the server. For example, the handle() hook runs whenever a request hits the server, making it perfect for tasks like handling authentication or integrating middlewares.

SvelteKit Hooks

Shared Hooks:

Shared hooks, like handleError(), can be defined for both the server and the client, although in separate files. This makes them ideal for handling unexpected errors in a centralized way, regardless of where they occur.

 

Universal Hooks:

These hooks function seamlessly across both server and client environments, giving you flexibility in how you manage application behavior.

SvelteKit Hooks

export function handleError({error}) {
  // do error handling stuff
  
  return {
    message: "Error message",
    //... other data
  }
}

hooks.client.js

import { redirect } from '@sveltejs/kit';

export function handle({event, resolve}) {
  if(event.url.pathname.startsWith("/api")
    && !userIsAuthenticated()) {
    redirect(302, "/login")
  }
  return resolve(event)
}

hooks.server.js

SvelteKit Layouts

SvelteKit Layouts

File-Based Layouts in SvelteKit:

Just like routes, layouts in SvelteKit are organized based on the file structure.

 

Default Behavior:

By default, a layout applies to the route in the same folder and all its nested routes.

 

For instance,

src/routes/+layout.svelte 

automatically applies to:

  • src/routes/+page.svelte
  • src/routes/login/+page.svelte, and so on.

SvelteKit Layouts

Inheritance of Layouts:

Layouts can also inherit from other layouts higher up in the folder structure.

 

For example, src/routes/+layout.svelte wraps around src/routes/login/+layout.svelte, making it easy to manage nested layouts.

 

Layout Groups:

When some specific nesting of layouts is required but a change of routs is undesired, pages can be grouped by folders in parantheses: For example

src/routes/(authenticated)/ could be used to group authenticated pages without altering the route hierarchy.

SvelteKit Layouts

Your +layout.svelte files can also load data, via +layout.js or +layout.server.js.

SvelteKit Layouts

<script>
  let { children } = $props();
</script>

<div class="wrapper">
  <header>Headline</header>
  <main>
    {@render children?.()}
  </main>
  <footer>Footer</footer>
</div>

+layout.svelte

Live Coding

# https://github.com/nilsroehrig/taskpal

git checkout p3_exercise -b <new-branch-name>

Part III Exercise