In this tutorial, we’ll delve into the implementation of JSON Web Token (JWT) authentication within SvelteKit. We’ll be using TypeScript along with Prisma, a popular Object-Relational Mapping (ORM) tool for Node.js. By integrating these components, we’ll empower our API to handle crucial user-related functions such as registration, sign-ins, sign-outs, and retrieval of user account details.

To ensure the security of our API, we’ll leverage SvelteKit’s powerful feature known as hooks, which acts as middleware. This will allow us to establish protection for private routes within the API. This approach is particularly advantageous as it enables us to secure multiple endpoints from a single location, simplifying the route protection process.

It’s worth noting that we previously created a similar API in a prior article titled ‘JWT Authentication in Next.js 13 API Route Handlers‘. However, that implementation was built using Next.js 13 and included a frontend to interact with the API. If you’re interested, you can refer to that article to gain insight into the flow. In an upcoming article, we’ll create the frontend for the SvelteKit API developed in this tutorial.

Without further ado, let’s dive straight into the tutorial and start building a JWT authentication system for our SvelteKit application.

More practice (Optional):

Implement JWT Authentication in SvelteKit API Routes

Running the SvelteKit JWT App Locally

To run the SvelteKit application on your local machine, follow these steps:

  1. Download or clone the project from its GitHub repository at https://github.com/wpcodevo/sveltekit-jwt-authentication. Once you have access to the project, open it in your preferred IDE or text editor.
  2. Install all the necessary dependencies by running the command pnpm i in the integrated terminal of your code editor. If you do not already have PNPM installed, you can do so by running npm i -g pnpm.
  3. Duplicate the .env.example file and rename the copy to .env.
  4. If you have a cloud PostgreSQL database, add its URL as the value of the DATABASE_URL variable in the .env file. However, if you don’t have one, you can set it up using Docker by running the command docker-compose up -d.
  5. Run the command pnpm dev --port 3000. This command will synchronize the Prisma migrations with the PostgreSQL database, generate the Prisma Client in the node_modules folder, and start the SvelteKit development server.
  6. Import the SvelteKit HS256 JWT.postman_collection.json file provided in the source code into Postman or the Thunder Client VS Code extension to access predefined requests for testing the API. You can use these requests to test the JWT authentication flow, including signing up, signing in, signing out, and retrieving your account information.

Setting up the SvelteKit Project

As usual, let’s begin by generating a new SvelteKit project. If you already have one, feel free to skip this step. Open a new terminal in the folder where you want to keep the source code, and then run the following command based on your preferred package manager to bootstrap the SvelteKit project:


# For PNPM
pnpm create svelte@latest sveltekit-jwt-authentication

# For Yarn
yarn create svelte@latest sveltekit-jwt-authentication

# For NPM
npm create svelte@latest sveltekit-jwt-authentication

Running this command will temporarily download the Svelte project scaffolding binary from the NPM repository. Next, you’ll be prompted to choose a template. Select the “skeleton project” option, and then choose “Yes” for TypeScript type checking. Optionally, you can include ESLint and Prettier by selecting them with the spacebar. Finally, press the Enter key to generate the project.

Once the project is created, navigate to its folder and run pnpm install to install all the necessary dependencies. After the installation is complete, you can open the project in your preferred IDE. Now you’re all set to start building your SvelteKit JWT authentication application!

Setting up a PostgreSQL Database

Moving on, let’s set up a cloud Postgres database, which will be beneficial when we decide to deploy the application online. I’ll guide you through the process of obtaining the connection URI from Supabase, but feel free to choose any other provider if you prefer. Follow these simple steps:

  1. Sign up at https://supabase.com/dashboard/sign-up using your GitHub account.
  2. Create a new project. Once the project is set up, copy the PostgreSQL database password and keep it in a place you can easily access. Next, click on the settings icon located in the left sidebar.
  3. In the settings page, select “Database” and scroll down to the “Connection string” section. Under this, choose the “URI” tab and copy the provided URI.
  4. Now, create a .env file at the root level of your project and paste the copied connection string as the value for the DATABASE_URL variable. Be sure to replace the [YOUR-PASSWORD] placeholder with your actual Postgres database password, which you received when creating the new project on Supabase.
    Copy the PostgreSQL Connection String from Supabase

