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
actionattribute 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
POSTrequests.
// 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.jsby 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
formprop; - Invalidates all data on a successful response, causing
loadfunctions 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,PATCHandDELETE. Function name in+server.jsshould 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 anidproperty 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 allloadfunctions; -
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:
-
fromandto— objects withparams,routeandurlproperties; -
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 awillUnloadnavigation, 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
loadfunctions; - form actions;
- API routes;
-
handlehook (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
handlehooks 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.jsand+page.sveltefiles 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 inhandleFetchhook (and can be modified by this hook); -
getClientAddress()— a function to get the client's IP address; -
isDataRequest—trueif the browser is requesting data for a page during client-side navigation,falseif 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 subsequentloadfunctions; -
params— the route parameters; -
request— the Request object; -
route— an object with anidproperty 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
cookieandauthorizationheaders from the incoming request; - relative requests on the server (without providing origin);
- internal requests (e.g. for
+server.jsroutes) 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.jsandsrc/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.sveltepage →/en/aboutor/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!
Top comments (0)