SvelteKit and the "client pattern"
NOTE: This article was written with a beta version of SvelteKit in mind. Most of it’s content is probably outdated and useless by now. Tread carefully!
I fell in love with Svelte a long time ago. Recently, I have fallen in love all over again when SvelteKit hit beta. It’s an excellent example of a web framework that delivers heavily on developer happiness and productivity.
The way SvelteKit marries server-side rendering and API requests is incredibly well done. The framework addresses the differences in server and client environments by injecting a fetch
function into the load
callback of page- and layout components. This very flexible approach makes sure that there is always a valid fetch
whether it is on the server (e.g. node-fetch) or on the client (e.g. window.fetch). This also allows different platforms to specify their own fetch
which is important on platforms like Cloudflare workers, Vercel Edge Functions, or Deno deploy.
The problem
The injection of the fetch
function solves the problem for code that runs inside the load
function but not for code that runs inside other modules. This restriction usually works fine because there is a common workaround for most cases:
<script>
import { browser } from '$app/env';
let promise = Promise.reject();
$: if (browser) {
promise = fetch(/* snip */);
}
</script>
{#await promise then value}
<!-- snip -->
{/await}
We need to check for the browser
variable before fetching. In this context, the server doesn’t have a proper fetch
and we are not inside of a load
callback. This seems like an inelegant hack though.
A real solution
One of the best solutions I found to this problem is the use of a pattern I call the “client pattern”. To better understand this, let’s take a look at the “client” part first.
The client
A good and simple example of an API client would be a factory function that returns an object that wraps our API calls into functions:
// lib/api.ts
export function createClient() {
return {
async getPosts() {
return await fetch(/* snip */);
},
async getPost(id: number) {
return await fetch(/* snip */);
},
};
}
The problem of this version is that it depends on a global fetch
which makes it really inflexible. To circumvent this, we use dependency injection and add fetch
as a parameter to the factory function:
// lib/api.ts
export function createClient(fetch: typeof global.fetch) {
/* snip */
}
This way, we have to pass a working instance of fetch
to the factory function in order to get our API wrapper.
Constructing the client
Now we construct the client inside the load
callback of our __layout.svelte
and inject the fetch
given by the load
function into our factory:
<!-- __layout.svelte -->
<script lang="ts" context="module">
import type { Load } from '@sveltejs/kit';
import { createClient } from '$lib/api';
export const load: Load = async ({ fetch, session }) => {
const api = createClient(fetch);
return {
stuff: { api },
props: { api }
};
};
</script>
Context matters
To make the api
object available to the whole app and all components, it is easiest to construct an unambiguous wrapper around the Svelte context API:
// api.ts
export function createClient(fetch: typeof global.fetch) {
/* snip */
}
const contextKey = Symbol("API");
export type Client = ReturnType<typeof createClient>;
export const getClient = (): Client => getContext(contextKey);
export const setClient = (client: Client): void => {
setContext(contextKey, client);
};
Afterwards, we need to put a call to setClient
into our root __layout.svelte
:
<!-- __layout.svelte -->
<!-- snip -->
<script lang="ts">
import type { Client } from '$lib/api';
import { setClient } from '$lib/api';
export let api: Client;
setClient(api);
</script>
You obviously don’t have to set the client in the root __layout.svelte
. You can choose any layout file where the related subtree needs access to the API client.
Finally, we can now use the getClient
function to obtain a copy of our client that works on every platform:
<script lang="ts">
import { getClient } from '$lib/api';
const api = getClient();
$: api.fetchSomeResource(anotherDependentVariable);
</script>
This is not something I invented. Many GraphQL frameworks, such as Apollo and URQL, already use this pattern. Going through the source code of these frameworks allowed me to discover this pattern in the first place. I call it the “Client pattern”. I don’t claim this to be the best name, but for me, it sums up the technique pretty nicely.