As someone who has led data engineering teams through the evolution from traditional ETL to modern AI-powered data platforms, I’ve observed a clear shift in what makes data professionals truly effective in today’s landscape. See the article on The Modern Data Stack: Why Data and AI Engineers Should Master FastAPI and React
Data and AI engineers who can build complete solutions that include API endpoints with a frontend for user engagement and a robust authentication system bring tremendous value to organizations.
In this tutorial, we will build a full-stack authentication module using FastAPI for the backend and React for the frontend. This project includes user authentication, role-based access control (RBAC), and profile management with support for uploading profile photos. By the end of this guide, you will have a fully functional authentication module that can be extended for real-world applications. This can be used as a starter template that you can extend for your use case. A link to the repository with the full code implementation is included at the end of the tutorial.
Project Overview
Features
- User Authentication: Login, signup, and token-based authentication.
- Role-Based Access Control: Differentiate between admin and regular users.
- Profile Management: Users can update their profile, including uploading profile photos and adding a profile summary.
- Dark Mode Support: Users can toggle between light and dark themes.
- Modern UI: Built with React, TailwindCSS, and React Router.
Tech Stack
- Backend: FastAPI, SQLAlchemy, Alembic, PostgreSQL.
- Frontend: React, TailwindCSS, React Router, Axios.
- Database: PostgreSQL.
- Deployment: Docker Compose for containerized services.
Step 1: Setting Up the Backend
1.1 Install Dependencies
Create a Python virtual environment and install the required dependencies
python -m venv venv
source venv/bin/activate # On Windows: venv\Scripts\activate
pip install -r backend/requirements.txt
1.2 Database Configuration
Set up the PostgreSQL database in docker-compose.yml:
services:
db:
image: postgres:15
environment:
- POSTGRES_DB=auth_module
- POSTGRES_USER=auth_admin
- POSTGRES_PASSWORD=auth12345
ports:
- "5435:5435"
Start the database service:
docker-compose up -d
1.3 Backend Directory Structure
The backend is organized as follows:
backend/
app/
__init__.py
auth.py
crud.py
database.py
main.py
models.py
schemas.py
migrations/
requirements.txt
1.4 Models and Database
Define the User model in models.py
:
from sqlalchemy import Column, Integer, String, Text
from .database import Base
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True, index=True)
username = Column(String, unique=True, index=True)
hashed_password = Column(String)
role = Column(String, default="user")
email = Column(String, unique=True, nullable=True)
profile_photo = Column(String, nullable=True)
profile_summary = Column(Text, nullable=True)
Run migrations to create the database schema:
cd backend
alembic revision --autogenerate -m "Initial migration"
alembic upgrade head
1.5 Authentication
Implement JWT-based authentication in auth.py
:
from passlib.context import CryptContext
from jose import jwt
from datetime import datetime, timedelta
SECRET_KEY = "YOUR_SECRET_KEY"
ALGORITHM = "HS256"
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def get_password_hash(password):
return pwd_context.hash(password)
def verify_password(plain_password, hashed_password):
return pwd_context.verify(plain_password, hashed_password)
def create_access_token(data: dict, expires_delta: timedelta = None):
to_encode = data.copy()
expire = datetime.utcnow() + (expires_delta or timedelta(minutes=15))
to_encode.update({"exp": expire})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
1.6 Profile Photo Upload
Add an endpoint in main.py
to handle profile updates, including photo uploads:
@app.put("/update-profile", response_model=schemas.UserOut)
async def update_profile(
username: str = None,
email: str = None,
profile_summary: str = None,
profile_photo: UploadFile = File(None),
db: AsyncSession = Depends(get_db),
current_user: models.User = Depends(get_current_user)
):
user = await crud.get_user_by_username(db, current_user.username)
if username:
user.username = username
if email:
user.email = email
if profile_summary:
user.profile_summary = profile_summary
if profile_photo:
file_location = f"uploads/{profile_photo.filename}"
with open(file_location, "wb") as file:
file.write(await profile_photo.read())
user.profile_photo = file_location
await db.commit()
return user
Step 2: Setting Up the Frontend

