Modern web applications require secure authentication and controlled access to resources. In this article, we implement a login system in React that communicates with a backend API, stores JWT tokens, and enables authenticated API calls.
The architecture we implement follows industry best practices by separating UI, logic, and API layers.
1. Understanding the Authentication Flow
Before implementation, it is important to understand the complete login flow.
User enters username and password
↓
React calls Login API
↓
Server validates credentials
↓
Server returns Access Token + Refresh Token
↓
React stores tokens in localStorage
↓
User navigates to protected pages
↓
Axios automatically attaches token to API requests
This approach ensures:
Secure communication with backend APIs
Controlled access to protected resources
Stateless authentication using JWT tokens
2. Project Structure
To keep the application maintainable, we organize the project as follows:
src
├── api
│ axiosClient.js
│
├── services
│ authService.js
│
├── components
│ LoginHeader.jsx
│ LoginForm.jsx
│
├── pages
│ LoginPage.jsx
│ EmployeePage.jsx
│
└── App.js
Each folder has a specific responsibility.
| Folder | Responsibility |
|---|
| api | Axios configuration |
| services | API calls |
| components | UI elements |
| pages | Page level logic |
| App.js | Routing |
3. Creating Axios Client
The Axios client centralizes API configuration.
src/api/axiosClient.js
import axios from "axios";
const axiosClient = axios.create({
baseURL: "http://localhost:5288/api",
headers: {
"Content-Type": "application/json"
}
});
export default axiosClient;
Benefits of this approach:
4. Creating the Authentication Service
The service layer is responsible for communicating with the backend API.
src/services/authService.js
import axiosClient from "../api/axiosClient";
export const login = async (username, password) => {
const response = await axiosClient.post(
`/auth/login?username=${username}&password=${password}`
);
return response.data;
};
This method sends credentials to the backend and returns:
{
accessToken: "...",
refreshToken: "..."
}
5. Creating the Login Header Component
src/components/LoginHeader.jsx
import React from 'react'
function LoginHeader() {
return (
<div className="bg-gray-500 text-white p-6 rounded shadow text-3xl text-center mb-6">
Login
</div>
)
}
export default LoginHeader
What is this?
This component is purely responsible for UI rendering.
6. Creating the Login Form Component
The LoginForm component receives props from the parent page.
src/components/LoginForm.jsx
import React from 'react'
function LoginForm({
userName,
password,
setUserName,
setPassword,
handleLogin
}) {
return (
<div>
<input
type="text"
value={userName}
placeholder="Enter User Name"
onChange={(e)=>setUserName(e.target.value)}
/>
<input
type="password"
value={password}
placeholder="Enter Password"
onChange={(e)=>setPassword(e.target.value)}
/>
<br/>
<button onClick={handleLogin}>
Login
</button>
</div>
)
}
export default LoginForm
What is this?
Key concept here:
The component does not contain business logic.
It simply receives props from the parent component.
7. Implementing the Login Page
The LoginPage manages the application logic.
src/pages/LoginPage.jsx
import React, { useState } from 'react'
import LoginForm from '../components/LoginForm'
import LoginHeader from '../components/LoginHeader'
import { login } from '../services/authService'
import { useNavigate } from "react-router-dom"
function LoginPage() {
const navigate = useNavigate()
const [userName, setUserName] = useState("")
const [password, setPassword] = useState("")
const handleLogin = async () => {
try {
const data = await login(userName, password)
localStorage.setItem("accessToken", data.accessToken)
localStorage.setItem("refreshToken", data.refreshToken)
navigate("/employees")
} catch (error) {
console.error("Login failed", error)
}
}
return (
<div className="min-h-screen flex justify-center items-center bg-gray-100">
<div className="w-full max-w-md">
<LoginHeader />
<LoginForm
userName={userName}
password={password}
setUserName={setUserName}
setPassword={setPassword}
handleLogin={handleLogin}
/>
</div>
</div>
)
}
export default LoginPage
Responsibilities of this page:
Manage state
Call authentication API
Store tokens
Navigate after login
8. Configuring Application Routing
src/App.js
import { BrowserRouter, Routes, Route } from "react-router-dom";
import LoginPage from "./pages/LoginPage";
import EmployeePage from "./pages/EmployeePage";
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<LoginPage />} />
<Route path="/employees" element={<EmployeePage />} />
</Routes>
</BrowserRouter>
);
}
export default App;
After successful login, the user is redirected to:
/employees
9. Storing Tokens
The access token is stored using:
localStorage.setItem("accessToken", token)
This allows future API requests to include authentication.
10. Next Step: Axios Interceptor (Important)
In real applications we configure Axios interceptors to attach the token automatically.
Example:
axiosClient.interceptors.request.use((config)=>{
const token = localStorage.getItem("accessToken")
if(token){
config.headers.Authorization = `Bearer ${token}`
}
return config
})
This ensures every API request includes:
Authorization: Bearer <token>
11. Benefits of This Architecture
This structure provides:
Clean separation of concerns
Reusable components
Centralized API management
Scalable authentication structure
Industry-level React architecture
Conclusion
Implementing authentication in React requires a clear separation between:
UI components
Business logic
API communication
By using JWT authentication, Axios services, and proper project structure, we can build a secure and scalable authentication system suitable for real-world applications.