.env


# -----------------------------------------------------------------------------
# Database (PostgreSQL - Supabase)
# -----------------------------------------------------------------------------

DATABASE_URL=paste_cloud_postgres_uri_here

Optional: Using Docker

If you haven’t obtained a PostgreSQL connection URI from a cloud provider, don’t worry. We can easily set it up using Docker and add pgAdmin to help us view and manipulate the data stored in the PostgreSQL database. This is an optional step for those who didn’t use a cloud-based Postgres database.

To set up a Postgres server and pgAdmin in Docker, follow these steps:

  1. Create a docker-compose.yml file at the root level of your project.
  2. Add the following configurations to the docker-compose.yml file:

docker-compose.yml


services:
  postgres:
    image: postgres
    container_name: postgres
    ports:
      - 6500:5432
    env_file:
      - ./.env
    volumes:
      - postgres:/var/lib/postgresql/data
  pgadmin:
    image: dpage/pgadmin4
    container_name: pgadmin
    ports:
      - "5050:80"
    env_file:
      - ./.env
    volumes:
      - pgadmin-data:/var/lib/pgadmin
    restart: always
volumes:
  postgres:
  pgadmin-data:

In the configuration, we used a .env file in the env_file fields of both the postgres and pgadmin services. This file will hold the credentials needed to build the PostgreSQL and pgAdmin images. To ensure these credentials are available, create a .env file at the project’s root directory and include the following environment variables:

.env


# -----------------------------------------------------------------------------
# PostgreSQL Credentials for Docker Compose
# -----------------------------------------------------------------------------
POSTGRES_HOST=127.0.0.1
POSTGRES_USER=admin
POSTGRES_PASSWORD=password123
POSTGRES_DB=sveltekit_jwt_app
POSTGRES_PORT=6500

# -----------------------------------------------------------------------------
# pgAdmin4 Credentials for Docker Compose
# -----------------------------------------------------------------------------
PGADMIN_DEFAULT_EMAIL=admin@admin.com
PGADMIN_DEFAULT_PASSWORD=password123
PGADMIN_LISTEN_PORT=80


# -----------------------------------------------------------------------------
# Database (PostgreSQL - Docker)
# -----------------------------------------------------------------------------
DATABASE_URL=postgresql://admin:password123@localhost:6500/sveltekit_jwt_app?schema=public

With the environment variables set, run the command docker-compose up -d to start both the PostgreSQL and pgAdmin servers in their respective containers.

Setting up Prisma ORM

Now that the PostgreSQL database is up and running, let’s proceed to set up Prisma in the project. Follow these steps:

Step 1: Install Prisma CLI and Client

Run the command below to install both the Prisma CLI and the Client:


# For PNPM
pnpm add @prisma/client && pnpm add -D prisma

# For Yarn
yarn add @prisma/client && yarn add -D prisma

# For NPM
npm i @prisma/client && npm i -D prisma

Step 2: Initialize Prisma in the Project

Next, initialize Prisma in the project by running the command below:


# For PNPM
pnpm prisma init --datasource-provider postgresql

# For Yarn
yarn prisma init --datasource-provider postgresql

# For NPM
npx prisma init --datasource-provider postgresql

This command will generate a prisma directory at the root level, containing a schema.prisma file. The schema.prisma file holds information about the database provider along with the necessary Prisma models. Open the schema.prisma file and replace its existing content with the following:

prisma/schema.prisma


// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id        String    @id @unique @default(uuid())
  name      String
  email     String    @unique
  password  String
  role      String?   @default("user")
  photo     String?   @default("default.png")
  verified  Boolean?  @default(false)
  createdAt DateTime? @default(now())
  updatedAt DateTime? @updatedAt
}

In this schema, we defined a User model with fields that the Prisma engine will later convert into their corresponding columns in the database.

Step 3: Performing Database Migrations

With the User model defined in the Prisma schema, it’s time to generate and apply migrations to the PostgreSQL database. Follow these steps:

Step 3.1: Generate Migrations

Run the following command to establish a connection to the PostgreSQL database, generate migrations named init, and apply the necessary changes:


# For PNPM
pnpm prisma migrate dev --name init

# For Yarn
yarn prisma migrate dev --name init

# For NPM
npx prisma migrate dev --name init

This command will generate a migration with the name init in the migrations directory. The migration contains the SQL commands to create the necessary tables and columns based on the defined User model.

Step 3.2: View Tables in pgAdmin

To view the tables added to the PostgreSQL database, access the pgAdmin GUI by opening your web browser and navigating to http://localhost:5050/. Ensure that your Docker container is up and running.

Next, log in to pgAdmin using the credentials specified in the .env file. Once logged in, on the pgAdmin dashboard, create a new server using the PostgreSQL credentials from the .env file. Use the default Postgres port (5432) instead of the one provided in the .env file.

To obtain the IP address of the running Postgres server, follow these steps:

  1. Run the command docker ps to list all running Docker containers.
  2. Find the container ID of the PostgreSQL container from the output.
  3. Run the command docker inspect <container_id>, replacing <container_id> with the actual container ID from the previous step.
  4. In the output, scroll down to the “NetworkSettings” section and copy the value of the “IPAddress” field.
    Flask App Add a New Postgres Server on pgAdmin

After successfully registering the Postgres server in pgAdmin, navigate to sveltekit_jwt_app -> Schemas -> Tables section. Next, right-click on the User table and select “Properties” to view its details. In the displayed popup, click on the “Columns” tab to explore the table’s columns and their properties.

Viewing the User Table in pgAdmin

Connecting to Database

We are now ready to establish the connection between the SvelteKit application and the PostgreSQL database using the Prisma Client. To ensure smooth development and prevent unnecessary database connections during hot reloads, we will store the Prisma Client globally.

Open the app.d.ts file located in the src directory. This file is used for type declarations in SvelteKit projects. Add the following code to declare a global variable named prisma with the type PrismaClient:

src/app.d.ts


// See https://kit.svelte.dev/docs/types#app

import type { PrismaClient } from '@prisma/client';

// for information about these interfaces
declare global {
	namespace App {
		// interface Error {}
		// interface Locals {}
		// interface PageData {}
		// interface Platform {}
	}
	var prisma: PrismaClient;
}

export {};

Next, navigate to the src/lib/ directory and create a new folder called server. Inside the server folder, add a new file named prisma.ts. This file will handle the creation of the Prisma Client and manage its global instance. Include the following code in the prisma.ts file:

src/lib/server/prisma.ts


import { PrismaClient } from '@prisma/client';

const options: any = process.env.NODE_ENV === 'development' ? { log: ['query'] } : {};
const prisma = globalThis.prisma ?? new PrismaClient(options);

if (process.env.NODE_ENV === 'development') globalThis.prisma = prisma;

export { prisma };

This code sets up the Prisma Client with additional logging in the development environment to view executed queries. It checks if a global prisma variable already exists, and if not, it creates a new instance of the Prisma Client. During development, the prisma instance is stored globally to avoid multiple connections.

Creating the Request Validation Schemas

When building an API, it’s essential to validate incoming data before storing it in the database to ensure data integrity. To achieve this, we’ll use the zod library, which provides powerful schema validation capabilities. Let’s install Zod first:


# For PNPM
pnpm add zod

# For Yarn
yarn add zod

# For NPM
npm i zod

