Svelte journey | SvelteKit: Server-side, SvelteKit utility stores, Error handling, Hooks
Tags: javascript, frontend, tutorial, svelte
Published: 2024-01-11 17:00:00

Welcome,

In the previous part, we started to [explore SvelteKit](https://dev.to/chillyhill/sveltekit-introduction-routing-data-loading-shared-modules-13en), 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](https://en.wikipedia.org/wiki/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:

```jsx
// 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](https://developer.mozilla.org/en-US/docs/Web/API/Request) object, and `await request.formData()` returns a [FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData) instance:

```jsx
// 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.

```jsx
// src/routes/+page.svelte
<form method="POST" action="?/create">
	<label>
		add a todo:
		<input
			name="description"
			autocomplete="off"
		/>
	</label>
</form>
```

```jsx
// 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](https://learn.svelte.dev/tutorial/form-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 through `export let form`.

```jsx
// 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](https://kit.svelte.dev/docs/form-actions#anatomy-of-an-action-redirects) 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.

```jsx
<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:

```jsx
<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](https://kit.svelte.dev/docs/form-actions#progressive-enhancement-customising-use-enhance). An extract from the documentation:

```jsx
<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` and `DELETE`. Function name in `+server.js` should be the same as the method name;
- Request handlers must return a [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response/Response) object;
- From the client (`+page.js`), you need to make requests as from usual JS code;

```jsx
// 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](https://dev.to/chillyhill/svelte-journey-bindings-lifecycles-stores-4fe3), 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](https://developer.mozilla.org/en-US/docs/Web/API/URL) of the current page;
- `params` — the current page's [parameters](https://learn.svelte.dev/tutorial/params);
- `route` — an object with an `id` 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 all `load` functions;
- `form` — the data returned from a [form action](https://learn.svelte.dev/tutorial/the-form-element).

```jsx
// 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](https://kit.svelte.dev/docs/types#public-types-navigation)

The `navigating` store represents the current navigation:

- `from` and `to` — objects with `params`, `route` and `url` 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 a `goto(...)` 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 a `willUnload` navigation, the promise will never resolve.

```jsx
// 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](https://kit.svelte.dev/docs/configuration#files).

### `handle` [hook](https://kit.svelte.dev/docs/hooks#server-hooks-handle)

- server hook only;
- runs every time server receives a [request](https://kit.svelte.dev/docs/web-standards#fetch-apis-request) and modifies the [response](https://kit.svelte.dev/docs/web-standards#fetch-apis-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](https://kit.svelte.dev/docs/modules#sveltejs-kit-hooks-sequence) utility function;

```jsx
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](https://learn.svelte.dev/tutorial/get-handlers) in `+server.js` files, [form actions](https://learn.svelte.dev/tutorial/the-form-element) in `+page.server.js` files, and `load` functions in `+page.server.js` and `+layout.server.js`.

It has inside:

- `cookies` — the [cookies API](https://learn.svelte.dev/tutorial/cookies);
- `fetch` — the [fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) with extra perks that are described in `handleFetch` 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 subsequent `load` functions;
- `params` — the route parameters;
- `request` — the [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object;
- `route` — an object with an `id` property representing the route that was matched;
- `setHeaders(...)`;
- `url` — a [URL](https://developer.mozilla.org/en-US/docs/Web/API/URL) object representing the current request.

### `event.fetch` extra functionality

- make credentialed requests on the server, as it inherits the `cookie` and `authorization` 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](https://kit.svelte.dev/docs/hooks#server-hooks-handlefetch)

The hook that allows modifying event’s `fetch` behaviour.

- server hook only;

```jsx
export async function handleFetch({ event, request, fetch }) {
	const url = new URL(request.url);
	return url.pathname === '/foo' ? fetch('/bar') : fetch(request);
}
```

### `handleError` [hook](https://kit.svelte.dev/docs/hooks#shared-hooks-handleerror)

Interceptor for **unexpected** errors.:

- it is shared hook — can be added to `src/hooks.server.js` *and* `src/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.

```jsx
export function handleError({ event, error }) {
	return {
		message: 'Unknown error happened!',
		code: generateCode(error)
	};
}
```

### `reroute` [hook](https://kit.svelte.dev/docs/hooks#universal-hooks-reroute)

- 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 with `reroute`:

```jsx
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

- [SvelteKit Tutorial](https://learn.svelte.dev/tutorial)
- [SvelteKit Docs | Form actions](https://kit.svelte.dev/docs/form-actions)
- [SvelteKit Docs | use:enhance customizations](https://kit.svelte.dev/docs/form-actions#progressive-enhancement-customising-use-enhance)
- [SvelteKit Docs | Hooks](https://kit.svelte.dev/docs/hooks)
- [SvelteKit Tutorial | The RequestEvent object](https://learn.svelte.dev/tutorial/event)