Stop Sprinkling Axios Calls Through Your React Components
Many people who are just starting React projects write directly in their components:
axios.post('/api/login', {
username,
password
})
It's fine at the beginning.
But as the project grows, an obvious problem emerges:
Page code and request logic start getting tangled together.
The login page has to handle input fields, API endpoints, Axios, and server response data all at once.
So this article won't rush into JWT. Let's first sort out a fundamental issue:
How should a React page communicate with the backend?
1. First, think clearly: Why does React need an API layer?
Suppose there's a login page.
The most direct way to write it is:
async function handleLogin() {
const res = await axios.post('/api/login', {
username,
password
})
}
It looks simple.
But later the project might also have:
Login
Get user info
Get orders
Get repositories
Create order
Delete data
If every component uses Axios directly, it becomes:
Login.jsx → axios
Home.jsx → axios
Pay.jsx → axios
Profile.jsx → axios
Order.jsx → axios
Request logic is scattered across various pages.
So we want to extract the "calling the interface" part separately.
It ends up as:
React Page
↓
Call API method
↓
API file handles the request
↓
Axios sends the HTTP request
This way the page only cares about:
"I want to log in."
And doesn't need to care about:
"Is the login request POST or GET, what's the endpoint, how is Axios configured?"
2. Create the api directory
Under src, create:
src/
└── api/
This directory specifically holds the frontend code for calling backend interfaces.
Next, let's first create:
src/api/config.js
Why create this first?
Because all interfaces need to use Axios.
If each file creates its own Axios instance:
axios.create(...)
Then later, if you want to uniformly modify:
- baseURL
- timeout
- Request interceptors
- Response interceptors
You'd have to change many files.
So first create a unified Axios instance.
3. Create a unified Axios instance
In src/api/config.js:
import axios from 'axios';
const instance = axios.create({
baseURL: '/api',
timeout: 5000
});
export default instance;
The most important part here is:
baseURL: '/api'
Later when we write:
axios.post('/login')
The actual request is:
/api/login
Instead of writing the full address every time.
So:
config.js
Solves:
How the entire project uniformly uses Axios.
It doesn't handle any specific business logic.
4. Create the user API
Now we need to handle login.
Under src/api, create:
src/api/user.js
Why create user.js?
Because config.js only handles Axios's common configuration.
While:
user.js
Handles:
User-related interfaces.
For example:
- Login
- Get user info
- Modify user info
Therefore, in user.js:
import axios from './config';
export const login = async (data) => {
const res = await axios.post('/login', data);
return res.data;
}
Now we have two files:
src/api/
├── config.js // Unified Axios configuration
└── user.js // User-related interfaces
The responsibilities of these two files are already distinct.
5. Pages no longer use Axios directly
The login page no longer needs to write:
axios.post('/api/login')
Instead:
import { login } from '../api/user';
On login:
const res = await login(formData);
At this point, the whole process is very clear:
User fills in username and password
↓
Login page calls login()
↓
user.js handles sending the /login request
↓
config.js provides the Axios instance
↓
Axios sends the request
The page handles "what to do."
The API layer handles "how to request."
This is the most basic frontend layering.
6. But there's still one problem: Where is the backend?
We've now written:
axios.post('/login')
But who receives this request?
If we really write a backend, we'd also need:
- Node.js
- Express
- Routing
- Database
- Server
For learning frontend, these things can sometimes distract from the main focus.
So this demo uses:
Mock
The purpose of Mock is simple:
Pretend we already have a backend.
7. Use Vite Mock to simulate the backend
Create in the project root:
mock/
Then create:
mock/user.js
Also configure the Mock plugin in Vite.
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { viteMockServe } from 'vite-plugin-mock'
export default defineConfig({
plugins: [
react(),
viteMockServe({
mockPath: 'mock',
localEnable: true
})
],
})
Here:
mockPath: 'mock'
Tells Vite:
Mock interfaces are in the
mockdirectory.
8. Create the first Mock login interface
In:
mock/user.js
Write:
export default [
{
url: '/api/login',
method: 'post',
response: req => {
const body = req.body || {};
if (
body.username !== 'admin' ||
body.password !== '123456'
) {
return {
code: -1,
message: 'username or password error'
};
}
return {
code: 0,
user: {
username: body.username
}
};
}
}
]
Now, when the browser requests:
POST /api/login
Mock will catch it.
So the whole process becomes:
User fills in username and password
↓
Login page calls login()
↓
user.js sends /login
↓
Axios handles the request
↓
Vite Mock receives /api/login
↓
mock/user.js validates username and password
↓
Returns JSON
At this point you should understand:
Mock is not the frontend API.
It simulates the backend during development.
And the API layer is:
The code organization the frontend uses to call the backend.
These are two completely different things.
9. Why can't we say "the login system is complete" yet?
Because right now we can only do:
Username and password are correct
↓
Server returns success
But the next time the user visits:
/api/repo
How does the server know:
"This person just logged in?"
That's the problem to solve in the next article.