2.1 Install Dependencies
Navigate to the auth_module directory and install dependencies:
npm install @babel/runtime axios jwt-decode prop-types react react-avatar react-dom react-router-dom react-toastify
2.2 Directory Structure
The frontend is organized as follows:
frontend/auth_module/
├── src/
│ ├── components/
│ │ ├── Layout.jsx # Layout component for consistent UI
│ │ ├── ProtectedRoute.jsx # Component to protect routes based on authentication
│ ├── pages/
│ │ ├── Login.jsx # Login page
│ │ ├── Dashboard.jsx # Dashboard page
│ │ ├── AdminDashboard.jsx # Admin dashboard page
│ │ ├── UserProfile.jsx # User profile page
│ ├── services/
│ │ ├── AuthService.jsx # Authentication-related API calls
│ │ ├── AdminService.jsx # Admin-related API calls
│ ├── App.jsx # Main application component
│ ├── main.jsx # Entry point for the React app
│ ├── index.css # Global styles
│ └── tailwind.config.js # TailwindCSS configuration
├── index.html # HTML template
├── package.json # Project dependencies and scripts
└── vite.config.js # Vite configuration
2.3 User Profile Component
Create the UserProfile.jsx
component to display and update user profiles:
// src/pages/UserProfile.jsx
import { useState, useEffect } from "react";
import axios from "axios";
import { toast } from "react-toastify";
import Avatar from "react-avatar";
function UserProfile() {
const [username, setUsername] = useState("");
const [email, setEmail] = useState("");
const [profilePhoto, setProfilePhoto] = useState("");
const [profileSummary, setProfileSummary] = useState("");
const [selectedFile, setSelectedFile] = useState(null);
useEffect(() => {
async function fetchProfile() {
const token = localStorage.getItem("access_token");
try {
const response = await axios.get("http://localhost:8000/profile", {
headers: { Authorization: `Bearer ${token}` },
});
const { username, email, profile_photo, profile_summary } = response.data;
setUsername(username);
setEmail(email);
setProfilePhoto(profile_photo);
setProfileSummary(profile_summary);
} catch (error) {
console.error("Failed to fetch profile:", error);
toast.error("Failed to load profile.");
}
}
fetchProfile();
}, []);
async function handleSubmit(e) {
e.preventDefault();
const token = localStorage.getItem("access_token");
const formData = new FormData();
formData.append("username", username);
formData.append("email", email);
formData.append("profile_summary", profileSummary);
if (selectedFile) {
formData.append("profile_photo", selectedFile);
}
await axios.put("http://localhost:8000/update-profile", formData, {
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "multipart/form-data",
},
});
toast.success("Profile updated successfully!");
}
return (
<div className="flex items-center justify-center min-h-screen bg-background dark:bg-neutral">
<div className="card w-full max-w-md">
<h1 className="text-2xl font-bold mb-6 text-center text-primary">
Update Profile
</h1>
<div className="flex justify-center mb-4">
{profilePhoto ? (
<img
src={profilePhoto}
alt="Profile"
className="w-24 h-24 rounded-full"
/>
) : (
<Avatar
name={username || "User"}
size="96"
round={true}
className="w-24 h-24"
/>
)}
</div>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-secondary font-medium mb-1">
Username
</label>
<input
type="text"
value={username || ""}
onChange={(e) => setUsername(e.target.value)}
className="w-full p-2 border rounded text-primary focus:outline-none focus:ring-2 focus:ring-accent"
/>
</div>
<div>
<label className="block text-secondary font-medium mb-1">
Email
</label>
<input
type="email"
value={email || ""}
onChange={(e) => setEmail(e.target.value)}
className="w-full p-2 border rounded text-primary focus:outline-none focus:ring-2 focus:ring-accent"
/>
</div>
<div>
<label className="block text-secondary font-medium mb-1">
Profile Photo
</label>
<input
type="file"
accept="image/*"
onChange={(e) => setSelectedFile(e.target.files[0])}
className="w-full p-2 border rounded text-primary dark:text-darkText focus:outline-none focus:ring-2 focus:ring-accent"
/>
</div>
<div>
<label className="block text-secondary font-medium mb-1">
Profile Summary
</label>
<textarea
value={profileSummary || ""}
onChange={(e) => setProfileSummary(e.target.value)}
className="w-full p-2 border rounded text-primary focus:outline-none focus:ring-2 focus:ring-accent"
/>
</div>
<button
type="submit"
className="w-full bg-primary text-white py-2 rounded hover:bg-secondary"
>
Save Changes
</button>
</form>
</div>
</div>
);
}
export default UserProfile;
Step 3: Styling with TailwindCSS
Add TailwindCSS to the project for modern styling. Update tailwind.config.js
:
module.exports = {
content: ["./src/**/*.{js,jsx}"],
theme: {
extend: {
colors: {
primary: "#1C3F60",
secondary: "#007BFF",
background: "#E9F1FA",
card: "#FDFEFF",
neutral: "#4A5568",
success: "#00D97E",
warning: "#FFB547",
cta: "#6B46C1",
},
},
},
};
Step 4: Running the Application
4.1 Start the Backend
Run the FastAPI backend:
cd backend
uvicorn app.main:app --reload
4.2 Start the Frontend
Run the React frontend:
cd frontend/auth_module
npm run dev
Conclusion
Congratulations! You’ve built a full-stack authentication system with FastAPI and React. This project is a great starting point for building scalable, secure, and modern web applications. You can extend this project by adding features like email verification, password reset, or advanced role-based access control.
For the full project check out the repository on GitHub:
Discover more from The Data Lead
Subscribe to get the latest posts sent to your email.