We are live on DevHunt: tool of the week contest

check it out

Sveltekit Protected Routes in SPA mode

Jul 26, 2024 Authentication Written by Vivek Shukla

[Nov 28, 2024] Update: Code and approach has been updated to suit Svelte 5.


I have been working on one of my project where I already had a separate backend server (not Sveltekit), so I thought it’s only logical that I should just use SPA mode on my frontend (Sveltekit) to avoid having another server.

Most common approach I came across when implementing protected routes is to use hooks with cookies or session on the server.

But I want to avoid Sveltekit’s server because of the following reasons:

  • It will most probably bottleneck my blazing fast backend server
  • Serverless platforms like Vercel, Netlify charges for function invocation, charges can skyrocket really fast
  • I don’t want to self-host my own Node server either

So after some tinkering and research I came to this solution, please only follow through if below conditions apply to you

  • You already have another backend server
  • You are just integrating REST APIs into your Sveltekit frontend
  • You are fine with storing your auth credential in localStorage or you are using cookie (set by your backend) based authentication

This code demonstrates localstorage method for storing auth token.

Let’s first understand the flow, starting with Login:

  1. User authenticates himself
  2. You get a auth token in response
  3. Store the token in localStorage
  4. Take user to protected route and call whatever api you need to
  5. If any of the API you are calling responds with 401 then log user out

Basically we will have two kinds of route: public and private. Any route inside private will only be accessible for logged in user.

You can find the repository with all the code here.

Routes directory structure:

routes/
├── (private)
│   ├── +layout.svelte
│   ├── +layout.ts
│   └── +page.svelte
└── (public)
│   ├── +layout.ts
    └── login
        └── +page.svelte

Disable SSR

Since we want to use SPA mode, we will disable Sveltekit’s ssr (server side rendering). Set ssr to false in (public)/+layout.ts and (private)/+layout.ts by adding below line:

export const ssr = false;

Auth

We are gonna create a new file lib/auth.svelte.ts to store our helper auth function.

lib/auth.svelte.ts

function authToken() {
	return {
		get token(): string | null {
			return localStorage.getItem('token') || null;
		},
		set token(value: string) {
			localStorage.setItem('token', value);
		},
		clear() {
			localStorage.removeItem('token');
			window.location.href = '/login';
		}
	};
}

export const userAuth = authToken();

We are storing token in the localStorage, to make things easier we are creating a helper function which will help us in operating on it.

Here we have created getter and setter function and clear function which will remove the token from localStorage and redirect user to login page.

(public)/+layout.ts

If token value is set in localStorage then check it’s valid. If valid redirect to Home page (protected route) else clear token and continue to login page.

Make sure to group only auth related page like login, signup, reset password, etc. so that user will be redirected to home page (protected route) if token is valid. And not for other public pages like docs, blogs, etc.

import { userAuth } from '$lib/auth.svelte.js';
import type { LayoutLoad } from './$types';

export const ssr = false;

export const load: LayoutLoad = async ({ fetch }) => {
	if (userAuth.token) {
		fetch('/api/user', {
			method: 'GET',
			headers: {
				'Content-Type': 'application/json',
				Authorization: `Bearer ${userAuth.token}`
			}
		})
			.then((response) => {
				if (!response.ok) {
					userAuth.clear();
				}
				return response.json();
			})
			.then((data) => {
				console.log(data);
			})
			.catch((error) => {
				console.error(error);
			});
	}
	return {};
};

Login Page

In login/+page.svelte

  • call your authentication/login api which will give you some kind of auth token
  • store it on the localStorage
  • redirect user to protected route, in this case it’s /
<script lang="ts">
	import { goto } from '$app/navigation';
	import { userAuth } from '$lib/auth.svelte.js';

	let email = $state('');
	let password = $state('');
	let message = $state('');

	function formSubmit(event: any) {
		event.preventDefault();
		fetch('/api/login', {
			method: 'POST',
			headers: {
				'Content-Type': 'application/json'
			},
			body: JSON.stringify({ email, password })
		})
			.then((response) => {
				if (response.ok) {
					return response.json();
				}
				message = 'Invalid credential';
				throw new Error('Network response was not ok.');
			})
			.then((data) => {
				userAuth.token = data.token;
				goto('/');
			});
	}
</script>

<div>
	<form method="post" onsubmit={formSubmit}>
		<fieldset>
			<label>
				Email
				<input
					type="email"
					bind:value={email}
					name="email"
					placeholder="admin@example.com"
					autocomplete="email"
				/>
				<p style="color: red">{message}</p>
			</label>

			<label>
				Password
				<input type="password" bind:value={password} name="password" placeholder="password" />
				<p style="color: red">{message}</p>
			</label>
		</fieldset>

		<input type="submit" value="Login" />
	</form>
</div>

Protected Route

Now, for protected routes we want that only authenticated user can access those. That means just having token in localStorage is not enough, we must verify that by calling user detail api.

(private)/+layout.ts

import { goto } from '$app/navigation';
import { userAuth } from '$lib/auth.svelte.js';
import type { LayoutLoad } from './$types';

export const ssr = false;

export const load: LayoutLoad = async ({ fetch }) => {
	if (!userAuth.token) {
		goto('/login');
	} else {
		fetch('/api/user', {
			method: 'GET',
			headers: {
				'Content-Type': 'application/json',
				Authorization: `Bearer ${userAuth.token}`
			}
		})
			.then((response) => {
				if (!response.ok) {
					userAuth.clear();
				}
				return response.json();
			})
			.then((data) => {
				console.log(data);
			})
			.catch((error) => {
				console.error(error);
			});
	}
	return {};
};

(private)/+layout.svelte

<script lang="ts">
	import { userAuth } from '$lib/auth.svelte.js';

	interface Props {
		children?: import('svelte').Snippet;
	}

	let { children }: Props = $props();
</script>

<div>
	<h2>PROTECTED ROUTE</h2>
	<br />
	<p><a href="/">Home</a></p>
	<br />
	<p><a href="/page2">Page2</a></p>
	<br />
	<p><a href="/unauthorized">This page give 401</a></p>
	<br />
	<button onclick={userAuth.clear}>Logout</button>
</div>

{@render children?.()}

How to handle 401 in child pages

The +layout.ts takes care of token validation when we first enter the protected route or refresh the page.

However we also need to handle the scenarios when the API request inside any of those routes gives back HTTP 401 in response.

HTTP 401 is used as status code for Unauthorized access.

So, in your in your load function in +page.ts file you can do something like this to handle 401:

import { userAuth } from '$lib/auth.svelte.js';
import type { PageLoad } from './$types';

export const load: PageLoad = async ({ fetch }) => {
	const response = await fetch('/api/401');
	if (response.status === 401) {
		userAuth.clear();
	}
	return {};
};

Well not just in load function, even in your +page.svelte file if you are calling fetch on external api, you can handle 401 the same way.

GitHub Repository