A React+Vite Stack That Ships Frontends Before the Backend Exists
Goodbye to Waiting for Backend! React+Vite Frontend Independent Development Full Workflow: API Wrapping + Mock Solution + State Management
Author: YIAN Tags: Frontend, React.js, Vite
In the frontend-backend separation development model, frontend developers often face an awkward situation: the page logic is already sorted out, but the backend APIs are not yet developed, causing the frontend progress to be blocked. They either write dead data against empty pages and replace them one by one when the APIs are ready, which is both inefficient and error-prone.
This article uses a classic Todos full-stack project as an example to fully explain how the frontend implements API engineering encapsulation, achieves frontend development completely independent of the backend with the help of Mock solutions, and at the same time sorts out core knowledge points such as routing selection and global state management, building a standardized and maintainable frontend project architecture.
1. Overall Project Architecture and Frontend-Backend Responsibility Separation
1.1 Technology Stack Overview
We adopt a typical frontend-backend separation architecture, with completely decoupled responsibilities on both ends:
- Frontend Technology Stack: React 18 + React Router v6 + Zustand + Axios + Vite
- Backend Technology Stack: Node.js + Koa + MySQL
The frontend is responsible for page rendering, interaction logic, routing control, client-side state management, and API calls; the backend is responsible for data persistence, core business logic, permission control, and API services. The two communicate via HTTP APIs, and development progress does not block each other.
1.2 Core Idea of Frontend Independent Development
An important goal of frontend engineering is to complete full business development without depending on the backend. We use Mock technology to simulate backend API return data, first running through all the frontend's page flows, interaction logic, and state management; when the backend API development is complete, we only need to modify one configuration to seamlessly switch to real APIs.
1.3 Difference Between Mock and BFF
Many beginners easily confuse these two concepts. Here is a clear distinction:
- Mock: Intercepts requests in the frontend development environment and directly returns preset fake data, completely bypassing the real backend service, used only in the local development phase.
- BFF (Backend For Frontend): A backend layer specifically serving the frontend. After receiving frontend requests, it can return fake data, forward to the real backend, or perform data aggregation and API adaptation, generally used in production environments or multi-backend scenarios.
2. Frontend Routing Selection: HashRouter vs BrowserRouter
Routing is the core of single-page applications (SPA). react-router-dom provides two most commonly used routing modes, with completely different applicable scenarios.
2.1 BrowserRouter (History Mode)
Implemented based on the HTML5 standard History API (pushState / replaceState), the URL address is clean and tidy, without the # sign.
- Advantages: The URL is aesthetically pleasing, conforming to conventional web address habits, and is the standard choice for formal projects.
- Disadvantages: The production environment requires backend cooperation to configure redirection (such as Nginx's
try_files), otherwise refreshing the page will result in a 404. - Applicable Scenarios: Formal online projects, production environments with backend cooperation.
2.2 HashRouter (Hash Mode)
Uses the hash fragment after # in the URL for routing matching. Hash changes do not trigger page refreshes and do not send requests to the server.
- Advantages: No backend configuration is needed, refreshing the page will not 404, and deployment cost is extremely low.
- Disadvantages: The address contains a
#sign, which is not aesthetically pleasing. - Applicable Scenarios: Static page deployment, Demo prototypes, rapid development without backend cooperation.
2.3 Basic Usage Example
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Home from './pages/Home';
import Todos from './pages/Todos';
function App() {
return (
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/todos" element={<Todos />} />
</Routes>
</Router>
);
}
Note: React Router v6 replaces the old
<Switch>with<Routes>, and the matching rules are more precise.
3. Frontend API Engineering: Axios Encapsulation and Layered Management
Many beginners like to write axios requests directly in the component's useEffect. It seems fine in the short term, but once the project grows, maintenance costs will rise sharply. Standardized projects will definitely implement API layering.
3.1 Why Do API Layering?
- Code Reuse: When multiple components call the same API, there is no need to repeatedly write request logic; just import the function directly.
- Responsibility Decoupling: Components only care about business data and rendering, without needing to perceive details like API addresses, request headers, and timeouts.
- Easy Maintenance: When backend API addresses or parameters change, you only need to modify one place in the API layer, and all calling components will automatically take effect.
- Unified Handling: Convenient for later adding request interceptors (unified Token injection) and response interceptors (unified error code handling).
3.2 Step 1: Encapsulate Axios Instance
We do not use native axios directly, but create a custom instance to uniformly configure global parameters.
// src/api/config.js
import axios from 'axios';
// Create a unified axios instance
const request = axios.create({
baseURL: '/api', // Unified prefix for all APIs
timeout: 5000 // Request timeout in milliseconds
});
export default request;
baseURL will automatically concatenate with the relative path of subsequent requests. For example, request.get('/todos') will ultimately request the address /api/todos.
3.3 Step 2: Encapsulate APIs by Business Module
Put APIs of the same business module in the same file. For example, all todo-related APIs are placed in api/todos.js.
// src/api/todos.js
import request from './config';
/**
* Get Todo list
*/
export const getTodos = async () => {
const res = await request.get('/todos');
// res.data is the real business data returned by the backend
return res.data;
};
Components only need to import the getTodos function and call it, without caring at all whether axios or fetch is used internally.
4. Mock Solution: The Core of Completely Independent Frontend Development
We use vite-plugin-mock to implement API simulation. It works at the Vite development server level, providing a better debugging experience than traditional Mock.js.
4.1 Why Choose vite-plugin-mock
Compared to the commonly used frontend Mock.js:
- Mock.js: Intercepts XHR requests at the browser level. The browser's Network panel cannot see the real requests, making debugging unintuitive.
- vite-plugin-mock: Intercepts requests at the Vite development server (Node layer). The Network panel can see complete requests and responses, with an experience completely identical to calling real APIs.
4.2 Complete Configuration Steps
1. Install Dependency
-D indicates a development dependency, which only takes effect in the local development environment and will not enter production code during packaging.
npm i -D vite-plugin-mock
2. Register Plugin in Vite Configuration File
Modify the vite.config.js in the project root directory and add the mock plugin to the plugins array.
// vite.config.js
import { defineConfig } from 'vite';
import { vitePluginMock } from 'vite-plugin-mock';
export default defineConfig({
plugins: [
vitePluginMock({
mockPath: './mock', // Directory for storing mock API files
enable: true // Master switch: true to enable mock, false to disable
})
]
});
⚠️ Important Note: After modifying vite.config.js, you must restart the development server for the configuration to take effect.
3. Write Mock API Files
Create a mock folder in the project root directory, and create corresponding mock files by business module, such as mock/todos.js.
Plugin convention: Each mock file exports an array by default, and each object in the array corresponds to one API rule.
// mock/todos.js
export default [
{
url: '/api/todos', // API address, must match the full path of the axios request
method: 'get', // Request method
timeout: 200, // Simulate network delay in milliseconds
response: () => {
// Return simulated response data
return {
code: 0, // Business status code, 0 indicates success
message: 'success',
todos: [
{ id: 1, title: 'Learn frontend API engineering', completed: true },
{ id: 2, title: 'Learn backend development', completed: false }
]
};
}
}
];
4.3 Effect and Switching
After configuration is complete, when the frontend initiates a get /api/todos request, the Vite development server will directly intercept the request and return the simulated data we wrote, completely eliminating the need to start the backend service.
When the backend API development is complete, simply change enable in vite.config.js to false, and requests will go to the real backend API. Not a single line of business code needs to be changed, achieving a smooth switch.
5. Global State Management: Context vs Zustand
Sharing state across components is a high-frequency requirement in React development. Here we compare two mainstream solutions to help you clarify your selection thinking.
5.1 Context Solution
Context is React's native state sharing solution. The principle is to hang data on the Provider node of the component tree, and descendant components read it through useContext.
- Disadvantages: Requires layer-by-layer wrapping of Providers, leading to deep component tree nesting; whenever data in the Provider changes, all consuming components will re-render, causing large performance overhead; complex state requires pairing with useReducer, making the code bloated.
- Applicable Scenarios: Lightweight UI state passing within a local component tree, such as themes and language configurations.
5.2 Zustand Solution
Zustand is a very popular lightweight state management library currently. Its store is a global object independent of the component tree, and components directly import and use it.
- Advantages: No Provider wrapping needed, extremely low invasiveness; components subscribe to state on demand, only triggering re-renders when the data they care about changes, resulting in better performance; concise API, small amount of code, naturally supports async and middleware.
- Applicable Scenarios: Global business state sharing across components.
5.3 One-Sentence Summary
Zustand can completely replace the "Context + useReducer" global state solution; if it is only lightweight state passing within a local component tree, Context is still applicable. The two do not conflict.
6. Complete Page Practice: Todo List Page
Now we string together all the previous knowledge points to implement a complete Todo list page.
6.1 Navigation Component
Use <Link> instead of native <a> tags to achieve frontend refreshless route navigation.
// src/components/Nav.jsx
import { Link } from 'react-router-dom';
const Nav = () => {
return (
<nav style={{ padding: '10px 20px', borderBottom: '1px solid #ccc' }}>
<Link to="/" style={{ marginRight: 20 }}>Home</Link>
<Link to="/todos">Todo List</Link>
</nav>
);
};
export default Nav;
6.2 Todo Page Component
Call the API in useEffect, store the returned data in state, and render it to the page.
// src/pages/Todos.jsx
import { useEffect, useState } from 'react';
import { getTodos } from '../api/todos';
function Todos() {
const [todos, setTodos] = useState([]);
useEffect(() => {
// useEffect callback cannot be directly written as async, wrap with an immediately invoked function expression
(async () => {
const result = await getTodos();
// mock return format is {code:0, todos:[]}, take the todos array
setTodos(result.todos);
})();
}, []);
return (
<div style={{ padding: 20 }}>
<h2>Todo List</h2>
<ul>
{todos.map(item => (
<li
key={item.id}
style={{
listStyle: 'none',
padding: '8px 0',
textDecoration: item.completed ? 'line-through' : 'none',
color: item.completed ? '#999' : '#333'
}}
>
{item.title}
</li>
))}
</ul>
</div>
);
}
export default Todos;
Why use an immediately invoked function expression? If the useEffect callback function is directly declared as async, it will return a Promise; but useEffect expects the return value to be a cleanup function. Therefore, wrapping asynchronous logic with an IIFE (Immediately Invoked Function Expression) is the standard practice.
6.3 Root Component Routing Configuration
Finally, integrate navigation and routing in App.jsx:
// src/App.jsx
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Nav from './components/Nav';
import Home from './pages/Home';
import Todos from './pages/Todos';
function App() {
return (
<Router>
<Nav />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/todos" element={<Todos />} />
</Routes>
</Router>
);
}
export default App;
7. Project Architecture Summary
This architecture is the standard paradigm for medium to large React projects, with very clear core advantages:
- Clear Responsibilities: The component layer is responsible for rendering and interaction, the API layer is responsible for network requests, the mock layer is responsible for simulating data. Layers are clearly defined, each performing its own role.
- Independent Development: The frontend does not need to wait for backend APIs at all. Based on Mock, it can run through the complete business process.
- Easy Maintenance: APIs, configurations, and states all have unified management entry points, making modification costs extremely low when requirements change.
- Smooth Switching: Switching from the Mock environment to the real backend only requires changing one configuration item, with zero changes to business code.
Subsequently, you can further expand on this foundation: add request interceptors to uniformly inject Tokens, response interceptors to uniformly handle error prompts, use Zustand to take over global Todo state, add mock APIs for create/delete/update, etc., gradually perfecting it into a complete frontend engineering project.