PERN Auth Guide | © @devarthastudio
PERN Stack
Authentication System
A Beginner-Friendly Step-by-Step Guide
Stack Used
PostgreSQL • [Link] • React • [Link]
© 2025 @devarthastudio
All rights reserved. Unauthorized reproduction prohibited.
Page 1
PERN Auth Guide | © @devarthastudio
What Is This Guide About?
This guide will teach you, step-by-step, how to build a Login & Signup system using the PERN
stack. If you've never written a single line of code — don't worry. Every step is explained in plain
English with examples.
🎯 What You'll Build: A full-stack web app where users can Sign Up, Log In, and access a
protected page — all with passwords stored safely.
What is PERN?
PERN is just a fancy word for 4 tools that work together to build web apps:
Letter Stands For What It Does (Simple Words)
P PostgreSQL The database — stores all your user info (like a super-smart
Excel sheet)
E [Link] The backend server — handles requests from the browser (like a
restaurant waiter)
R React The frontend — what the user sees on screen (buttons, forms,
pages)
N [Link] Runs JavaScript on the server — powers [Link] behind the
scenes
Part 1 — Setting Up Your Computer
Before writing any code, you need to install a few free tools. Think of this like getting your art
supplies before painting.
1 Install [Link]
1. Open your browser and go to: [Link]
2. Download the LTS version (the green button — it says 'Recommended')
3. Run the installer and click Next until it finishes
4. To check if it worked, open your Terminal (Mac) or Command Prompt (Windows) and type:
node -v
npm -v
Page 2
PERN Auth Guide | © @devarthastudio
✅ Success: You should see version numbers like v20.x.x — that means [Link] is
installed!
2 Install PostgreSQL
5. Go to: [Link]
6. Choose your operating system (Windows / Mac)
7. Download and install. During setup, set a password — remember it! (Example:
mypassword123)
8. Also install pgAdmin (it usually comes with PostgreSQL) — this is a visual tool to see your
database
📝 Write It Down: Your PostgreSQL password is very important. Write it somewhere safe.
You'll need it later.
3 Install VS Code (Code Editor)
9. Go to: [Link]
10. Download and install — it's free!
11. This is where you'll write all your code. Think of it as Microsoft Word, but for code.
Part 2 — Creating the Project
Now let's create the folder structure for our app. Our project will have two main parts:
• server — This is the backend (Express + [Link] + PostgreSQL)
• client — This is the frontend (React)
4 Create the Project Folders
Open your Terminal or Command Prompt and type these commands one by one:
mkdir pern-auth
cd pern-auth
mkdir server
mkdir client
Page 3
PERN Auth Guide | © @devarthastudio
💡 What just happened: mkdir creates a new folder. cd means 'change directory' — it
moves you into that folder.
5 Set Up the Backend (Server)
Type these commands to enter the server folder and set it up:
cd server
npm init -y
npm install express pg bcrypt jsonwebtoken dotenv cors
Here's what each package does:
Package What It Does
express Creates the server — handles routes like /login and /register
pg Connects [Link] to PostgreSQL database
bcrypt Scrambles passwords so they're stored safely (hashing)
jsonwebtoken Creates a 'token' — like a VIP pass — that proves you're logged in
dotenv Loads secret values (like passwords) from a .env file
cors Allows the React frontend to talk to the Express backend
Part 3 — Setting Up the Database
Now let's create the database where we'll store user accounts.
6 Create the Database in pgAdmin
12. Open pgAdmin (search it in your Start Menu or Applications)
13. Login with the password you set during PostgreSQL install
14. Right-click on Databases → Create → Database
15. Name it: pern_auth_db → Click Save
7 Create the Users Table
In pgAdmin, click on pern_auth_db → go to the Query Tool (the little SQL icon) and run this:
Page 4
PERN Auth Guide | © @devarthastudio
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(150) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
Breaking it down in plain English:
• id — auto-number for each user (1, 2, 3...)
• name — user's full name
• email — must be unique (no two users with the same email)
• password — will store the scrambled (hashed) password
• created_at — automatically records when the account was made
Part 4 — Writing the Backend Code
Now we write the actual server code. Open VS Code, open your server folder.
8 Create the .env File
Inside the server folder, create a new file called .env (just that name, nothing else). This file holds
your secrets:
DB_USER=postgres
DB_HOST=localhost
DB_NAME=pern_auth_db
DB_PASSWORD=yourpassword123
DB_PORT=5432
JWT_SECRET=mysupersecretkey_changethis
PORT=5000
⚠️ Important: Replace yourpassword123 with your actual PostgreSQL password. Never
share this file with anyone!
9 Create the Database Connection File
Create a file: server/[Link]
Page 5
PERN Auth Guide | © @devarthastudio
// [Link] — connects our app to PostgreSQL
const { Pool } = require("pg");
require("dotenv").config();
const pool = new Pool({
user: [Link].DB_USER,
host: [Link].DB_HOST,
database: [Link].DB_NAME,
password: [Link].DB_PASSWORD,
port: [Link].DB_PORT,
});
[Link] = pool;
💡 What is this: Pool is a connection manager. Think of it as a phone line between your app
and the database.
10 Create the Main Server File
Create a file: server/[Link]
// [Link] — the main server entry point
const express = require("express");
const cors = require("cors");
require("dotenv").config();
const app = express();
// Middleware
[Link](cors());
[Link]([Link]()); // allows reading JSON from requests
// Routes
[Link]("/api/auth", require("./routes/auth"));
const PORT = [Link] || 5000;
[Link](PORT, () => {
[Link](`Server running on port ${PORT}`);
});
11 Create Auth Routes
Page 6
PERN Auth Guide | © @devarthastudio
Create a folder: server/routes — then create server/routes/[Link]
// routes/[Link] — handles /register and /login
const express = require("express");
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
const pool = require("../db");
const router = [Link]();
// ─── REGISTER ──────────────────────────────────────────
[Link]("/register", async (req, res) => {
try {
const { name, email, password } = [Link];
// Check if user already exists
const exists = await [Link](
"SELECT * FROM users WHERE email = $1", [email]
);
if ([Link] > 0) {
return [Link](400).json({ message: "Email already registered" });
}
// Hash (scramble) the password — never store plain text!
const saltRounds = 10;
const hashedPassword = await [Link](password, saltRounds);
// Save user to database
const newUser = await [Link](
"INSERT INTO users (name, email, password) VALUES ($1, $2, $3)
RETURNING id, name, email",
[name, email, hashedPassword]
);
[Link](201).json({ message: "User created!", user:
[Link][0] });
} catch (err) {
[Link](err);
[Link](500).json({ message: "Server error" });
}
});
// ─── LOGIN ─────────────────────────────────────────────
[Link]("/login", async (req, res) => {
try {
const { email, password } = [Link];
Page 7
PERN Auth Guide | © @devarthastudio
// Find user by email
const userResult = await [Link](
"SELECT * FROM users WHERE email = $1", [email]
);
if ([Link] === 0) {
return [Link](400).json({ message: "Invalid credentials" });
}
const user = [Link][0];
// Compare entered password with hashed password
const isMatch = await [Link](password, [Link]);
if (!isMatch) {
return [Link](400).json({ message: "Invalid credentials" });
}
// Create a JWT token (VIP pass)
const token = [Link](
{ id: [Link], email: [Link] },
[Link].JWT_SECRET,
{ expiresIn: "1h" }
);
[Link]({ message: "Login successful!", token, name: [Link] });
} catch (err) {
[Link](err);
[Link](500).json({ message: "Server error" });
}
});
[Link] = router;
🔐 How Security Works Here: The password is hashed using bcrypt (one-way scramble).
Even if someone steals your database, they can't read the passwords. JWT gives the user a
token after login — like a key card.
12 Create Middleware to Protect Routes
Create: server/middleware/[Link] — This checks if a user is logged in before allowing them access.
// middleware/[Link]
const jwt = require("jsonwebtoken");
Page 8
PERN Auth Guide | © @devarthastudio
require("dotenv").config();
[Link] = (req, res, next) => {
const authHeader = [Link]["authorization"];
const token = authHeader && [Link](" ")[1]; // format: Bearer
TOKEN
if (!token) {
return [Link](401).json({ message: "No token. Access denied." });
}
try {
const decoded = [Link](token, [Link].JWT_SECRET);
[Link] = decoded; // attach user info to request
next(); // move to the next step
} catch (err) {
[Link](403).json({ message: "Invalid or expired token." });
}
};
Now create: server/routes/[Link] — a route only logged-in users can access:
// routes/[Link]
const express = require("express");
const authMiddleware = require("../middleware/auth");
const router = [Link]();
[Link]("/dashboard", authMiddleware, (req, res) => {
[Link]({ message: `Welcome back, user ${[Link]}!` });
});
[Link] = router;
Also add this line to [Link] (just below the auth route):
[Link]("/api/protected", require("./routes/protected"));
Part 5 — Building the React Frontend
Now let's create the part users actually see! Go back to the main pern-auth folder in your terminal.
13 Create the React App
Page 9
PERN Auth Guide | © @devarthastudio
cd ../client
npm create vite@latest . -- --template react
npm install
npm install axios react-router-dom
💡 What is Axios: Axios is a tool that lets React send requests to your Express server —
like a messenger between frontend and backend.
14 Create the Signup Page
Create: client/src/pages/[Link]
// [Link]
import { useState } from "react";
import axios from "axios";
import { useNavigate } from "react-router-dom";
export default function Signup() {
const [form, setForm] = useState({ name: "", email: "", password: "" });
const [msg, setMsg] = useState("");
const navigate = useNavigate();
const handleChange = (e) => {
setForm({ ...form, [[Link]]: [Link] });
};
const handleSubmit = async (e) => {
[Link]();
try {
const res = await
[Link]("[Link] form);
setMsg([Link]);
navigate("/login"); // redirect to login after signup
} catch (err) {
setMsg([Link]?.data?.message || "Something went wrong");
}
};
return (
<div style={{ maxWidth: 400, margin: "80px auto", padding: 24 }}>
<h2>Create an Account</h2>
{msg && <p style={{ color: "red" }}>{msg}</p>}
<form onSubmit={handleSubmit}>
Page 10
PERN Auth Guide | © @devarthastudio
<input name="name" placeholder="Full Name"
onChange={handleChange} required /><br/>
<input name="email" placeholder="Email"
onChange={handleChange} required /><br/>
<input name="password" placeholder="Password" type="password"
onChange={handleChange} required /><br/>
<button type="submit">Sign Up</button>
</form>
</div>
);
}
15 Create the Login Page
Create: client/src/pages/[Link]
// [Link]
import { useState } from "react";
import axios from "axios";
import { useNavigate } from "react-router-dom";
export default function Login() {
const [form, setForm] = useState({ email: "", password: "" });
const [msg, setMsg] = useState("");
const navigate = useNavigate();
const handleChange = (e) => {
setForm({ ...form, [[Link]]: [Link] });
};
const handleSubmit = async (e) => {
[Link]();
try {
const res = await [Link]("[Link]
form);
[Link]("token", [Link]); // save the VIP pass
[Link]("name", [Link]);
navigate("/dashboard"); // go to protected page
} catch (err) {
setMsg([Link]?.data?.message || "Something went wrong");
}
};
return (
<div style={{ maxWidth: 400, margin: "80px auto", padding: 24 }}>
Page 11
PERN Auth Guide | © @devarthastudio
<h2>Login</h2>
{msg && <p style={{ color: "red" }}>{msg}</p>}
<form onSubmit={handleSubmit}>
<input name="email" placeholder="Email"
onChange={handleChange} required /><br/>
<input name="password" placeholder="Password" type="password"
onChange={handleChange} required /><br/>
<button type="submit">Login</button>
</form>
</div>
);
}
16 Create the Dashboard (Protected Page)
Create: client/src/pages/[Link] — Only accessible after login.
// [Link]
import { useEffect, useState } from "react";
import axios from "axios";
import { useNavigate } from "react-router-dom";
export default function Dashboard() {
const [message, setMessage] = useState("");
const name = [Link]("name");
const navigate = useNavigate();
useEffect(() => {
const fetchData = async () => {
try {
const token = [Link]("token");
const res = await [Link](
"[Link]
{ headers: { Authorization: `Bearer ${token}` } }
);
setMessage([Link]);
} catch (err) {
navigate("/login"); // not logged in? send them back
}
};
fetchData();
}, []);
const logout = () => {
[Link]("token");
Page 12
PERN Auth Guide | © @devarthastudio
[Link]("name");
navigate("/login");
};
return (
<div style={{ maxWidth: 600, margin: "80px auto", padding: 24 }}>
<h2>Dashboard</h2>
<p>Hello, {name}! You are logged in.</p>
<p>{message}</p>
<button onClick={logout}>Logout</button>
</div>
);
}
17 Set Up React Router (Navigation)
Replace everything in client/src/[Link] with this:
// [Link]
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import Signup from "./pages/Signup";
import Login from "./pages/Login";
import Dashboard from "./pages/Dashboard";
export default function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Navigate to="/login" />} />
<Route path="/signup" element={<Signup />} />
<Route path="/login" element={<Login />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</BrowserRouter>
);
}
Part 6 — Running Your App
Almost there! Now let's start both the server and the React app.
18 Start the Backend Server
Page 13
PERN Auth Guide | © @devarthastudio
Open a Terminal window, go to the server folder, and run:
cd pern-auth/server
node [Link]
✅ Expected Output: Server running on port 5000 — this means your backend is live!
19 Start the React Frontend
Open a SECOND Terminal window (don't close the first one), go to the client folder:
cd pern-auth/client
npm run dev
🌐 Open Your Browser: Go to [Link] — you should see your app!
Test the app like this:
16. Go to /signup → Fill in your name, email, password → Click Sign Up
17. You'll be redirected to /login → Enter the same email + password → Click Login
18. You'll land on the /dashboard page — you are now authenticated!
19. Click Logout → You'll be sent back to /login
Part 7 — Final Folder Structure
Here is what your complete project should look like:
pern-auth/
├── server/
│ ├── middleware/
│ │ └── [Link] ← JWT check middleware
│ ├── routes/
│ │ ├── [Link] ← /register and /login
│ │ └── [Link] ← /dashboard (protected)
│ ├── [Link] ← PostgreSQL connection
│ ├── [Link] ← Main server file
│ └── .env ← Secret keys (never share!)
│
└── client/
Page 14
PERN Auth Guide | © @devarthastudio
└── src/
├── pages/
│ ├── [Link]
│ ├── [Link]
│ └── [Link]
└── [Link] ← Routes setup
Part 8 — How It All Works Together
Here's the full flow in plain English:
1 User fills the Signup form in React and clicks Submit
2 React sends the data (name, email, password) to Express server at POST
/api/auth/register
3 Express checks if the email exists in PostgreSQL. If yes → error. If no → continue.
4 bcrypt scrambles the password (e.g. mypassword → $2b$10$xyz...) and saves the user
to the database
5 User now goes to Login. React sends email + password to POST /api/auth/login
6 Express finds the user in the database, compares the password using bcrypt
7 If password matches, Express creates a JWT token and sends it back to React
8 React stores the token in localStorage. Future requests include this token in the headers
9 When accessing /dashboard, React sends the token to Express. Middleware checks it —
if valid, access granted!
Part 9 — Common Errors & Fixes
Error What To Do
Cannot connect to database Check your .env file — make sure DB_PASSWORD
matches what you set in PostgreSQL
CORS error in browser Make sure [Link](cors()) is in [Link] before your routes
Token is not defined You forgot to login first, or the token expired (1 hour limit).
Log in again.
Port 5000 already in use Change PORT=5000 to PORT=5001 in .env and update
axios URLs in React
Page 15
PERN Auth Guide | © @devarthastudio
Module not found error You forgot to run npm install in that folder. Go to the folder
and run it.
Password is correct but login fails Make sure you're using [Link]() — never
compare plain passwords directly
Part 10 — What's Next?
Congrats! You've built a full authentication system from scratch. Here's what you can add next:
• Email verification — send a confirmation email when someone signs up
• Forgot Password — let users reset their password via email link
• Refresh Tokens — JWT tokens that auto-renew so users stay logged in longer
• Role-Based Access — admins see different pages than regular users
• Rate Limiting — prevent bots from trying 1000 passwords per second
• React Context API — manage the auth state globally instead of localStorage
🚀 Keep Going: Every big web app you use — Instagram, YouTube, Amazon — has an
auth system like this at its core. You just built the same thing!
© 2025 @devarthastudio — All Rights Reserved
This document is the intellectual property of @devarthastudio.
Unauthorized reproduction, redistribution, or resale is strictly prohibited.
Page 16