跪拜 Guibai
← Back to the summary

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:

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:

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.

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.

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?

  1. Code Reuse: When multiple components call the same API, there is no need to repeatedly write request logic; just import the function directly.
  2. Responsibility Decoupling: Components only care about business data and rendering, without needing to perceive details like API addresses, request headers, and timeouts.
  3. 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.
  4. 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:

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.

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.

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:

  1. 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.
  2. 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.
  3. Easy Maintenance: APIs, configurations, and states all have unified management entry points, making modification costs extremely low when requirements change.
  4. 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.