Tutorial: Building Login and Sign-Up Pages with React, FastAPI, and XAMPP (MySQL)
This tutorial guides you through creating a secure, modern authentication system using React for the frontend, FastAPI for the backend, and XAMPP (MySQL) for the database. We'll use Tailwind CSS for responsive styling and JWT for authentication. The content is formatted as an HTML file for your Google blog, with detailed explanations of every component, file, and code segment. All files (package.json
, requirements.txt
, CSS, etc.) are included, and the code is modular, reusable, and beginner-friendly.
Prerequisites
Before starting, ensure you have:
- XAMPP installed with MySQL and Apache running.
- Node.js and npm (v16 or later) for React.
- Python 3.8+ with
pip
for FastAPI. - Basic knowledge of React, Python, and MySQL.
- A code editor (e.g., VS Code).
- Postman or a browser for API testing.
Project Structure
Create a project folder named auth-app/
with the following structure:
auth-app/
├── backend/
│ ├── .env
│ ├── requirements.txt
│ ├── main.py
├── frontend/
│ ├── public/
│ │ ├── index.html
│ ├── src/
│ │ ├── components/
│ │ │ ├── FormInput.jsx
│ │ │ ├── AuthForm.jsx
│ │ ├── pages/
│ │ │ ├── Login.jsx
│ │ │ ├── Signup.jsx
│ │ │ ├── Protected.jsx
│ │ ├── App.js
│ │ ├── index.js
│ │ ├── index.css
│ ├── package.json
│ ├── tailwind.config.js
Step 1: Set Up the Backend with FastAPI and MySQL
We'll create a FastAPI backend to handle user registration and login, storing data in a MySQL database via XAMPP. FastAPI is chosen for its speed, async support, and automatic Swagger documentation.
1.1 Install Python Dependencies
Create a backend/
folder and set up a virtual environment:
mkdir auth-app/backend
cd auth-app/backend
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
Create requirements.txt
to list dependencies:
fastapi==0.115.0
uvicorn==0.30.6
python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4
mysql-connector-python==9.0.0
python-dotenv==1.0.1
Explanation:
fastapi
: Core framework for building the API.uvicorn
: ASGI server to run FastAPI.python-jose[cryptography]
: Generates JWT tokens for authentication.passlib[bcrypt]
: Handles secure password hashing.mysql-connector-python
: Connects to MySQL database.python-dotenv
: Loads environment variables from.env
.
Install dependencies:
pip install -r requirements.txt
1.2 Set Up MySQL Database in XAMPP
- Start XAMPP Control Panel and ensure MySQL and Apache are running.
- Open phpMyAdmin (http://localhost/phpmyadmin).
- Create a database named
auth_db
. - Run the following SQL to create a
users
table:
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Explanation:
id
: Auto-incrementing primary key for each user.username
: Unique field to identify users.password
: Stores hashed passwords (255 characters for bcrypt).created_at
: Tracks when the user was created.
1.3 Create FastAPI Backend Files
1.3.1 .env
File
Store sensitive configuration securely.
DATABASE_HOST=localhost DATABASE_USER=root DATABASE_PASSWORD= DATABASE_NAME=auth_db JWT_SECRET_KEY=your-secure-secret-key-1234567890Explanation:
DATABASE_*
: MySQL connection details (XAMPP defaults:root
user, empty password).JWT_SECRET_KEY
: Secret for signing JWT tokens. Generate a secure key usingopenssl rand -hex 32
in production.
1.3.2 main.py
(FastAPI Application)
Defines API endpoints for sign-up, login, and a protected route.
from fastapi import FastAPI, HTTPException, Depends
from fastapi.security import OAuth2PasswordBearer
from pydantic import BaseModel
from jose import jwt, JWTError
from passlib.context import CryptContext
from mysql.connector import connect, Error
from datetime import datetime, timedelta
from typing import Optional
from dotenv import load_dotenv
import os
import re
load_dotenv()
app = FastAPI()
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login")
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# Database configuration
db_config = {
"host": os.getenv("DATABASE_HOST"),
"user": os.getenv("DATABASE_USER"),
"password": os.getenv("DATABASE_PASSWORD"),
"database": os.getenv("DATABASE_NAME")
}
# JWT configuration
SECRET_KEY = os.getenv("JWT_SECRET_KEY")
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
# Pydantic models
class User(BaseModel):
username: str
password: str
class Token(BaseModel):
access_token: str
token_type: str
# Database connection
def get_db_connection():
return connect(**db_config)
# Password validation
def validate_password(password: str) -> bool:
if len(password) < 6:
return False
if not re.search(r"[A-Za-z]", password) or not re.search(r"[0-9]", password):
return False
return True
# Password hashing
def verify_password(plain_password, hashed_password):
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password):
return pwd_context.hash(password)
# JWT token creation
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
# Get current user from token
async def get_current_user(token: str = Depends(oauth2_scheme)):
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
if username is None:
raise HTTPException(status_code=401, detail="Invalid token")
return username
except JWTError:
raise HTTPException(status_code=401, detail="Invalid token")
@app.post("/signup", response_model=dict)
async def signup(user: User):
if len(user.username) < 3:
raise HTTPException(status_code=400, detail="Username must be at least 3 characters")
if not validate_password(user.password):
raise HTTPException(status_code=400, detail="Password must be at least 6 characters and contain letters and numbers")
try:
conn = get_db_connection()
cursor = conn.cursor()
hashed_password = get_password_hash(user.password)
query = "INSERT INTO users (username, password) VALUES (%s, %s)"
cursor.execute(query, (user.username, hashed_password))
conn.commit()
return {"message": "User created successfully"}
except Error as e:
if "Duplicate entry" in str(e):
raise HTTPException(status_code=400, detail="Username already exists")
raise HTTPException(status_code=500, detail="Database error")
finally:
cursor.close()
conn.close()
@app.post("/login", response_model=Token)
async def login(user: User):
try:
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute("SELECT password FROM users WHERE username = %s", (user.username,))
result = cursor.fetchone()
if not result or not verify_password(user.password, result[0]):
raise HTTPException(status_code=401, detail="Invalid credentials")
access_token = create_access_token(data={"sub": user.username}, expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES))
return {"access_token": access_token, "token_type": "bearer"}
except Error:
raise HTTPException(status_code=500, detail="Database error")
finally:
cursor.close()
conn.close()
@app.get("/protected", response_model=dict)
async def protected(current_user: str = Depends(get_current_user)):
return {"message": f"Hello, {current_user}! This is a protected route."}
Code Explanation:
- Imports: FastAPI for the API,
pydantic
for data validation,jose
for JWT,passlib
for password hashing,mysql-connector-python
for database access, anddotenv
for environment variables. - Pydantic Models:
User
validates incoming JSON;Token
defines the login response. - Database Connection:
get_db_connection
creates a MySQL connection using.env
variables. - Password Handling:
validate_password
enforces strong passwords;get_password_hash
andverify_password
use bcrypt. - JWT:
create_access_token
generates tokens with a 30-minute expiry;get_current_user
decodes tokens. - Endpoints:
/signup
: Validates input, hashes password, stores user in MySQL./login
: Verifies credentials, returns JWT./protected
: Requires valid token, returns personalized message.
- Error Handling: Uses
HTTPException
for clear errors (e.g., duplicate username).
1.4 Run the FastAPI Backend
- Ensure XAMPP's MySQL is running.
- In
auth-app/backend/
, run:
uvicorn main:app --reload
The API runs at http://localhost:8000. Test endpoints at http://localhost:8000/docs (Swagger UI).
1.5 Test the Backend
Use Postman or curl:
- Sign Up:
curl -X POST "http://localhost:8000/signup" -H "Content-Type: application/json" -d '{"username":"testuser","password":"test123"}'
- Login:
curl -X POST "http://localhost:8000/login" -H "Content-Type: application/json" -d '{"username":"testuser","password":"test123"}'
- Protected Route (use token from login):
curl -X GET "http://localhost:8000/protected" -H "Authorization: Bearer <your-token>"
Step 2: Set Up the React Frontend
We'll create a React app with login, sign-up, and protected pages, styled with Tailwind CSS, using Axios for API requests.
2.1 Create the React App
In auth-app/
, create the frontend:
npx create-react-app frontend
cd frontend
npm install axios react-router-dom prop-types
Create package.json
:
{
"name": "frontend",
"version": "0.1.0",
"private": true,
"dependencies": {
"axios": "^1.7.7",
"prop-types": "^15.8.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.26.2"
},
"devDependencies": {
"tailwindcss": "^3.4.13"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
}
}
Explanation:
- Dependencies:
axios
for API calls,react-router-dom
for routing,prop-types
for type checking. - Dev Dependencies:
tailwindcss
for styling. - Scripts: Standard Create React App scripts.
2.2 Set Up Tailwind CSS
- Initialize Tailwind:
npx tailwindcss init
- Create
tailwind.config.js
:
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{js,jsx,ts,tsx}",
],
theme: {
extend: {
colors: {
primary: '#1D4ED8', // Blue for buttons
secondary: '#6B7280', // Gray for text
accent: '#F3F4F6', // Light gray for background
error: '#EF4444', // Red for errors
},
boxShadow: {
'custom': '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
},
animation: {
'fade-in': 'fadeIn 0.5s ease-out',
},
keyframes: {
fadeIn: {
'0%': { opacity: '0', transform: 'translateY(10px)' },
'100%': { opacity: '1', transform: 'translateY(0)' },
},
},
},
},
plugins: [],
}
Explanation:
content
: Scans all JSX files for Tailwind classes.theme.extend
: Adds custom colors, shadow, and fade-in animation.colors
: Definesprimary
(buttons),secondary
(text),accent
(background),error
(error messages).animation
/keyframes
: Creates a smooth form entrance effect.
Create src/index.css
:
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
body {
@apply bg-accent font-sans;
}
input {
@apply w-full p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary text-gray-700;
}
button {
@apply w-full p-3 rounded-lg font-semibold transition duration-300;
}
label {
@apply block text-secondary font-medium mb-1;
}
}
Explanation:
- Directives: Include Tailwind's base, components, and utilities.
- Base Styles:
body
: Light gray background, sans-serif font.input
: Full-width, padded, with rounded borders and blue focus ring.button
: Full-width, padded, with smooth hover transitions.label
: Styled for form readability.
2.3 Create React Components
2.3.1 public/index.html
HTML entry point with Tailwind CSS CDN.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>React Auth App</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body>
<div id="root"></div>
</body>
</html>
Explanation:
- Includes Tailwind CSS via CDN for simplicity (local Tailwind via
tailwind.config.js
). div#root
: React app's mount point.
2.3.2 src/components/FormInput.jsx
Reusable input component for forms.
import PropTypes from 'prop-types';
function FormInput({ label, type, value, onChange, required, error }) {
return (
<div className="mb-4">
<label>{label}</label>
<input
type={type}
value={value}
onChange={onChange}
required={required}
aria-label={label}
className={error ? 'border-error' : ''}
/>
{error && <p className="text-error text-sm mt-1">{error}</p>}
</div>
);
}
FormInput.propTypes = {
label: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
required: PropTypes.bool,
error: PropTypes.string,
};
FormInput.defaultProps = {
required: false,
error: null,
};
export default FormInput;
Explanation:
- Props: Accepts
label
,type
,value
,onChange
,required
,error
. - Styling: Uses Tailwind classes; applies
border-error
for invalid inputs. - Accessibility:
aria-label
for screen readers. - Error Handling: Displays error messages in red.
2.3.3 src/components/AuthForm.jsx
Reusable form component for login and sign-up.
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
function AuthForm({ title, onSubmit, buttonText, linkText, linkTo, message, children }) {
return (
{title}
{linkText} here
{message && {message}
}
);
}
AuthForm.propTypes = {
title: PropTypes.string.isRequired,
onSubmit: PropTypes.func.isRequired,
buttonText: PropTypes.string.isRequired,
linkText: PropTypes.string.isRequired,
linkTo: PropTypes.string.isRequired,
message: PropTypes.string,
children: PropTypes.node.isRequired,
};
export default AuthForm;
Explanation:
- Props: Configures form title, submit handler, button text, navigation link, and error message.
- Styling: Uses
max-w-md
for responsive width,shadow-custom
for depth,animate-fade-in
for entrance. - Structure: Wraps child inputs (
FormInput
) with styled button and link.
2.3.4 src/pages/Login.jsx
Login page with form validation and API integration.
import { useState } from 'react';
import axios from 'axios';
import { useNavigate } from 'react-router-dom';
import AuthForm from '../components/AuthForm';
import FormInput from '../components/FormInput';
function Login() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [errors, setErrors] = useState({ username: '', password: '' });
const [message, setMessage] = useState('');
const navigate = useNavigate();
const validateForm = () => {
let isValid = true;
const newErrors = { username: '', password: '' };
if (username.length < 3) {
newErrors.username = 'Username must be at least 3 characters';
isValid = false;
}
if (password.length < 6) {
newErrors.password = 'Password must be at least 6 characters';
isValid = false;
}
setErrors(newErrors);
return isValid;
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!validateForm()) return;
try {
const response = await axios.post('http://localhost:8000/login', {
username,
password,
});
localStorage.setItem('token', response.data.access_token);
setMessage('Login successful!');
navigate('/protected');
} catch (error) {
setMessage(error.response?.data.detail || 'Login failed');
}
};
return (
setUsername(e.target.value)}
required
error={errors.username}
/>
setPassword(e.target.value)}
required
error={errors.password}
/>
);
}
export default Login;
Explanation:
- State: Manages
username
,password
,errors
, andmessage
. - Validation:
validateForm
checks input lengths. - API Call: Sends credentials to
/login
, stores JWT inlocalStorage
. - Navigation: Redirects to
/protected
on success. - Components: Uses
AuthForm
andFormInput
for modularity.
2.3.5 src/pages/Signup.jsx
Sign-up page with similar functionality.
import { useState } from 'react';
import axios from 'axios';
import { useNavigate } from 'react-router-dom';
import AuthForm from '../components/AuthForm';
import FormInput from '../components/FormInput';
function Signup() {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [errors, setErrors] = useState({ username: '', password: '' });
const [message, setMessage] = useState('');
const navigate = useNavigate();
const validateForm = () => {
let isValid = true;
const newErrors = { username: '', password: '' };
if (username.length < 3) {
newErrors.username = 'Username must be at least 3 characters';
isValid = false;
}
if (password.length < 6) {
newErrors.password = 'Password must be at least 6 characters';
isValid = false;
}
setErrors(newErrors);
return isValid;
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!validateForm()) return;
try {
await axios.post('http://localhost:8000/signup', {
username,
password,
});
setMessage('Sign-up successful! Please log in.');
navigate('/login');
} catch (error) {
setMessage(error.response?.data.detail || 'Sign-up failed');
}
};
return (
setUsername(e.target.value)}
required
error={errors.username}
/>
setPassword(e.target.value)}
required
error={errors.password}
/>
);
}
export default Signup;
Explanation:
- Similar to
Login.jsx
, with validation and API calls to/signup
. - Redirects to
/login
on success.
2.3.6 src/pages/Protected.jsx
Demonstrates authenticated access.
import { useState, useEffect } from 'react';
import axios from 'axios';
import { useNavigate } from 'react-router-dom';
function Protected() {
const [message, setMessage] = useState('');
const navigate = useNavigate();
useEffect(() => {
const token = localStorage.getItem('token');
if (!token) {
navigate('/login');
return;
}
axios.get('http://localhost:8000/protected', {
headers: { Authorization: `Bearer ${token}` },
})
.then((response) => setMessage(response.data.message))
.catch(() => {
localStorage.removeItem('token');
navigate('/login');
});
}, [navigate]);
return (
Protected Page
{message}
);
}
export default Protected;
Explanation:
- Effect: Checks for token in
localStorage
, fetches protected route. - Authentication: Sends JWT in
Authorization
header. - Logout: Clears token, redirects to
/login
.
2.3.7 src/App.js
Sets up routing.
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Login from './pages/Login';
import Signup from './pages/Signup';
import Protected from './pages/Protected';
function App() {
return (
} />
} />
} />
} />
);
}
export default App;
Explanation:
- Uses
react-router-dom
for client-side routing. - Centers content with Tailwind's
flex items-center justify-center
. - Routes to
/login
,/signup
,/protected
, with/
redirecting to/login
.
2.3.8 src/index.js
Entry point for React.
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
);
Explanation:
- Renders
App
intodiv#root
. - Includes
index.css
for Tailwind styles. - Uses
StrictMode
for development checks.
Step 3: Test the Application
- Start the Backend:
In
auth-app/backend/
:uvicorn main:app --reload
Ensure XAMPP's MySQL is running.
- Start the Frontend:
In
auth-app/frontend/
:npm start
Open http://localhost:3000.
- Test Flow:
- Sign Up: Go to
/signup
, enter username (3+ characters) and password (6+ characters, letters, numbers). Redirects to/login
. - Login: At
/login
, use same credentials. Redirects to/protected
. - Protected Page: Displays greeting; click "Logout" to return to
/login
. - Errors: Invalid inputs show red error messages.
- Sign Up: Go to
- Database Check:
In phpMyAdmin, verify
users
table inauth_db
has hashed passwords and timestamps.
Step 4: Enhance Security
- JWT Secret: Use a strong
JWT_SECRET_KEY
in.env
. - CORS: Add to
main.py
:from fastapi.middleware.cors import CORSMiddleware app.add_middleware(CORSMiddleware, allow_origins=["http://localhost:3000"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
- Password Policy: Enhanced in
main.py
with regex. - HTTPS: Use in production (e.g., Nginx reverse proxy).
- Rate Limiting: Install
slowapi
:pip install slowapi
Step 5: Styling Details
- Responsive Design: Forms use
max-w-md mx-auto
for mobile-friendly layout. - Custom Colors:
primary
(blue),secondary
(gray),accent
(light gray),error
(red). - Animations:
animate-fade-in
for smooth entrance. - Accessibility: Inputs have
aria-label
, focus states usefocus:ring-primary
. - Hover Effects: Buttons change color (
hover:bg-blue-600
,hover:bg-red-600
).
Extensions:
- Add loading spinner with
animate-spin
. - Use
dark:
classes for dark mode. - Customize fonts in
tailwind.config.js
(e.g.,fontFamily: { sans: ['Inter', 'sans-serif'] }
).
Step 6: Deployment
- Backend:
- Use
gunicorn
withuvicorn
:gunicorn -k uvicorn.workers.UvicornWorker main:app
. - Host on Heroku, AWS, or DigitalOcean.
- Use managed MySQL (e.g., AWS RDS).
- Use
- Frontend:
- Build:
npm run build
. - Serve with Nginx or static hosts (Netlify, Vercel).
- Build:
- CORS: Update
allow_origins
for production domain. - Environment: Use platform-specific environment variable management.
Conclusion
You've built a secure, responsive authentication system with React, FastAPI, and MySQL (XAMPP). The app features modular React components, Tailwind CSS styling, and a robust FastAPI backend with JWT authentication.
Test the API at http://localhost:8000/docs and share this tutorial to inspire others!
Comments
Post a Comment