Back to blogs

How I Built a Simple Authentication System with React Node js and PostgreSQL

June 7, 2025
6 min read
How I Built a Simple Authentication System with React Node js and PostgreSQL

Hi everyone! If you’re a developer looking to build your own authentication system from scratch, you’re in the right place. In this post, I’ll walk you through how I built a simple but secure authentication system using React for the frontend, Node.js and Express for the backend, and PostgreSQL for the database. I’ll also reference my GitHub repository throughout, so you can follow along with the actual code.


Why Build Your Own Authentication System?


Before we dive in, you might wonder why not just use an existing service like Firebase or Auth0. While those are solid choices, building your own system helps you understand the fundamentals of authentication—things like password hashing, JSON Web Tokens (JWT), and secure API design. It also gives you full control over user management and customization.

If you’re just getting started or want to gain a deeper understanding of how authentication works under the hood, this project is a great learning opportunity.


The Tech Stack


Here’s a quick look at the technologies used in this project:

  1. React: For building the frontend UI
  2. Node.js: To run JavaScript on the server
  3. Express: A lightweight framework for building APIs
  4. PostgreSQL: Our relational database
  5. bcryptjs: For password hashing
  6. jsonwebtoken: For generating and verifying JWTs


1. Setting Up the Backend


Start by setting up the backend. Create a new folder and initialize a Node.js project:

mkdir auth-system
cd auth-system
npm init -y


Install the required dependencies:


npm install express pg bcryptjs jsonwebtoken dotenv cors


These libraries will help you create the API, connect to PostgreSQL, hash passwords, handle authentication, and manage environment variables.


Connecting to PostgreSQL


Create a db.js file for the database connection:


const { Pool } = require('pg');
require('dotenv').config();

const pool = new Pool({
user: process.env.DB_USER,
host: process.env.DB_HOST,
database: process.env.DB_NAME,
password: process.env.DB_PASSWORD,
port: process.env.DB_PORT,
});

module.exports = pool;


Add your database credentials to a .env file:


DB_USER=your_username
DB_HOST=localhost
DB_NAME=auth_system
DB_PASSWORD=your_password
DB_PORT=5432
JWT_SECRET=your_jwt_secret


Creating the Users Table


Run the following SQL command in your database to create the users table:

CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);


2. Handling User Registration


Create a POST endpoint to handle user sign-ups:

const express = require('express');
const bcrypt = require('bcryptjs');
const pool = require('./db');
const cors = require('cors');
const app = express();

app.use(express.json());
app.use(cors());

app.post('/api/auth/register', async (req, res) => {
const { email, password } = req.body;
try {
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash(password, salt);
const result = await pool.query(
'INSERT INTO users (email, password_hash) VALUES ($1, $2) RETURNING *',
[email, hashedPassword]
);
res.status(201).json(result.rows[0]);
} catch (err) {
res.status(500).json({ error: err.message });
}
});


3. Handling User Login


Create another POST endpoint to verify credentials and issue a JWT:

const jwt = require('jsonwebtoken');

app.post('/api/auth/login', async (req, res) => {
const { email, password } = req.body;
try {
const result = await pool.query('SELECT * FROM users WHERE email = $1', [email]);
if (result.rows.length === 0) {
return res.status(400).json({ error: 'User not found' });
}
const user = result.rows[0];
const isValid = await bcrypt.compare(password, user.password_hash);
if (!isValid) {
return res.status(400).json({ error: 'Invalid password' });
}
const token = jwt.sign({ id: user.id }, process.env.JWT_SECRET, { expiresIn: '1h' });
res.json({ token, user: { id: user.id, email: user.email } });
} catch (err) {
res.status(500).json({ error: err.message });
}
});


4. Protecting Routes


Add middleware to validate JWTs for protected routes:

const authenticate = (req, res, next) => {
const token = req.header('Authorization')?.replace('Bearer ', '');
if (!token) return res.status(401).send('Access denied');

try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (err) {
res.status(400).send('Invalid token');
}
};

app.get('/api/profile', authenticate, async (req, res) => {
try {
const result = await pool.query('SELECT * FROM users WHERE id = $1', [req.user.id]);
res.json(result.rows[0]);
} catch (err) {
res.status(500).json({ error: err.message });
}
});


5. Building the Frontend with React


Create a React frontend using Vite:

npm create vite@latest frontend -- --template react-ts
cd frontend
npm install axios react-router-dom


Here’s a simple login component example:


import React, { useState } from 'react';
import axios from 'axios';

const Login = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
const response = await axios.post('http://localhost:3001/api/auth/login', {
email,
password,
});
localStorage.setItem('token', response.data.token);
// Navigate or update UI as needed
} catch {
setError('Login failed. Please try again.');
}
};

return (
<form onSubmit={handleSubmit}>
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" />
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Password" />
<button type="submit">Login</button>
{error && <p>{error}</p>}
</form>
);
};

export default Login;


6. Protecting Frontend Routes


Use a wrapper component to guard private routes:

import { Navigate } from 'react-router-dom';

const PrivateRoute = ({ children }: { children: JSX.Element }) => {
const token = localStorage.getItem('token');
return token ? children : <Navigate to="/login" />;
};


Then use it in your route definitions:

<Route path="/profile" element={<PrivateRoute><Profile /></PrivateRoute>} />


7. Testing the System


  1. Register a new user using the register form.
  2. Log in with the new credentials.
  3. Attempt to access protected routes like /profile and see if they are blocked without a token.


8. Security Tips


  1. Always hash passwords before storing them.
  2. Use HTTPS in production to encrypt network traffic.
  3. Set expiration times on JWTs to reduce security risks.
  4. Validate and sanitize user input.
  5. Keep secret keys in environment variables, not in your code.


9. What to Build Next


Once the basics are done, you can add:

  1. Password reset functionality
  2. Email verification
  3. Social login (Google, Facebook)
  4. Role-based access (admin, user, etc.)


10. Final Thoughts


Building an authentication system from scratch helps you truly understand how user authentication works. It's more than just writing code—it's about understanding how to protect user data, manage sessions, and structure secure applications.

Check out the full source code on GitHub. I’ve done my best to keep it clean and beginner-friendly. If you have questions or suggestions, feel free to open an issue or connect with me there.

Thanks for reading and good luck with your project.

react authenticationnode authenticationpostgres authenticationauthentication systemauth system tutorialreact node postgres authenticationhow to build authentication systemsimple auth systemsecure login systemreact express postgres authenticationauthentication for web appsfullstack authenticationreact node authentication guideauthentication system for beginnersstep by step auth system