Building a robust authentication system from scratch requires careful consideration of various components, including user registration. In this article, we will delve into the process of building a register user flow in Next.js 15, covering both frontend and backend aspects.
When a user hits the “Create account” button, a series of events unfolds, culminating in a working signup page, a server endpoint, a users table, and a verification email getting delivered. To achieve this, we need to create a server endpoint to handle the registration request, a users table to store user data, and a verification email system to ensure users verify their email addresses before accessing their accounts.
### Setting Up the Users Table
Before diving into the registration flow, let’s establish the users table schema. As mentioned in the previous article, we will use the following schema:
sql
create table users (
id bigserial primary key,
email citext not null unique,
password_hash text not null,
email_verified boolean not null default false,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
We also need to create an `email_verification_tokens` table to store verification tokens:
sql
create table email_verification_tokens (
id bigserial primary key,
user_id bigint not null references users(id) on delete cascade,
token_hash text not null unique,
expires_at timestamptz not null,
used_at timestamptz,
created_at timestamptz not null default now()
);
To ensure efficient lookup of unused tokens for a given user, we create a partial index:
sql
create index idx_evt_user_unused on email_verification_tokens (user_id) where used_at is null;
### Creating the Registration Endpoint
Next, we create a server endpoint to handle the registration request. In Next.js, we can achieve this using a POST request to the `/api/auth/register` endpoint. The endpoint will receive the user’s email and password, create a new user record in the database, and send a verification email to the user’s email address.
javascript
import { NextApiRequest, NextApiResponse } from ‘next’;
import { db } from ‘./lib/db’;
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method!== ‘POST’) {
return res.status(405).json({ error: ‘Method not allowed’ });
}
const { email, password } = req.body;
// Validate user input
if (!email ||!password) {
return res.status(400).json({ error: ‘Email and password are required’ });
}
// Hash password
const passwordHash = await hashPassword(password);
// Create new user record
const user = await db.users.create({
data: {
email,
password_hash: passwordHash,
email_verified: false,
},
});
// Create new verification token
const token = await db.email_verification_tokens.create({
data: {
user_id: user.id,
token_hash: generateToken(),
expires_at: new Date(Date.now() + 86400000), // 1 day
},
});
// Send verification email
await sendVerificationEmail(email, token.token_hash);
return res.status(201).json({ message: ‘User created and verification email sent’ });
}
### Implementing Email Verification
To implement email verification, we need to create a separate endpoint to handle verification requests. This endpoint will receive the verification token, check if it’s valid, and update the user’s email_verified status accordingly.
javascript
import { NextApiRequest, NextApiResponse } from ‘next’;
import { db } from ‘./lib/db’;
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method!== ‘GET’) {
return res.status(405).json({ error: ‘Method not allowed’ });
}
const { token } = req.query;
// Validate token
if (!token) {
return res.status(400).json({ error: ‘Token is required’ });
}
// Check if token is valid
const tokenRecord = await db.email_verification_tokens.findOne({
where: {
token_hash: token,
used_at: null,
expires_at: { gt: new Date() },
},
});
if (!tokenRecord) {
return res.status(401).json({ error: ‘Invalid or expired token’ });
}
// Update user’s email_verified status
await db.users.update({
where: {
id: tokenRecord.user_id,
},
data: {
email_verified: true,
},
});
// Delete used token
await db.email_verification_tokens.delete({
where: {
id: tokenRecord.id,
},
});
return res.status(302).redirect(‘/login’);
}
### Frontend Implementation
On the frontend, we need to create a registration form that sends a POST request to the `/api/auth/register` endpoint when submitted. We also need to handle the verification email by creating a new endpoint that redirects the user to the login page after verification.
javascript
import { useState } from ‘react’;
import { useRouter } from ‘next/navigation’;
export default function Register() {
const router = useRouter();
const [email, setEmail] = useState(”);
const [password, setPassword] = useState(”);
const [pending, setPending] = useState(false);
const [error, setError] = useState(null);
const rules = {
minLength: password.length >= 12,
};
const handleSubmit = async (event) => {
event.preventDefault();
if (pending) return;
setPending(true);
try {
const response = await fetch(‘/api/auth/register’, {
method: ‘POST’,
headers: { ‘Content-Type’: ‘application/json’ },
body: JSON.stringify({ email, password }),
});
if (response.ok) {
router.push(‘/check-email’);
} else {
setError(‘Error creating account’);
}
} catch (error) {
setError(‘Error creating account’);
} finally {
setPending(false);
}
};
return (
);
}
### Building the Register User Flow
To build the register user flow, we need to combine the backend and frontend implementations. Here are the 15 key steps to achieve this:
1. Create a Server Endpoint for Registration
Create a new server endpoint to handle the registration request. This endpoint will receive the user’s email and password, create a new user record in the database, and send a verification email to the user’s email address.
2. Implement Email Verification
Create a separate endpoint to handle verification requests. This endpoint will receive the verification token, check if it’s valid, and update the user’s email_verified status accordingly.
3. Create a Users Table Schema
Establish the users table schema to store user data. This includes the email, password hash, email verification status, and created and updated timestamps.
4. Create an Email Verification Tokens Table Schema
Create an email verification tokens table schema to store verification tokens. This includes the user ID, token hash, expiration date, and used status.
5. Implement Partial Index on Unused Tokens
Create a partial index on unused tokens to efficiently lookup unused tokens for a given user.
6. Handle Registration Form Submission
Handle the registration form submission by sending a POST request to the `/api/auth/register` endpoint.
7. Validate User Input
Validate user input by checking if the email and password are provided.
8. Hash Password
Hash the password using a secure hashing algorithm.
9. Create New User Record
Create a new user record in the database with the provided email and password hash.
You may also enjoy reading: "13 Shocking Ways Google's New Deep Research Max Agents Can Access Your Private Data".
10. Create New Verification Token
Create a new verification token and store it in the email verification tokens table.
11. Send Verification Email
Send a verification email to the user’s email address with the verification token.
12. Handle Verification Email Request
Handle the verification email request by checking if the verification token is valid and updating the user’s email verification status accordingly.
13. Update User’s Email Verification Status
Update the user’s email verification status to true after verification.
14. Delete Used Token
Delete the used token from the email verification tokens table.
15. Redirect to Login Page
Redirect the user to the login page after verification.
By following these steps, you can build a robust register user flow in Next.js 15 that handles user registration, email verification, and authentication securely.





