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.