eyecatch

How to Protect Your SvelteKit Routes

Posted on 2025/01/04
# Technology

Many applications need to protect their routes for pages such as a user account page.
This blog will demonstrate how to protect SvelteKit routes with two different examples.

You can view the actual code for these examples on GitHub below.

Where we authenticate the user in?

Auth cookies can be checked inside server hooks. If a user is found matching the provided credentials, the user information can be stored in locals.

According to the SvelteKit official site, we will implement authentication in src/hooks.server.ts.
These examples will NOT work in SPA mode because server hooks are only called when the SvelteKit server receives a request.

Folder structure

src
├── routes
│   ├── (protected)
│   │   ├── user
│   │       ├── +page.svelte
│   │       ├── +page.server.ts
│   ├── about
│   │   ├── +page.svelte
│   │   ├── +page.server.ts
│   ├── login
│   │   ├── +page.svelte
│   │   ├── +page.server.ts
│   ├── +page.svelte
│   ├── +page.server.ts
│   ├── +layout.svelte
│   ├── +layout.server.ts
├── hooks.server.ts

I will protect routes within the (protected) folder.
Create empty +page.server.ts files in each route to ensure hooks.server.ts is called; otherwise, it will not be triggered.
I will not describe the details of each file, as they simply display basic text.

For now, let's focus on src/hooks.server.ts.

Two ways to protect routes

  • Directory name base protection (Recommended)
  • Pathname base protection

Directory name base protection (Recommended)

import { redirect, type Handle } from '@sveltejs/kit';
import { sequence } from '@sveltejs/kit/hooks';

const authHandle: Handle = async ({ event, resolve }) => {
	const response = await resolve(event);

	// Get the root directory name
	const rootDirName = event.route.id?.split('/')[1] || '';
	const protectedDirName = '(protected)';

	// Get the auth token from the cookie
	const auth_token = event.cookies.get('auth_token');

        // You may need authorize auth_token in real world.

	// Redirect to the root if the user is not authenticated
	if (
          rootDirName === protectedDirName
          && !auth_token
        ) {
		redirect(301, '/login');
	}

	return response;
};

export const handle = sequence(authHandle);

This approach allows you to protect routes within the (protected) folder without any additional configuration. If you want to secure other routes, simply add files to this directory.

The event.route.id property returns the ID of the current route. For example, for src/routes/(protected)/user, it would return /(protected)/user. You can check whether the current route is protected, and if so, validate the auth_token. If the user is not authorized, redirect them to the login page.

In my opinion, this method is an excellent choice for starting small, as it is both intuitive and easy to maintain.

You can find more detailed code here.

Pathname base protection

import { redirect, type Handle } from '@sveltejs/kit';
import { sequence } from '@sveltejs/kit/hooks';

// Protected path list
const protectedPaths = ['/user'];

const authHandle: Handle = async ({ event, resolve }) => {
	const response = await resolve(event);

	// Get the path name
	const pathName = event.url.pathname;

	// Get the auth token from the cookie
	const auth_token = event.cookies.get('auth_token');

        // You may need authorize auth_token in real world.

	// Redirect to the root if the user is not authenticated
	// and check if the path is in the protected path list
	if (
          protectedPaths.some((protectedPath) => pathName.startsWith(protectedPath))
          && !auth_token
        ) {
		redirect(301, '/login');
	}

	return response;
};

export const handle = sequence(authHandle);

You can protect routes defined in protectedPaths, which provides greater flexibility by allowing path-level protection. However, you need to update protectedPaths whenever new protected routes are added. This could become difficult to maintain as the project grows.

Additionally, the use of protectedPaths.some((protectedPath) => pathName.startsWith(protectedPath)) has a time complexity of O(n). As the number of routes in protectedPaths increases, the time complexity will grow accordingly.

You can view the detailed code here.

Conclusion

Based on my experience, I recommend using directory name-based protection.

When I previously relied on pathname-based protection, it was challenging to develop applications. I often forgot to update protectedPaths, and the project folder structure quickly became messy.

Using directory name-based protection offers better maintainability and is more intuitive, making it easier to manage in the long run.