Once Zod is installed, create a new folder named validations inside the src/lib/ directory. This folder will hold all our validation schemas. Although we’ll define just one schema in this article, placing the validations folder in the src/lib/ directory allows us to use the validation schemas on both the server and client.

Within the validations folder, create a user.schema.ts file and add the following code to it:

src/lib/validations/user.schema.ts


import { z } from 'zod';

export const RegisterUserSchema = z
	.object({
		name: z
			.string({
				required_error: 'Name is required'
			})
			.min(1, 'Full name is required'),
		email: z
			.string({
				required_error: 'Email is required'
			})
			.min(1, 'Email is required')
			.email('Email is invalid'),
		photo: z.string().optional(),
		password: z
			.string({
				required_error: 'Password is required'
			})
			.min(1, 'Password is required')
			.min(8, 'Password must be more than 8 characters')
			.max(32, 'Password must be less than 32 characters'),
		passwordConfirm: z
			.string({
				required_error: 'Confirm your password'
			})
			.min(1, 'Confirm your password')
	})
	.refine((data) => data.password === data.passwordConfirm, {
		path: ['passwordConfirm'],
		message: 'Passwords do not match'
	});

export const LoginUserSchema = z.object({
	email: z
		.string({
			required_error: 'Email is required'
		})
		.min(1, 'Email is required')
		.email('Email is invalid'),
	password: z
		.string({
			required_error: 'Password is required'
		})
		.min(1, 'Password is required')
		.min(8, 'Password must be at least 8 characters')
});

export type LoginUserInput = z.infer<typeof LoginUserSchema>;
export type RegisterUserInput = z.infer<typeof RegisterUserSchema>;

We’ve defined two validation schemas: RegisterUserSchema for validating the request body during the account registration process, and LoginUserSchema for validating the request body during the login process. At the bottom of the file, we use Zod’s z.infer generic type to obtain the TypeScript types from the schemas. This will ensure that we get type safety when using the schemas throughout our application.

Signing and Verifying JSON Web Tokens

To handle JSON Web Tokens (JWTs) in a secure and efficient way, we’ll use the jose library instead of the jsonwebtoken library. The advantage of using jose is that it doesn’t depend on the Crypto module, making it suitable for serverless environments where Crypto might not be available. Let’s install jose:


# For PNPM
pnpm add jose

# For Yarn
yarn add jose

# For NPM
npm i jose

Next, open the .env file and add the following environment variables to define the JWT secret and expiration time. Ensure that your JWT secret key is strong and hard to guess for enhanced security.

.env


# -----------------------------------------------------------------------------
# JSON Web Token Credentials
# -----------------------------------------------------------------------------
JWT_SECRET_KEY=my_ultra_secure_jwt_secret_key
JWT_EXPIRES_IN=60

  • JWT_SECRET_KEY – will be used to sign the tokens securely.
  • JWT_EXPIRES_IN – specifies the token expiration time in minutes (here set to 60 minutes).

Sign JWTs

In this section, we’ll create a utility function called signJWT responsible for signing new JSON Web Tokens (JWTs). The signJWT function takes two parameters: payload, which contains the subject (sub) of the token, and options, which includes the expiration time (exp) of the token. The function will sign the JWT using the HS256 algorithm and return the signed token.

To get started, create a new file named token.ts in the src/lib/server/ directory and add the following code:

src/lib/server/token.ts


import { JWT_SECRET_KEY } from '$env/static/private';
import { SignJWT, jwtVerify } from 'jose';

export const signJWT = async (payload: { sub: string }, options: { exp: string }) => {
	try {
		const secret = new TextEncoder().encode(JWT_SECRET_KEY);
		const alg = 'HS256';
		return new SignJWT(payload)
			.setProtectedHeader({ alg })
			.setExpirationTime(options.exp)
			.setIssuedAt()
			.setSubject(payload.sub)
			.sign(secret);
	} catch (error) {
		throw error;
	}
};

