Sitemap

Creating the Login and Registration System with FastAPI & React

6 min readMay 27, 2025

--

In our previous post, we set up the React frontend and FastAPI backend, laid the groundwork with Docker and Swagger, and ensured our app could talk to itself. Now it’s time to give life to our app with something every modern web app needs — authentication.

In this blog, we’ll walk through how we implemented:

  • CORS setup in main.py
  • user_router to separate concerns
  • Register and login APIs in the backen
  • Clean and modern React login/register UI
  • Axios-powered Redux slices to communicate with the backend

Backend Setup (FastAPI)

1. CORS Setup in main.py

Cross-Origin Resource Sharing (CORS) is a security feature that restricts which domains can access your API.

from fastapi.middleware.cors import CORSMiddleware
from fastapi import FastAPI
from routes.user import user_router # We will keep our routes organized

app = FastAPI()

# Add CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # In production, limit this to your frontend domain
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

app.include_router(user_router, prefix="/api")

Now your backend is fully ready to accept requests from your React app running on a different port or domain.

2. Creating user_router

Keep your project modular by creating a new file routes/user.py.

This router handles:

  • /register – To register new users
  • /login – To authenticate users

We use:

  • bcrypt to securely hash passwords
  • jose for JWT-based authentication
  • MongoDB to persist user data
from fastapi import FastAPI, HTTPException, APIRouter
from pydantic import BaseModel
import bcrypt
import uuid
from jose import jwt
from db.config import db # assuming you set this up in your previous steps

user_router = APIRouter()
SECRET_KEY = "supersecret"
ALGORITHM = "HS256"

class RegisterRequest(BaseModel):
name: str
email: str
password: str
user_type: str

class LoginRequest(BaseModel):
email: str
password: str

@user_router.post("/register")
async def register_user(user: RegisterRequest):
users_collection = db["users"]
if users_collection.find_one({"email": user.email}):
raise HTTPException(status_code=400, detail="User already exists")

hashed_pw = bcrypt.hashpw(user.password.encode(), bcrypt.gensalt())
user_id = str(uuid.uuid4())
token = jwt.encode({"user_id": user_id}, SECRET_KEY, algorithm=ALGORITHM)

users_collection.insert_one({
"user_id": user_id,
"email": user.email,
"password": hashed_pw.decode(),
"token": token,
"name": user.name,
"user_type": user.user_type,
})

return {"token": token, "user_id": user_id, "user_type": user.user_type}

@user_router.post("/login")
async def login_user(user: LoginRequest):
users_collection = db["users"]
found_user = users_collection.find_one({"email": user.email})

if not found_user or not bcrypt.checkpw(user.password.encode(), found_user["password"].encode()):
raise HTTPException(status_code=400, detail="Invalid credentials")

token = jwt.encode({"user_id": found_user["user_id"]}, SECRET_KEY, algorithm=ALGORITHM)
return {"token": token, "user_id": found_user["user_id"], "user_type": found_user["user_type"]}

Backend is ready!

Frontend Setup (React + MUI + Redux)

We created a beautiful login/sign-up page using Material-UI (MUI), and connected it with Redux Toolkit’s createApi.

1. UI Form

Our LoginPage.jsx component handles both login and sign-up, toggled via state. We included proper form validation, password length checks, and user-type selection.

Some UI highlights:

  • Toggle between login/sign-up
  • Password strength validation
  • Email format validation
  • Redirect based on user role
import React, { useState } from "react";
import {
Container,
Typography,
TextField,
Button,
Box,
Link,
CssBaseline,
ThemeProvider,
createTheme,
Select,
MenuItem,
FormControl,
InputLabel,
} from "@mui/material";
import { useSigninUserMutation, useLoginUserMutation } from "../slices/login/loginSliceApi";
import { useNavigate } from "react-router-dom";

const darkTheme = createTheme({
palette: {
mode: "dark",
primary: {
main: "#90caf9",
},
background: {
paper: "#121212",
default: "#202020",
},
text: {
primary: "#ffffff",
},
},
});

