Svelte journey | SvelteKit: Server-side, SvelteKit utility stores, Error handling, Hooks
Denys Sych
Posted on January 11, 2024
Welcome,
In the previous part, we started to explore SvelteKit, and today we’ll continue on that. The previous chapter touched on routing, data loading, and the concept of shared modules in SvelteKit.
This article will focus on server-side interaction, specifically API Routes, which are included in SvelteKit. We will also cover form submitting actions with progressive enhancement, specific stores built into the kit, and error handling.
Lastly, we will discuss hooks that help customize default SvelteKit mechanisms for data loading and error handling.
Forms — server-side interaction without client-side JS execution
A form is a standard tool for sending data back to the server:
// src/routes/+page.svelte
<form method="POST">
<label>
add a todo:
<input
name="description"
autocomplete="off"
/>
</label>
</form>
To handle this on the server-side, actions should be used inside +page.server.js. The request
is a standard Request object, and await request.formData()
returns a FormData instance:
// src/routes/+page.server.js
export function load() {
// ...
}
export const actions = {
// Unnamed form = default action
default: async ({ cookies, request }) => {
const data = await request.formData();
db.createTodo(cookies.get('userid'), data.get('description'));
}
};
- No JS fetching here → the application operates even with disabled JS.
Named form actions
- Default actions cannot coexist with named actions;
- The
action
attribute can be any URL. If the action was defined on another page, you might have something like/todos?/create
; - When the action is on this page, we can omit the pathname altogether and use just
?
; - After an action runs, the page will be re-rendered, and the load action will run after the action completes;
- Actions are always about
POST
requests.
// src/routes/+page.svelte
<form method="POST" action="?/create">
<label>
add a todo:
<input
name="description"
autocomplete="off"
/>
</label>
</form>
// src/routes/+page.server.js
export const actions = {
// action="?/create"
create: async ({ cookies, request }) => { // ... }
};
Markup-based validation
The most basic one, like required
in <input name="title" required />
.
Server-side validation
The common flow is as follows:
- Throw
new Error()
from a service (e.g.,src/lib/server/database.js
); - Catch this error in
+page.server.js
(e.g.,src/routes/+page.server.js
); - Show the error in
+page.js
by accessing the form's metadata throughexport let form
.
// src/lib/server/database.js
export function createTodo(userid, description) {
if (description === '') {
throw new Error('todo must have a description'); // 1. Throw an error
}
}
// src/routes/+page.server.js
import { fail } from '@sveltejs/kit';
import * as db from '$lib/server/database.js';
export const actions = {
create: async ({ cookies, request }) => {
const data = await request.formData();
try {
db.createTodo(cookies.get('userid'), data.get('description'));
} catch (error) {
return fail(422, { // 2. Catch and return expected failure
description: data.get('description'),
error: error.message
});
}
}
};
// src/routes/+page.svelte
<script>
export let data;
export let form;
</script>
<div class="centered">
<h1>todos</h1>
{#if form?.error} // 3. Handle an error
<p class="error">{form.error}</p>
{/if}
<form method="POST" action="?/create">
<label>
add a todo:
<input
name="description"
value={form?.description ?? ''}
autocomplete="off"
required
/>
</label>
</form>
</div>
Instead of using fail
, you can also perform a redirect or just return some data — it will be accessible via form
.
Progressive enhancement use:enhance
— SPA-like API interaction
When JS is enabled in a client's environment, Svelte progressively enhances the functionality without full-page reloads. The same happens with <a/>
anchor navigations.
- Updates the
form
prop; - Invalidates all data on a successful response, causing
load
functions to re-run; - Navigates to the new page on a redirect response;
- Renders the nearest error page if an error occurs.
<script>
import { enhance } from '$app/forms';
</script>
<form method="POST" action="?/create" use:enhance>
<!-- Form content -->
</form>
Customised enhancing for optimistic updates, pending states, etc.
For example, there is usually some delay in server-to-client communication, which requires loading states.
Pass a callback to use:enhance
to handle such cases:
<script>
import { fly, slide } from 'svelte/transition';
import { enhance } from '$app/forms';
export let data;
export let form;
let creating = false;
let deleting = [];
</script>
<form
method="POST"
action="?/create"
use:enhance={() => { // <--- custom handling happens here
creating = true;
return async ({ update }) => {
await update();
creating = false;
};
}}
>
{#if creating}
<span class="saving">saving...</span>
{/if}
<label>
add a todo:
<input
disabled={creating}
name="description"
value={form?.description ?? ''}
/>
</label>
<!-- Additional form elements -->
</form>
More details on use:enhance
can be found here. An extract from the documentation:
<form
method="POST"
use:enhance={({ formElement, formData, action, cancel, submitter }) => {
// `formElement` is this `<form>` element
// `formData` is its `FormData` object that's about to be submitted
// `action` is the URL to which the form is posted
// Calling `cancel()` will prevent the submission
// `submitter` is the `HTMLElement` that caused the form to be submitted
return async ({ result, update }) => {
// `result` is an `ActionResult` object
// `update` is a function that triggers the default logic that would be triggered if this callback wasn't set
};
}}
>
<!-- Form content -->
</form>
API Routes
SvelteKit allows you to define API routes that handle server-side functionality. These routes can be used to fetch data from databases, interact with external APIs, file system etc. or perform any other server-side operations. This makes it seamless to integrate your frontend with backend functionality.
- Supports all default HTTP methods:
GET
,PUT
,POST
,PATCH
andDELETE
. Function name in+server.js
should be the same as the method name; - Request handlers must return a Response object;
- From the client (
+page.js
), you need to make requests as from usual JS code;
// src/routes/todo/+server.js
import { json } from '@sveltejs/kit';
import * as database from '$lib/server/database.js';
export async function GET() {
return json(await database.getTodos());
}
export async function POST({ request, cookies }) {
const { description } = await request.json();
const userid = cookies.get('userid');
const { id } = await database.createTodo({ userid, description });
return json({ id }, { status: 201 });
}
// src/routes/todo/+page.svelte
<script>
export let data;
</script>
<!-- Additional page content -->
<input
type="text"
autocomplete="off"
on:keydown={async (e) => {
if (e.key !== 'Enter') return;
const input = e.currentTarget;
const description = e.currentTarget.value;
const response = await fetch('/todo', {
method: 'POST',
body: JSON.stringify({ description }),
headers: {
'Content-Type': 'application/json'
}
});
const { id } = await response.json();
data.todos = [...data.todos, {
id,
description
}];
input.value = '';
}}
/>
Stores
We’ve met stores before, now we are going to see predefined, readonly stores in SvelteKit. There are three of them: page
, navigating
, and updated
. They are available via $app/stores
module.
Page
Provides information about the current page:
-
url
— the URL of the current page; -
params
— the current page's parameters; -
route
— an object with anid
property representing the current route; -
status
— the HTTP status code of the current page; -
error
— the error object of the current page, if any; -
data
— the data for the current page, combining the return values of allload
functions; -
form
— the data returned from a form action.
// src/routes/+layout.svelte
<script>
import { page } from '$app/stores';
</script>
<nav>
<a href="/" class:active={$page.url.pathname === '/'}>
home
</a>
<a href="/about" class:active={$page.url.pathname === '/about'}>
about
</a>
</nav>
<slot />
Navigating
The navigating
store represents the current navigation:
-
from
andto
— objects withparams
,route
andurl
properties; -
type
— the type of navigation:-
form
: The user submitted a<form>
; -
leave
: The app is being left either because the tab is being closed or a navigation to a different document is occurring; -
link
: Navigation was triggered by a link click; -
goto
: Navigation was triggered by agoto(...)
call or a redirect; -
popstate
: Navigation was triggered by back/forward navigation;
-
-
willUnload
— Whether or not the navigation will result in the page being unloaded (i.e. not a client-side navigation); -
delta
? — in case of a history back/forward navigation, the number of steps to go back/forward; -
complete
— promise that resolves once the navigation is complete, and rejects if the navigation fails or is aborted. In the case of awillUnload
navigation, the promise will never resolve.
// src/routes/+layout.svelte
<script>
import { page, navigating } from '$app/stores';
</script>
<!-- Additional layout content -->
{#if $navigating}
navigating to {$navigating.to.url.pathname}
{/if}
Updated
The updated
store contains true
or false
depending on whether a new version of the app has been deployed since the page was first opened. For this to work, your svelte.config.js
must specify kit.version.pollInterval
.
Errors and Redirects
We can either throw error
or redirect
(import { redirect, error } from '@sveltejs/kit'
, e.g. throw redirect(307, '/maintenance')
. redirect
can be called:
- inside
load
functions; - form actions;
- API routes;
-
handle
hook (described in the next section).
The most common status codes:
-
303
— form actions, after a successful submission; -
307
— temporary redirects; -
308
— permanent redirects.
Hooks
A way to intercept and override the framework's default behaviour. Can be also named as interceptors at some point.
- there are two types of hooks: server (should be in src/hooks.server.js) and client (src/hooks.server.js) ones. They also can be shared (environment-specific) and universal (run both on client and server);
- mentioned file paths are by default, it can be customized via config.kit.files.hooks.
handle
hook
- server hook only;
- runs every time server receives a request and modifies the response (runtime and pre-rendering);
- requests to static assets and already pre-rendered pages — not handled by SvelteKit;
- multiple
handle
hooks can be added via sequence utility function;
export async function handle({ event, resolve }) {
return await resolve(event, optionalParams);
}
-
resolve
— SvelteKit matches the incoming request URL to a route of your app, imports the relevant code (+page.server.js
and+page.svelte
files and so on), loads the data needed by the route, and generates the response;
event
object in server hooks
event
object passed into handle
is the same object — an instance of a [RequestEvent](<https://kit.svelte.dev/docs/types#public-types-requestevent>)
— that is passed into API routes in +server.js
files, form actions in +page.server.js
files, and load
functions in +page.server.js
and +layout.server.js
.
It has inside:
-
cookies
— the cookies API; -
fetch
— the fetch API with extra perks that are described inhandleFetch
hook (and can be modified by this hook); -
getClientAddress()
— a function to get the client's IP address; -
isDataRequest
—true
if the browser is requesting data for a page during client-side navigation,false
if a page/route is being requested directly; -
locals
— a place to put arbitrary data. Useful pattern: add some data here so it can be accessed in subsequentload
functions; -
params
— the route parameters; -
request
— the Request object; -
route
— an object with anid
property representing the route that was matched; -
setHeaders(...)
; -
url
— a URL object representing the current request.
event.fetch
extra functionality
- make credentialed requests on the server, as it inherits the
cookie
andauthorization
headers from the incoming request; - relative requests on the server (without providing origin);
- internal requests (e.g. for
+server.js
routes) run internally, without actual HTTP call.
handleFetch
hook
The hook that allows modifying event’s fetch
behaviour.
- server hook only;
export async function handleFetch({ event, request, fetch }) {
const url = new URL(request.url);
return url.pathname === '/foo' ? fetch('/bar') : fetch(request);
}
handleError
hook
Interceptor for unexpected errors.:
- it is shared hook — can be added to
src/hooks.server.js
andsrc/hooks.client.js
; - can be used to log an error somewhere or some other side effect;
- the default message can be either ‘Internal error’ or ‘Not found’. This hook allows customizing the message and the error object itself;
- it never throws an error.
export function handleError({ event, error }) {
return {
message: 'Unknown error happened!',
code: generateCode(error)
};
}
reroute
hook
- universal hook, can be added to
src/hooks.js
; - allows you to change how URLs are translated into routes;
-
src/routes/[[lang]]/about/+page.svelte
page →/en/about
or/de/ueber-uns
,/fr/a-propos
. Achieavable withreroute
:
const translated = {
'/en/about': '/en/about',
'/de/ueber-uns': '/de/about',
'/fr/a-propos': '/fr/about',
};
export function reroute({ url }) {
if (url.pathname in translated) {
return translated[url.pathname];
}
}
That's all for today, and it's a massive chunk – well done! Just a couple more steps, and we're ready to dive into our own projects with confidence, armed with new knowledge.
Take care, go Svelte!
Resources
Posted on January 11, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
January 11, 2024