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:
- User authenticates himself
- You get a auth token in response
- Store the token in localStorage
- Take user to protected route and call whatever api you need to
- 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
Table of Contents
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 {};
};
GitHub RepositoryWell not just in load function, even in your
+page.svelte
file if you are callingfetch
on external api, you can handle401
the same way.