In the signJWT function, we first convert the JWT_SECRET_KEY from the environment to bytes using TextEncoder. Then, we set the algorithm to HS256, which is a widely used hashing algorithm for signing JWTs. We set the expiration time of the token using the options.exp parameter and add the current timestamp as the issued at (iat) claim. Finally, we set the subject (sub) claim to the provided payload’s sub value and sign the token with the secret key.

Verify JWTs

In the same token.ts file, we’ll implement another utility function called verifyJWT to verify the integrity of a JWT and ensure it’s valid.

src/lib/server/token.ts


export const verifyJWT = async <T>(token: string): Promise<T> => {
	try {
		return (await jwtVerify(token, new TextEncoder().encode(JWT_SECRET_KEY))).payload as T;
	} catch (error) {
		console.log(error);
		throw new Error('Your token has expired.');
	}
};

The verifyJWT function takes a token parameter, which is the JWT to be verified. Inside the function, we use jwtVerify from the jose library to verify the JWT’s signature and integrity. If the token is valid, we return its payload as the generic type T, which allows us to use the verified payload with a specific interface. If the token is invalid or has expired, an error will be thrown, indicating that the token is no longer valid.

Creating the SvelteKit API Endpoints

Now, we’ve reached the heart of this tutorial – the point where we build the core API endpoints responsible for managing JWT authentication. Let’s outline the key endpoints we’ll be creating:

  1. /api/auth/register – Here, users can register and create new accounts.
  2. /api/auth/login – This endpoint handles the login process for registered users.
  3. /api/auth/logout – Designed for securely logging out authenticated users.
  4. /api/users/me – This endpoint allows authenticated users to retrieve their credentials.

Let’s dive right in. Open your terminal and install the Bcrypt library using this command:


# For PNPM
pnpm add bcryptjs && pnpm add -D @types/bcryptjs

# For Yarn
yarn add bcryptjs && yarn add -D @types/bcryptjs

# For NPM
npm i bcryptjs && npm i -D @types/bcryptjs

Why are we incorporating the Bcrypt library? Well, it serves a pivotal role in our authentication system by hashing user passwords before they’re stored in the database. This practice is crucial for maintaining security. By hashing passwords, even if a breach were to occur, hackers wouldn’t gain immediate access to users’ accounts through plain text passwords.

Registering a User

Let’s start with the first endpoint, which gets invoked to register a user when a POST request is submitted to the /api/auth/register route.

To create this route handler, follow these steps:

  1. Within the src/routes/ folder, create a new directory named api.
  2. Inside the api directory, create a subdirectory named auth.
  3. Within the auth directory, create a further subdirectory named register.
  4. Inside the register directory, create a file named +server.ts and paste the provided code.

src/routes/api/auth/register/+server.ts


import { RegisterUserSchema, type RegisterUserInput } from '$lib/validations/user.schema.js';
import { json } from '@sveltejs/kit';
import bcrypt from 'bcryptjs';
import { ZodError } from 'zod';
import { prisma } from '$lib/server/prisma';

export async function POST({ request }) {
	try {
		const body = (await request.json()) as RegisterUserInput;
		const data = RegisterUserSchema.parse(body);

		const hashedPassword = await bcrypt.hash(data.password, 12);

		const user = await prisma.user.create({
			data: {
				name: data.name,
				email: data.email,
				password: hashedPassword,
				photo: data.photo
			}
		});

		return json({ status: 'success', data: { ...user, password: undefined } }, { status: 201 });
	} catch (error: any) {
		if (error instanceof ZodError) {
			return json({ message: 'failed validations', error: error.flatten() }, { status: 400 });
		}

		if (error.code === 'P2002') {
			return json({ message: 'user with that email already exists' }, { status: 409 });
		}

		return json({ message: error.message }, { status: 500 });
	}
}

