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
Full-Stack Development with Svelte & SvelteKit (CF Edition)
By Nils Röhrig
Full-Stack Development with Svelte & SvelteKit (CF Edition)
- 221