React + Node.js + Express User Registration, Login, and Dashboard
Goal of This Demo
- ★ In this demo, we will build a modern full-stack login system.
- ★ The React frontend will display pages such as Register, Login, and Dashboard.
- ★ The Node.js + Express backend will handle requests from React.
- ★ The MySQL database will store user accounts.
What Students Will Build
- ★ A Register page to create a new account.
- ★ A Login page to sign in.
- ★ A Dashboard page to welcome the logged-in user.
- ★ React routes so each page appears separately.
Project Structure
- ★ We will use two folders: one for the frontend and one for the backend.
login-demo/
client/
src/
App.jsx
Register.jsx
Login.jsx
Dashboard.jsx
main.jsx
server/
index.js
package.json
How the Modern Stack Works
| Layer |
Technology |
Job |
| Frontend |
React |
Displays Register, Login, and Dashboard pages |
| Backend |
Node.js + Express |
Handles registration and login requests |
| Database |
MySQL |
Stores usernames and hashed passwords |
Step 1: Create the React Frontend
- ★ Open a terminal and create a React project using Vite.
npm create vite@latest client
cd client
npm install
npm install react-router-dom
- ★ react-router-dom allows us to create separate pages like Register, Login, and Dashboard.
Step 2: Create the Node.js + Express Backend
- ★ In a separate terminal, create the server folder and install the required packages.
mkdir server
cd server
npm init -y
npm install express mysql2 bcrypt cors
- ★ express builds the backend server.
- ★ mysql2 connects Node.js to MySQL.
- ★ bcrypt hashes passwords securely.
- ★ cors lets the React app call the Express server during development.
Step 3: Create the Database
- ★ In phpMyAdmin or MySQL Workbench, create a database and a table for users.
SQL:
CREATE DATABASE login_demo;
USE login_demo;
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL
);
- ★ The username must be unique.
- ★ The password column stores a hashed password, not plain text.
Step 4: Create server/index.js
- ★ This file creates the Express server, connects to MySQL, and defines the registration and login routes.
server/index.js:
const express = require('express');
const mysql = require('mysql2');
const bcrypt = require('bcrypt');
const cors = require('cors');
const app = express();
app.use(cors());
app.use(express.json());
const db = mysql.createConnection({
host: 'localhost',
user: 'root',
password: '',
database: 'login_demo'
});
db.connect((err) => {
if (err) {
console.error('Database connection failed:', err.message);
return;
}
console.log('Connected to MySQL');
});
app.post('/api/register', async (req, res) => {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ message: 'Username and password are required.' });
}
try {
const hashedPassword = await bcrypt.hash(password, 10);
db.query(
'INSERT INTO users (username, password) VALUES (?, ?)',
[username, hashedPassword],
(err, result) => {
if (err) {
if (err.code === 'ER_DUP_ENTRY') {
return res.status(400).json({ message: 'Username already taken.' });
}
return res.status(500).json({ message: err.message });
}
res.json({ message: 'Registered successfully!' });
}
);
} catch (error) {
res.status(500).json({ message: error.message });
}
});
app.post('/api/login', (req, res) => {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ message: 'Username and password are required.' });
}
db.query(
'SELECT id, username, password FROM users WHERE username = ?',
[username],
async (err, results) => {
if (err) {
return res.status(500).json({ message: err.message });
}
if (results.length === 0) {
return res.status(400).json({ message: 'Invalid username or password.' });
}
const user = results[0];
try {
const match = await bcrypt.compare(password, user.password);
if (!match) {
return res.status(400).json({ message: 'Invalid username or password.' });
}
res.json({
message: 'Login successful!',
user: {
id: user.id,
username: user.username
}
});
} catch (error) {
res.status(500).json({ message: error.message });
}
}
);
});
app.listen(3000, () => {
console.log('Server running at http://localhost:3000');
});
Important: Restart the Server
- ★ When you change server/index.js, Node.js does NOT automatically reload.
- ★ You must stop and restart the server.
Ctrl + C
node index.js
Step 5: Create App.jsx
- ★ App.jsx defines the React routes.
- ★ This is why Register, Login, and Dashboard appear on different pages.
client/src/App.jsx:
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Register from './Register';
import Login from './Login';
import Dashboard from './Dashboard';
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</BrowserRouter>
);
}
export default App;
Step 6: Create Register.jsx
- ★ This page collects a username and password and sends them to the backend.
client/src/Register.jsx:
import { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
function Register() {
const [formData, setFormData] = useState({
username: '',
password: ''
});
const [message, setMessage] = useState('');
const navigate = useNavigate();
const handleChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value
});
};
const handleSubmit = async (e) => {
e.preventDefault();
try {
const response = await fetch('http://localhost:3000/api/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
const data = await response.json();
setMessage(data.message);
if (response.ok) {
setTimeout(() => {
navigate('/');
}, 1000);
}
} catch (error) {
setMessage('Error connecting to server.');
}
};
return (
<div style={{ padding: '30px', fontFamily: 'Arial' }}>
<h1>Register</h1>
<form onSubmit={handleSubmit}>
<div>
<label>Username: </label>
<input
type="text"
name="username"
value={formData.username}
onChange={handleChange}
required
/>
</div>
<br />
<div>
<label>Password: </label>
<input
type="password"
name="password"
value={formData.password}
onChange={handleChange}
required
/>
</div>
<br />
<button type="submit">Register</button>
</form>
<p>{message}</p>
<p>
Already have an account? <Link to="/">Login here</Link>
</p>
</div>
);
}
export default Register;
Step 7: Create Login.jsx
- ★ This page sends the username and password to the login route.
- ★ If login is successful, the user is saved in localStorage and redirected to the Dashboard page.
client/src/Login.jsx:
import { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
function Login() {
const [formData, setFormData] = useState({
username: '',
password: ''
});
const [message, setMessage] = useState('');
const navigate = useNavigate();
const handleChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value
});
};
const handleSubmit = async (e) => {
e.preventDefault();
try {
const response = await fetch('http://localhost:3000/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
});
const data = await response.json();
setMessage(data.message);
if (response.ok && data.user) {
localStorage.setItem('user', JSON.stringify(data.user));
navigate('/dashboard');
}
} catch (error) {
setMessage('Error connecting to server.');
}
};
return (
<div style={{ padding: '30px', fontFamily: 'Arial' }}>
<h1>Login</h1>
<form onSubmit={handleSubmit}>
<div>
<label>Username: </label>
<input
type="text"
name="username"
value={formData.username}
onChange={handleChange}
required
/>
</div>
<br />
<div>
<label>Password: </label>
<input
type="password"
name="password"
value={formData.password}
onChange={handleChange}
required
/>
</div>
<br />
<button type="submit">Login</button>
</form>
<p>{message}</p>
<p>
Don't have an account? <Link to="/register">Register here</Link>
</p>
</div>
);
}
export default Login;
Step 8: Create Dashboard.jsx
- ★ This page checks whether a user is stored in localStorage.
- ★ If no user exists, React sends the visitor back to the Login page.
client/src/Dashboard.jsx:
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
function Dashboard() {
const [user, setUser] = useState(null);
const navigate = useNavigate();
useEffect(() => {
const storedUser = localStorage.getItem('user');
if (!storedUser) {
navigate('/');
return;
}
setUser(JSON.parse(storedUser));
}, [navigate]);
const handleLogout = () => {
localStorage.removeItem('user');
navigate('/');
};
return (
<div style={{ padding: '30px', fontFamily: 'Arial' }}>
<h1>Dashboard</h1>
{user ? (
<>
<p>Welcome, {user.username}! You are logged in.</p>
<button onClick={handleLogout}>Logout</button>
</>
) : (
<p>Loading...</p>
)}
</div>
);
}
export default Dashboard;
Step 9: Run the Backend
- ★ Open the server terminal and run:
node index.js
Connected to MySQL
Server running at http://localhost:3000
Step 11: Run the React Frontend
- ★ Open the client terminal and run:
npm run dev
- ★ Vite usually gives a URL such as http://localhost:5173.
- ★ Open that URL in your browser.
How the Full Login Flow Works
- ★ User opens the React app.
- ★ User clicks Register and creates a new account.
- ★ React sends the form data to /api/register.
- ★ Express hashes the password and stores the user in MySQL.
- ★ User goes to the Login page and signs in.
- ★ React sends the login data to /api/login.
- ★ Express checks the username and compares the hashed password.
- ★ If correct, Express returns success JSON.
- ★ React stores the user in localStorage and sends the user to the Dashboard page.
Diagram: React ↔ Express ↔ MySQL
| Step |
What Happens |
| 1 |
Register.jsx or Login.jsx sends a request using fetch() |
| 2 |
Express receives the request at /api/register or /api/login |
| 3 |
Express runs SQL queries on MySQL |
| 4 |
MySQL returns the results |
| 5 |
Express sends JSON back to React |
| 6 |
React updates the page or redirects the user |
React Page
→
Express API
→
MySQL Database
Dashboard or Message
←
JSON Response
←
Query Result
Why CORS is Needed Here
- ★ During development, React and Express often run on different ports.
- ★ Example: React at http://localhost:5173 and Express at http://localhost:3000.
- ★ Since they are different origins, the browser requires CORS.
- ★ That is why we use app.use(cors()) in server/index.js.
Important Note About Security
- ★ This classroom demo is for learning how React, Express, and MySQL work together.
- ★ In a real production system, we would also learn sessions or JWT, protected backend routes, and stronger validation.
- ★ For now, this demo focuses on the basic full-stack flow.
Summary
- ★ React creates separate pages using React Router.
- ★ Express handles registration and login API routes.
- ★ MySQL stores usernames and hashed passwords.
- ★ React uses fetch() to communicate with Express.
- ★ Together, they form a modern full-stack web application.