Here’s a detailed breakdown of how it works:

  1. Request Handling: When a client makes a POST request to /api/auth/register, this handler is triggered. It begins by extracting the request body using request.json() to access the submitted user registration data.
  2. Data Validation: The incoming data is then subjected to validation using the RegisterUserSchema schema. This schema is designed to ensure that the provided data adheres to predefined validation rules, such as required fields, email format, and password constraints.
  3. Validation Error Handling: If any validation errors occur during the schema validation, the handler captures these errors. The catch block is responsible for handling such cases.
  4. Hashing Password: Assuming validation passes successfully, the handler proceeds to securely hash the user’s provided password using the bcrypt library. This is a vital security measure, ensuring that sensitive user data is not stored in plain text.
  5. Storing User Data: The code then uses prisma.user.create() to insert the user’s information into the database. The name, email, hashed password, and optional photo are included in the data being stored.
  6. Response: Upon successful user creation, a JSON response is generated using the json function. This response includes a status indicating success and a modified version of the user data, excluding the sensitive password field. The HTTP status code 201 Created is also set.
  7. Error Handling: In case of any errors during the entire process, specific error scenarios are handled. If the error is due to failed schema validation, a 400 Bad Request response is generated, accompanied by details of the validation errors. Additionally, if a user with the provided email already exists in the database, a 409 Conflict response is returned. For any other errors, a 500 Internal Server Error response is generated.

Logging in a User

Moving forward, let’s address the login process. This route handler comes into play when a POST request is sent to the /api/auth/login endpoint.

To implement this route handler, follow these steps:

  1. Create a new directory named login within the auth directory under src/routes/api.
  2. Inside the login directory, create a file named +server.ts.
  3. Add the following code to the +server.ts file:

src/routes/api/auth/login/+server.ts


import { LoginUserSchema, type LoginUserInput } from '$lib/validations/user.schema.js';
import { json } from '@sveltejs/kit';
import { ZodError } from 'zod';
import { prisma } from '$lib/server/prisma.js';
import bcrypt from 'bcryptjs';
import { signJWT } from '$lib/server/token.js';
import { JWT_EXPIRES_IN } from '$env/static/private';

export async function POST({ request, cookies }) {
	try {
		const body = (await request.json()) as LoginUserInput;
		const data = LoginUserSchema.parse(body);

		const user = await prisma.user.findUnique({
			where: { email: data.email }
		});

		if (!user || !(await bcrypt.compare(data.password, user.password))) {
			return json({ message: 'Invalid email or password' }, { status: 401 });
		}

		const token = await signJWT({ sub: user.id }, { exp: `${JWT_EXPIRES_IN}m` });

		const tokenMaxAge = parseInt(JWT_EXPIRES_IN) * 60;

		const cookieOptions = {
			httpOnly: true,
			path: '/api',
			secure: process.env.NODE_ENV !== 'development',
			maxAge: tokenMaxAge
		};

		cookies.set('token', token, cookieOptions);
		cookies.set('logged-in', 'true', {
			...cookieOptions,
			httpOnly: false
		});

		return json({ token });
	} catch (error: any) {
		if (error instanceof ZodError) {
			return json({ message: 'failed validations', error: error.flatten() }, { status: 400 });
		}

		return json({ message: error.message }, { status: 500 });
	}
}

This route handler is responsible for processing user login attempts via a POST request to the /api/auth/login endpoint. Upon receiving the request, it validates the provided data against the LoginUserSchema. If the validation succeeds and the user is authenticated, a JSON Web Token (JWT) is created and returned in the response, allowing the user to access authenticated routes.

Additionally, the route handler sets cookies containing the JWT and relevant authentication information, which are then utilized to maintain user sessions. In the case of any errors, appropriate error messages and status codes are returned to facilitate effective communication with the client.

Logging out a User

Now that we have the route handlers responsible for registering and logging in users, let’s shift our attention to creating the handler that will facilitate signing them out of the API.

