I’ve been working on a hobby project in Svelte for the past few weeks and have been struggling to find resources on doing proper authentication when using a backend alongside SvelteKit. Here’s the setup:

  • Svelte Frontend – Svelte is extremely refreshing to use and makes me genuinely enjoy writing frontend code. The ecosystem is growing, and I think it’s a serious contender to React, at least for hobby projects.
  • SvelteKit – Svelte is significantly stunted without SvelteKit. A lot of really nice Svelte features are gated behind SvelteKit, and at the very least, I’d like to use SvelteKit for things like SSR, routing, and form actions.
  • A backend of your choice – SvelteKit is nice, but I’m not a fan of the developer experience when it comes to things like API routes and statefulness (which you sometimes want!). I’d prefer to use a proper backend framework, whether that be Flask, NestJS, or something else entirely, to do the heavy lifting.

TL;DR: handleFetch and a custom API layer.

Enter: +page.ts

SvelteKit is built around every page being hydrated and server-side rendered. This is by far one of the nicest things that SvelteKit does. My websites feel snappy and incredibly fast because SvelteKit is optimizing, depending on how a page is loaded, whether the data is queried and retrieved on the client-side or the server-side. You write data retrieval and processing code once in a +page.ts (or +page.js) file, and, depending on some simple heuristics, SvelteKit will run it in the user’s browser or on your server. In theory, you don’t need to care where your code is run. In practice, this becomes a pain in the ass when that matters.

This becomes a headache when you are trying to piggyback information, such as a session token for authentication, on to every fetch request to your actual backend. When a +page.ts is run on the server-side, the backend is stateless and the only way to pass information about the current session for authentication purposes is through a cookie. However, that cookie will not be automatically forwarded to your API if your API is running somewhere else on a non-child domain. When the +page.ts is run on the client-side, cookies are not forwarded, and you must piggyback the session token manually. Svelte provides no convenient way to solve both of these problems in one-go, and from my searching, this is a problem that people encounter all the time.

Solution

Let’s say you have a backend with the simplest possible authentication guard that runs on every API call:

def is_authenticated(request):
    token = request.headers.authentication
    return validate(token)

We want to forward the session token to the authentication header in both the client fetches and the server fetches. Somewhat conveniently, SvelteKit provides a method to do the latter using handleFetch. We will create a cookie for the session token under __session, and then we can put the following code in our hooks.server.ts:

export const handleFetch = async ({ event, request, fetch }) => {
    const token = event.cookies.get('__session');
    if (isRequestToAPI(request) && token) {
        request.headers.set('authorization', token);
    }
    return fetch(request);
};

Great, now any server-side requests will successfully piggyback the session token under the authorization header. handleFetch does not, however, run on client-side fetches. In my projects, to solve this, I create a wrapper around fetch. Let me demonstrate:

// lib/util/fetch.ts

const baseApiRoute: string = "..."

// ensure we don't use window.fetch
export type SvelteFetch = (
    input: RequestInfo | URL,
    init?: RequestInit | undefined
) => Promise<Response>;

export interface ApiOptions {
    fetch: SvelteFetch,
    init?: RequestInit;
}

// for type safety
export const fetchAPI = async <T>(apiRoute: string, opts: ApiOptions): Promise<T> => {
    const response = await fetchAPIHelper(apiRoute, opts);

    if (!response.ok) {
        throw new Error("...");
    }
  
    return response.json();
  };

const fetchAPIHelper = async (apiRoute: string, opts: ApiOptions): Promise<Response> => {
    const fetch = opts.fetch;

    const session = get(sessionStore);
    if(session) {
        const token = session.getToken();
        opts.init = {
            ...opts.init,
            headers: {
                ...opts.init?.headers,
                'authorization': token,
            }
        }
    }

    return fetch(`${baseApiRoute}${apiRoute}`, {
        ...opts?.init,
        credentials: 'include',
    });
}

This code will add the session token to the authorization header. It also has the benefit of making fetch type safe. For example, we can now do something like this in a +page.ts:

export const load: PageLoad = async ({fetch}) => {
	return {
        users: await fetchAPI<UserDTO>("/users", {fetch}),
	};
};

We must pass in the provided fetch from the PageLoad parameters, otherwise Svelte will (rightfully) complain that you are using window.fetch. This doesn’t do anything on the server-side, as the sessionStore won’t be initialized (server-side stores are global and should be avoided).

In retrospect, this is fairly simple, but I’ve seen a lot of people complain or ask about how to handle authentication in this manner, and haven’t seen an end-to-end example like the one above. I hope this was useful, and I’d be really curious to see how others approach this problem, or if anyone has a technique to avoid it entirely.