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

  1. Start XAMPP Control Panel and ensure MySQL and Apache are running.
  2. Open phpMyAdmin (http://localhost/phpmyadmin).
  3. Create a database named auth_db.
  4. 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-1234567890

Explanation:

  • DATABASE_*: MySQL connection details (XAMPP defaults: root user, empty password).
  • JWT_SECRET_KEY: Secret for signing JWT tokens. Generate a secure key using openssl 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, and dotenv 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 and verify_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

  1. Ensure XAMPP's MySQL is running.
  2. 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

  1. Initialize Tailwind:
    npx tailwindcss init
  2. 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: Defines primary (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}

{children}

{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, and message.
  • Validation: validateForm checks input lengths.
  • API Call: Sends credentials to /login, stores JWT in localStorage.
  • Navigation: Redirects to /protected on success.
  • Components: Uses AuthForm and FormInput 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 into div#root.
  • Includes index.css for Tailwind styles.
  • Uses StrictMode for development checks.

Step 3: Test the Application

  1. Start the Backend:

    In auth-app/backend/:

    uvicorn main:app --reload

    Ensure XAMPP's MySQL is running.

  2. Start the Frontend:

    In auth-app/frontend/:

    npm start

    Open http://localhost:3000.

  3. 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.
  4. Database Check:

    In phpMyAdmin, verify users table in auth_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 use focus: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 with uvicorn: gunicorn -k uvicorn.workers.UvicornWorker main:app.
    • Host on Heroku, AWS, or DigitalOcean.
    • Use managed MySQL (e.g., AWS RDS).
  • Frontend:
    • Build: npm run build.
    • Serve with Nginx or static hosts (Netlify, Vercel).
  • 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

Popular posts from this blog

Building and Deploying a Recommender System on Kubeflow with KServe

CrewAI vs LangGraph: A Simple Guide to Multi-Agent Frameworks