Here, we will use a simple technique to achieve the sign-out process by sending expired cookies to their API client or browser, effectively removing the existing ones they acquired during the login process. While this approach may not be the most advanced solution for handling logouts, it serves our purpose effectively for this basic API.

To proceed with implementing this route handler, follow these steps:

  1. Establish a new directory named logout within the auth directory under src/routes/api.
  2. Inside the logout directory, generate a file titled +server.ts.
  3. Embed the ensuing code snippet into the +server.ts file:

src/routes/api/auth/logout/+server.ts


import { json } from '@sveltejs/kit';

export async function POST({ cookies, locals }) {
	locals.user = null;

	const cookieOptions = {
		path: '/api',
		secure: process.env.NODE_ENV !== 'development'
	};

	cookies.delete('token', cookieOptions);
	cookies.delete('logged-in', cookieOptions);

	return json({ status: 'success' });
}

Fetching Authenticated User Data

Now, let’s focus on the final route handler, which serves to retrieve the credentials of an authenticated user. This handler gets invoked when a GET request is sent to the /api/users/me endpoint. The purpose of this endpoint is to provide authenticated users with their own information.

The process for this route handler is simple: it retrieves the user’s credentials from the locals object. If there is no user information found, it responds with a JSON message indicating that the user is not logged in. Otherwise, the user’s information is returned in a JSON response, with the password omitted for security reasons.

To create this route handler, follow these steps:

  1. Inside the src/routes/api directory, create a users directory.
  2. Within the users directory, create a file named me with the extension +server.ts.
  3. Add the following code to the me file:

src/routes/api/users/me/+server.ts


import { json } from '@sveltejs/kit';

export async function GET({ locals }) {
	try {
		const user = locals.user;

		if (!user) {
			return json(
				{
					message: 'You are not logged in, please provide token to gain access'
				},
				{ status: 401 }
			);
		}

		return json({ data: { ...user, password: undefined } });
	} catch (error: any) {
		return json({ message: error.message }, { status: 500 });
	}
}

Protecting the API Endpoints with SvelteKit Hooks

With the route handlers in place, we are now ready to implement SvelteKit hooks (middleware) to safeguard specific endpoints accessible only to authenticated users. To begin, let’s address TypeScript integration. Open the src/app.d.ts file and uncomment // interface Locals {} within the App namespace. Subsequently, add a new field named “user” and define its type using the User type generated by Prisma. Here’s the necessary adjustment:

src/app.d.ts


// See https://kit.svelte.dev/docs/types#app

import type { PrismaClient, User } from '@prisma/client';

// for information about these interfaces
declare global {
	namespace App {
		// interface Error {}
		interface Locals {
			user: User | null;
		}
		// interface PageData {}
		// interface Platform {}
	}
	var prisma: PrismaClient;
}

export {};

Now that this adjustment is complete, let’s move forward and create the middleware. Begin by creating a hooks.server.ts file within the src directory, and include the following code:

src/hooks.server.ts


import { verifyJWT } from '$lib/server/token';
import { error, type Handle } from '@sveltejs/kit';
import { prisma } from '$lib/server/prisma';

export const handle: Handle = async ({ resolve, event }) => {
	const { url, locals, request, cookies } = event;

	let authToken: string | undefined;

	if (cookies.get('token')) {
		authToken = cookies.get('token');
	} else if (request.headers.get('Authorization')?.startsWith('Bearer ')) {
		authToken = request.headers.get('Authorization')?.substring(7);
	}

	if (
		!authToken &&
		(url.pathname.startsWith('/api/users') || url.pathname.startsWith('/api/auth/logout'))
	) {
		throw error(401, 'You are not logged in. Please provide a token to gain access.');
	}

	try {
		if (authToken) {
			const { sub } = await verifyJWT<{ sub: string }>(authToken);
			const user = await prisma.user.findUnique({ where: { id: sub } });
			if (!user) {
				throw error(401, 'User belonging to this token no longer exists');
			}

			locals.user = user;
		}
	} catch (err: any) {
		if (url.pathname.startsWith('/api')) {
			throw error(401, "Token is invalid or user doesn't exists");
		}
	}

	const response = await resolve(event);

	return response;
};

