Mastering Authentication in Next.js

Osaro OnaiwuOsaro Onaiwu
7 min read

Hello, folks!

In this blog post, we'll dive deep into a (if not the) very crucial part of most websites- Authentication.

This tutorial is the first of 3 parts:

Part 1: Setting Up Next.js with a Database (Prisma & SQLite)

Part 2: Seeding a Database with Prisma (TypeScript)

Part 3: Authentication in Next.js


NEXT.JS

To get started with Next.js:

  1. Navigate to a named, empty folder/directory

  2. Create a new application using the command line.

npx create-next-app@latest ./

For a smooth follow-along, use the defaults when prompted.

Thereafter, replace the code in the default page to:

// src/app/page.tsx
const Home = () => {
 return <h1>Hello World!</h1>;
};
export default Home;

Let’s spin the server- start the application:

npm run dev

From this point on we will be setting up SQLite & Prisma.


SQLITE

SQLite is the most widely deployed database in the world. It is a self-contained, serverless database that stores data in a single file.

Let’s install it:

npm install sqlite3

SQLite database is simple to implement and can handle a significant amount of data, especially when optimized and used efficiently.


PRISMA

Prisma is a next-generation ORM (Object-Relational Mapping) tool designed to simplify database interactions. It offers a declarative way to define your database schema and provides a type-safe query builder for interacting with your data.

Using the following command, we would have Prisma installed:

npm install prisma --save-dev

Next is a critical step- to initialize Prisma with SQLite as the data source provider:

npx prisma init --datasource-provider sqlite

The preceding command creates

  1. at the root of the project/working directory, a “prisma” folder and inside this folder a file “schema.prisma
// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}
  1. At the project root, a .env” file. This file contains the path to the SQLite database file:

     DATABASE_URL="file:./dev.db"
    

    Create the file: dev.db inside the prisma folder referenced in 1.

    Up next is what’s referred to as Prisma Migrations that simplifies the process of managing database schema changes in your Prisma application.


PRISMA MIGRATIONS

This allows you to define your desired database schema using Prisma Schema and then automatically generates and applies necessary migrations to bring your existing database into sync.

For example, when you want to add a new table to the database, you can create it in the Prisma schema file.

Different workflows require unique database models. Here we are to employ 2 database models for the authentication flow.

First, the User model will hold the user data while the second model is the Session model where we will store the session data.

A user can have multiple sessions, but a session can only belong to a single user. This creates a one-to-many relationship between the User and Session models. In other words, one user can have many sessions, while one session belongs to only one user.

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

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

model User {
 id              String    @id
 sessions        Session[]
 firstName       String
 lastName        String
 email           String    @unique
 hashedPassword  String
}

model Session {
 id        String   @id
 expiresAt DateTime
 userId    String
 user      User     @relation(references: [id], fields: [userId], onDelete: Cascade)
}

I will briefly summarize what the codebase means:

User Model

  • id: A unique identifier for the user (often a UUID or auto-incrementing integer).

  • sessions: An array of Session objects, indicating the sessions associated with this user.

  • firstName: The user's first name.

  • lastName: The user's last name.

  • email: The user's email address, marked as @unique to ensure it is unique across all users.

  • hashedPassword: The hashed password of the user, stored securely to prevent unauthorized access.

Session Model

  • id: A unique identifier for the session (often a UUID).

  • expiresAt: The timestamp when the session expires.

  • userId: A foreign key referencing the id field of the User model, indicating the user associated with the session.

  • user: A relation to the User model, defined by the @relation directive.

    • references: [id]: Specifies that the userId field in Session references the id field in User.

    • fields: [userId]: Specifies that the userId field in Session is used to establish the relationship.

    • onDelete: Cascade: Specifies that if a User is deleted, all associated Sessions should also be deleted (a cascading delete).

Here’s how both models relate:

The @relation directive in the Session model establishes a one-to-many relationship between the User and Session models. This means that one user can have many sessions, but a session belongs to only one user.

In summary, the User model stores information about individual users, while the Session model tracks active user sessions. The @relation directive ensures that sessions are correctly linked to their respective users, and the onDelete: Cascade rule maintains data integrity by deleting associated sessions when a user is removed.

Next step is to synchronize your local database schema with your Prisma schema with the following command:

npx prisma db push

The above command auto-generates code in the dev.db file:

CREATE TABLE "User" (
  "id" TEXT NOT NULL PRIMARY KEY,
  "firstName" TEXT NOT NULL,
  "lastName" TEXT NOT NULL,
  "email" TEXT NOT NULL UNIQUE
);

CREATE TABLE "Session" (
  "id" TEXT NOT NULL PRIMARY KEY,
  "expiresAt" DATETIME NOT NULL,
  "userId" TEXT NOT NULL,
  CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE
);

To view and manage the data in your Prisma-connected database, you would have to launch a web-based GUI with the command:

npx prisma studio

When the command fully runs, you will find a brief report on operations/actions effected along-side the local server where the studio is located for example http://localhost:5555. There, you will find:

This confirms that the database has been successfully created.

The work is not yet done as we need to figure out how to read from and write to the database.

Enter Prisma Client…


PRISMA CLIENT

Prisma Client is an auto-generated, type-safe query builder that allows you to interact with your database using JavaScript or TypeScript, making it easier to perform CRUD operations.

To employ it, we will have to initialize Prisma Client.

// src/lib/prisma.ts
import { PrismaClient } from '@prisma/client';

const prismaClientSingleton = () => {
 return new PrismaClient();
};

declare global {
 var prisma: undefined | ReturnType<typeof prismaClientSingleton>;
}

export const prisma = globalThis.prisma ?? prismaClientSingleton();

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

Here is a breakdown of the code:

Singleton Function:

const prismaClientSingleton = () => { return new PrismaClient()};

This function creates a new instance of PrismaClient and returns it. The purpose of wrapping it in a function is to control when it's created (instead of directly creating an instance at the time of import).

Global Variable Declaration:

declare global { var prisma: undefined | ReturnType<typeof prismaClientSingleton> }

This declares a global variable named prisma with a type that can be either undefined or the return type of the prismaClientSingleton function. This allows you to access the prisma instance from anywhere in your application.

Export:

export const prisma = globalThis.prisma ?? prismaClientSingleton()

This line exports the prisma variable, making it available for use in other parts of your application. The ?? operator is used to check if globalThis.prisma is defined. If it's not, a new instance of PrismaClient is created and assigned to prisma.

Production Environment Check:

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

This ensures that in non-production environments (such as development), the Prisma Client instance is stored in the globalThis object. This prevents multiple instances from being created during hot-reloading, which can happen in serverless or development environments where the code is frequently reloaded.


SO FAR

In summary, we have our initial frontend setup- NextJS, database installed- SQLite, database model ready and our go-between our frontend and database- Prisma Client.

That concludes the first part of my Mastering Authentication in Next.js tutorial.

Next is the second part: a short tutorial on seeding our database, just to make sure everything is in order.

1
Subscribe to my newsletter

Read articles from Osaro Onaiwu directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Osaro Onaiwu
Osaro Onaiwu