const LoginPage = () => {
const [isSignUp, setIsSignUp] = useState(false);
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [fullName, setFullName] = useState(""); // For sign-up only
const [userType, setUserType] = useState(""); // For user type selection
const [signinUser] = useSigninUserMutation();
const [loginUser] = useLoginUserMutation();
const navigate = useNavigate();

async function handleSubmit(e) {
e.preventDefault();
if (isSignUp) {
console.log("Signing Up", { email, password, fullName, userType });
const isSignupuser = await signinUser({ name: fullName, email, password, userType });
console.log("isSignupuser", isSignupuser);
if (isSignupuser?.data?.token) {
localStorage.setItem("authToken", isSignupuser?.data?.token);
localStorage.setItem("userType", userType); // Store user type in local storage
if (userType === "interviewer") {
navigate("/interviewer"); // Redirect to interviewer home page
} else {
navigate("/interviewee"); // Redirect to interviewee home page
}
}
} else {
console.log("Logging In", { email, password });
const isLoginuser = await loginUser({ email, password });
console.log("isLoginuser", isLoginuser);
if (isLoginuser?.data?.token) {
localStorage.setItem("authToken", isLoginuser?.data?.token);
localStorage.setItem("userType", isLoginuser?.data?.user_type); // Store user type in local storage
if (userType === "interviewer") {
navigate("/interviewer"); // Redirect to interviewer home page
} else {
navigate("/interviewee"); // Redirect to interviewee home page
}
}
}
}

const toggleForm = () => {
setIsSignUp(!isSignUp);
};

return (
<ThemeProvider theme={darkTheme}>
<CssBaseline />
<Container
maxWidth="xs"
sx={{
mt: 8,
p: 4,
borderRadius: 2,
boxShadow: 3,
bgcolor: "background.paper",
}}
>
<Typography
variant="h4"
component="h1"
gutterBottom
align="center"
color="text.primary"
>
{isSignUp ? "Sign Up" : "Login"}
</Typography>
<Box
component="form"
noValidate
autoComplete="off"
sx={{ mt: 2 }}
>
{isSignUp && (
<>
<TextField
label="Full Name"
variant="outlined"
fullWidth
required
margin="normal"
value={fullName}
onChange={(e) => setFullName(e.target.value)}
/>
<TextField
label="Email"
type="email"
variant="outlined"
fullWidth
required
margin="normal"
value={email}
onChange={(e) => setEmail(e.target.value)}
error={email.length > 0 && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)}
helperText={
email.length > 0 && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
? "Please enter a valid email address"
: ""
}
/>
<FormControl fullWidth margin="normal">
<InputLabel>User Type</InputLabel>
<Select
value={userType}
onChange={(e) => setUserType(e.target.value)}
required
>
<MenuItem value="interviewee">Interviewee</MenuItem>
<MenuItem value="interviewer">Interviewer</MenuItem>
</Select>
</FormControl>
</>
)}
{!isSignUp && (
<TextField
label="Email"
type="email"
variant="outlined"
fullWidth
required
margin="normal"
value={email}
onChange={(e) => setEmail(e.target.value)}
error={email.length > 0 && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)}
helperText={
email.length > 0 && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
? "Please enter a valid email address"
: ""
}
/>
)}
<TextField
label="Password"
type="password"
variant="outlined"
fullWidth
required
margin="normal"
value={password}
onChange={(e) => setPassword(e.target.value)}
error={password.length > 0 && password.length < 6}
helperText={
password.length > 0 && password.length < 6
? "Password must be at least 6 characters"
: ""
}
/>
<Button
type="submit"
variant="contained"
color="primary"
fullWidth
sx={{ mt: 2 }}
onClick={(e) => {
e.preventDefault();
if (password.length < 6) {
console.log("Password must be at least 6 characters");
return;
}
handleSubmit(e);
}}
>
{isSignUp ? "Sign Up" : "Login"}
</Button>
</Box>
<Box textAlign="center" sx={{ mt: 2 }}>
<Link
component="button"
variant="body2"
onClick={toggleForm}
sx={{ textDecoration: "underline" }}
>
{isSignUp
? "Already have an account? Login"
: "Don't have an account? Sign Up"}
</Link>
</Box>
</Container>
</ThemeProvider>
);
};

export default LoginPage;

👉 You’ve already got the full component above, which includes form UI, state handling, and conditional navigation.

2. Redux Slices for Auth (API Integration)

We’re using RTK Query for handling API calls cleanly.

Here’s a simplified look at what your slice might look like:

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
import axios from 'axios';

export const loginSliceApi = createApi({
reducerPath: 'loginApi',
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
endpoints: (builder) => ({
signinUser: builder.mutation({
queryFn: _signinUser
}),
loginUser: builder.mutation({
queryFn: _loginUser
}),
logoutUser: builder.mutation({
query: () => ({
url: '/logout',
method: 'POST',
}),
}),
}),


});

async function _signinUser({name,email, password, userType}) {
const body={
'name': name,
'email': email,
'password': password,
'user_type': userType
}
try {
console.log("Registering user:", { name, email, password });
const response = await axios.post("http://localhost:8000/user/register",body, {

headers: {
"Content-Type": "application/json",
},
});

const data = await response.data;
console.log("User registered:", data);
return {data}; // Return the response data
} catch (err) {
console.error("Registration failed", err);
}
};

async function _loginUser({email, password}) {
const body={
'email': email,
'password': password
}
try {
console.log("Logging in user:", { email , password });
const response = await axios.post("http://localhost:8000/user/login",body, {

headers: {
"Content-Type": "application/json",
},
});

const data = await response.data;
console.log("User logged in:", data);
return {data}; // Return the response data
}
catch (err) {
console.error("Login failed", err);
}
}

export const { useSigninUserMutation, useLogoutUserMutation ,useLoginUserMutation} = loginSliceApi;

Add this loginApi.reducer to your Redux store and use useLoginUserMutation or useSigninUserMutation inside your React component — already seen in LoginPage.jsx.

3. Connecting Frontend to Backend

✅ Start the backend:

uvicorn main:app — reload

✅ Start the frontend:

npm start

Open http://localhost:3000 and you’ll see a modern auth UI talking to your FastAPI backend securely through JWT. 🎉’

Summary

In this phase, we have:

  • Set up secure user authentication
  • Built backend endpoints using FastAPI and JWT
  • Connected the React frontend using RTK Query
  • Added Material UI for a clean and responsive user interface

Coming Up Next

Next, we’ll integrate resume uploads and parsing features, let users track interview status, and enhance the dashboard for both interviewers and interviewees.

So grab your chai ☕, push your code to GitHub, and I’ll see you in the next blog. Happy coding!

--

--

No responses yet