The code’s purpose is to attempt to retrieve the token from two potential sources: cookies or the Authorization header. If the token is found, it’s assigned to the authToken variable. If no token is found, a 401 error is thrown or returned to inform the user that a token is required.

When authToken is present, the function proceeds to verify the token’s authenticity using the verifyJWT function. The payload of the token, containing the subject (sub), is extracted. The prisma client is then used to query the database and fetch user data based on the sub. If the user exists, their credentials are stored in the locals object.

For scenarios where errors occur during token verification or user retrieval, suitable error responses are generated based on the request’s context. For example, if the user doesn’t exist or the token is invalid, a 401 Unauthorized error is triggered. After authentication and error handling, the resolve function is called to continue the request/response cycle.

Testing the JWT Application

At this point, we have successfully completed the development of our SvelteKit application and are now poised to put our endpoints to the test. To begin, ensure that you’ve applied the Prisma migrations to your PostgreSQL database. Once this step is completed, you can launch the SvelteKit development server by running the command pnpm dev --port 3000.

Once the server is up and running, you’re ready to proceed. Open either Postman or utilize the Thunder Client extension within Visual Studio Code. Import the SvelteKit HS256 JWT.postman_collection.json file, which is included in the source code. This collection file equips you with preconfigured requests, simplifying the process of testing the various API endpoints.

Sign Up

To register an account, include your credentials in JSON format within the request body and send a POST request to the /api/auth/register endpoint. The structure should resemble the following:


{
    "email": "admin@admin.com",
    "name": "Admin",
    "password": "password123",
    "passwordConfirm": "password123"
}

Feel free to skip some fields to test the server’s validation logic. If you exclude certain fields from the JSON object or if the password and password confirmation fields don’t match, you’ll receive validation errors from the SvelteKit API. Assuming you’ve correctly provided the necessary fields and adhered to the appropriate data types in the JSON object, you can expect to receive a 201 response from the API.

SvelteKit JWT Authentication API Register a New Account

Sign In

To log into your account, provide your email and password in a JSON format within the request body. Send this as a POST request to the /api/auth/login endpoint. Here’s what it should look like:


{
    "email": "admin@admin.com",
    "password": "password123"
}

The API will then check if the email and password match what’s in the database. If they do, you’ll get a special code (JWT) in the response. This token can be utilized to access protected routes securely.

SvelteKit JWT Authentication API Log into Your Account

Accessing Protected Route

Now that you possess a token obtained from the API, you can use it to access your account details. You have the option to embed the token either within the Authorization header as Bearer or utilize Cookies. If your API client is compatible with cookies, send a GET request to the /api/users/me endpoint, and your account information will be delivered in the response.

In this scenario, your API client will automatically transmit the token contained within the cookie alongside the request. Alternatively, if your API client lacks cookie support, you can include the token as a Bearer in the Authorization header before sending the GET request to the /api/users/me endpoint.

SvelteKit JWT Authentication API Retrieve Your Account Details

Sign Out

To log out from your account, initiate a POST request to the /api/auth/logout endpoint. Ensure that you include the token within the Authorization header as Bearer, especially if your API client lacks cookie support. This step is essential as the endpoint is secured and only accessible to authenticated users.

SvelteKit JWT Authentication API Log Out From Your Account

Conclusion

Congratulations on reaching the end! Throughout this comprehensive tutorial, you have learned how to build an API in SvelteKit using Prisma ORM, which includes support for JWT authentication. I hope you found the tutorial helpful and enjoyable. If you have any questions or feedback, please feel free to leave them in the comment section. Thank you for reading!