跪拜 Guibai
← Back to the summary

MockJS and Vite Break the Frontend's Dependency on Backend APIs

From Frontend-Backend Separation to Frontend Interface Engineering: React + MockJS + Vite Analysis

Frontend-backend separation isn't just about putting code in different folders—the real key is decoupling development timelines. This article uses a full-stack Todos project to explain from the ground up how the frontend can independently complete its development loop.


1. The Real Pain Point of Frontend-Backend Separation

1.1 Superficial Separation vs. Actual Coupling

Many teams say "we've done frontend-backend separation," but the reality is:

Frontend Dev: I need a /api/todos endpoint
Backend Dev: It's not ready yet, wait two days
Frontend Dev: ...I guess I'll wait then

This is timeline coupling—although the code is separated, the frontend's development progress is still held hostage by the backend. The frontend pages are written, but the data interfaces aren't ready, so integration testing can't happen, and you can only wait.

1.2 The Ideal State

                    ┌─────────────────────┐
  Independent       │  Components + Routes │
  Frontend Dev      │  + State             │
  (from Day 1)      │  + Mock API Layer    │
                    └─────────────────────┘
                              │
                              │  Once backend is ready
                              │  Switch baseURL with one click
                              ▼
                    ┌─────────────────────┐
  Frontend-Backend  │  Real API Connection │
  Integration       │  Just change one line│
  (after backend    │  of config           │
   is done)         └─────────────────────┘

The frontend should not wait for the backend. This is the problem that "frontend interface engineering" aims to solve.


2. Project Architecture: Three-Layer Separation

todos-fullstack/
├── frontend/                    ← Frontend: Independent Kingdom
│   └── todos/
│       ├── mock/                ← Mock Interface Layer
│       │   └── todos.js
│       ├── src/
│       │   ├── api/             ← Frontend Interface Engineering
│       │   │   ├── config.js    ← Axios config + baseURL
│       │   │   └── todos.js     ← Specific API methods
│       │   ├── components/      ← Component Layer
│       │   │   └── Nav.jsx
│       │   ├── pages/           ← Page-level Routes
│       │   │   ├── Home.jsx
│       │   │   └── todos.jsx
│       │   └── App.jsx          ← Route Entry Point
│       └── vite.config.js       ← Vite + Mock Plugin
│
├── backend/                     ← Backend (Node + Koa + MySQL)
│   └── ...                      ← Independent dev, doesn't affect frontend
│
└── readme.md                    ← Architecture Notes

Three clearly distinct layers:

Layer Responsibility Independence
mock/ Development data source Steps in when backend is unavailable
api/ Unified request management Manages config, requests, switching
pages/ + components/ UI rendering + state Only interacts with the api layer, doesn't care where data comes from

3. The Frontend Troika: Components + Routing + State Management

The readme points out the three pillars of an independent frontend application:

Components (Reactive)  +  Routing  +  State Management
    ↓                      ↓              ↓
  What users see       Page navigation  Data flow

3.1 Routing: React Router Takes Over Everything

// App.jsx
import { Routes, Route, BrowserRouter as Router } from 'react-router-dom';

function App() {
  return (
    <Router>
      <Nav />
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/todos" element={<Todos />} />
      </Routes>
    </Router>
  )
}

<Router> wraps the entire application at the outermost level, and <Routes> takes over all page-level routing. The <Nav> component is placed outside <Routes>, meaning the navigation bar is visible on all pages—this is a classic pattern for route layouts.

3.2 Lazy Loading: Optimizing the First Screen

const Home = lazy(() => import('./pages/Home'));
const Todos = lazy(() => import('./pages/todos'));

React.lazy() combined with dynamic import() achieves Code Splitting:

Without lazy loading:  First screen loads all code → slow
With lazy loading:     First screen only loads current page code → fast
                       Todos component loaded dynamically when navigating to /todos

This is thanks to Vite's underlying Rollup bundling, and combined with React's Suspense, it can also display a fallback UI during loading.


4. Frontend Interface Engineering: api/ Directory Design

This is the core of this article—elevating API management to a first-class citizen of frontend engineering.

4.1 Why is an api/ Directory Needed?

The readme gives four reasons:

① Backend APIs are often not available in time
② Need to manage all APIs uniformly
③ Use mock data to run through the workflow first
④ Switch baseURL between dev/prod environments with one click

4.2 First Layer: Axios Configuration (config.js)

// api/config.js
import axios from 'axios';

const instance = axios.create({
    baseURL: '/api',
    timeout: 5000,
});

export default instance;

Instead of using the global axios instance directly, a dedicated instance is created:

Design Decision Reason
axios.create() Doesn't pollute global axios defaults; allows multiple independent instances (e.g., for different backend services)
baseURL: '/api' All requests automatically prepend the prefix. get('/todos') → actual request /api/todos
timeout: 5000 5-second timeout protection, prevents requests from hanging

axios vs fetch: Why choose axios?

fetch:                     axios:
  ❌ Need manual .json()     ✅ Automatic JSON parsing
  ❌ Timeout requires abort   ✅ Built-in timeout
  ❌ Interceptors need wrapping ✅ Request/response interceptors
  ❌ Cancellation is complex  ✅ CancelToken
  ❌ Rudimentary progress events ✅ Upload/download progress

fetch is a browser-native API, simple and low-level. axios provides a set of enterprise-grade request management capabilities on top of fetch, making it more suitable for serious projects.

4.3 Second Layer: Specific API Methods (todos.js)

// api/todos.js
import axios from './config';

export const getTodos = async () => {
    const res = await axios.get('/todos');
    return res.data;
}

Each API method does only one thing: initiate a request, return data. async/await makes asynchronous code read like synchronous code, so the caller doesn't need to worry about Promise chains.

Call chain:
  pages/todos.jsx
    → api/todos.js (getTodos)
      → api/config.js (axios instance)
        → HTTP Request

5. MockJS: Enabling the Frontend to Start Independently

5.1 Core Idea

The frontend's two types of routes need two routing systems to handle them separately:

Frontend Application:
├── Page Routes          → handled by react-router-dom
│   ├── /             → Home.jsx
│   └── /todos        → todos.jsx
│
└── API Routes (/api/*)  → handled by vite-plugin-mock
    └── /api/todos    → mock/todos.js

react-router-dom only manages page navigation; it cannot handle HTTP API requests. From the browser's perspective, /api/todos is an HTTP request URL. The Vite dev server needs to intercept it and return the fake data prepared by the frontend.

5.2 vite.config.js: Installing the Mock Plugin

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { viteMockServe } from 'vite-plugin-mock'

export default defineConfig({
  plugins: [
    react(),
    viteMockServe({
      mockPath: 'mock',       // mock file directory
      localEnabled: true       // enable in dev environment
    })
  ],
})

viteMockServe workflow:

1. Vite dev server starts
2. viteMockServe reads all files in the mock/ directory
3. Intercepts matching HTTP request paths
4. Directly returns the response defined in the mock file
5. The request never reaches the backend — it's intercepted at the Vite server layer

5.3 Defining Mock APIs

// mock/todos.js
export default [
    {
        url: '/api/todos',
        method: 'get',
        timeout: 2000,
        response: (req, res) => {
            return {
                code: 0,
                todos: [
                    {
                        id: 1,
                        title: 'Learn Frontend Interface Engineering',
                        completed: true,
                    },
                    {
                        id: 2,
                        title: 'Watch Dragon Restaurant',
                        completed: false,
                    }
                ]
            }
        }
    }
]

Field-by-field breakdown:

Field Type Purpose
url string Matches the request path, intercepts /api/todos
method string Matches the HTTP method get / post / put / delete
timeout number Intentionally delays response by 2 seconds—simulates real network latency, preventing bugs where loading states are missed during dev because it's too fast, only to be discovered after deployment
response function Returns fake data. Can be an object or a function (for dynamic generation)

timeout: 2000 is an easily overlooked but crucial detail. Many frontend developers see data appear instantly during the mock phase and neglect handling loading states. After deployment, network latency of 500ms or 1000ms causes blank screens, flickers, and no feedback—these are all pitfalls planted by the mock phase being "too fast." Intentionally adding a delay forces the frontend to develop the muscle memory for handling asynchronous states locally.

5.4 Complete Request Chain

Page visits http://localhost:5173/todos
    │
    ├── react-router matches → renders <Todos /> component
    │
    └── <Todos /> useEffect calls getTodos() i.e. GET /api/todos
          │
          │  viteMockServe intercepts ↓
          │
          │  url="/api/todos" method="get" match successful!
          │  → waits 2 seconds (timeout)
          │  → returns { code: 0, todos: [...] }
          │
          ├── axios receives response
          ├── getTodos returns response.data
          ├── setTodos(data) ← state update
          └── UI re-renders ← user sees the list

6. Page Layer: Connecting APIs and UI

6.1 todos.jsx: State-Driven Page

import { getTodos } from '../api/todos';
import { useEffect, useState } from 'react';

function Todos() {
    const [todos, setTodos] = useState([]);

    useEffect(() => {
        (async () => {
            const data = await getTodos();
            setTodos(data);
        })();
    }, []);

    return (
        <>
            Todos
        </>
    )
}

Line-by-line analysis:

useState([]): The todos state is initialized as an empty array, updated via setTodos after the API returns. The initial value [] is important here—if null were used, todos.map() would throw an error during rendering.

useEffect(() => {}, []): An empty dependency array means it runs only once when the component mounts. This is the classic pattern for data fetching—enter page → call API → get data → render.

IIFE (Immediately Invoked Function Expression):

(async () => {
    const data = await getTodos();
    setTodos(data);
})();

Why can't await be used directly?

// ❌ Invalid: useEffect callback cannot be an async function
useEffect(async () => {
    const data = await getTodos();  // async returns a Promise
}, []);
// Reason: useEffect's return value must be a cleanup function or undefined.
// An async function always returns a Promise. React would treat this Promise as a cleanup function
// → type mismatch → potential bug

// ✅ Correct: Regular function + internal IIFE
useEffect(() => {
    (async () => {
        const data = await getTodos();
        setTodos(data);
    })();
}, []);

7. One-Click baseURL Switch: From Mock to Real Backend

This is the most elegant design of frontend interface engineering:

// api/config.js
const instance = axios.create({
    baseURL: '/api',                  // ← Dev environment: use mock
    // baseURL: 'http://localhost:3000',  // ← Production integration: switch to real backend
    timeout: 5000,
})

Switching Comparison

Development Phase (Mock):
  getTodos() → GET /api/todos
  → Vite proxy intercepts → mock/todos.js → returns fake data

Integration Phase (Real Backend):
  Comment out baseURL: '/api'
  Enable baseURL: 'http://localhost:3000'
  getTodos() → GET http://localhost:3000/todos
  → Real Node + Koa backend → returns database data

Change one line of code, and all APIs in the entire application switch over—this is the power of centralized baseURL management.

Why Use the /api Prefix?

Frontend dev server: localhost:5173
Real backend server: localhost:3000

/api/todos  → Vite Mock intercepts (development phase)
/api/todos  → Vite proxy forwards to localhost:3000/todos (integration phase)

The /api prefix is a convention—all API URLs start with /api. When the browser sees localhost:5173/api/todos, the Vite dev server can decide whether to handle it itself (mock) or forward it (proxy). Without this prefix, it's impossible to distinguish whether /todos is a page route or an API request:

/todos      → Could be a page route (react-router) or an API (HTTP request)
/api/todos  → Clearly an API request

8. Complete Architecture Diagram

┌─────────────────────────────────────────────────────────────────┐
│                        Frontend App (localhost:5173)              │
│                                                                   │
│  ┌──────────────────────────────────────────────────────────┐    │
│  │                     UI Layer                               │    │
│  │  App.jsx ──Router── Nav.jsx                               │    │
│  │              ├── /      → Home.jsx                        │    │
│  │              └── /todos → todos.jsx [useState + useEffect]│    │
│  └──────────────────────┬───────────────────────────────────┘    │
│                         │ import { getTodos }                     │
│                         ▼                                         │
│  ┌──────────────────────────────────────────────────────────┐    │
│  │                   API Engineering Layer api/               │    │
│  │  config.js ← axios.create({baseURL:'/api', timeout:5000}) │    │
│  │  todos.js  ← getTodos() → axios.get('/todos')             │    │
│  └──────────────────────┬───────────────────────────────────┘    │
│                         │ HTTP GET /api/todos                     │
│                         ▼                                         │
│  ┌──────────────────────────────────────────────────────────┐    │
│  │                   Vite Dev Server                          │    │
│  │  viteMockServe({ mockPath: 'mock', localEnabled: true })   │    │
│  │       │                                                     │    │
│  │       ├── Dev phase: intercept /api/todos → mock/todos.js  │    │
│  │       └── Integration phase: proxy to localhost:3000       │    │
│  └──────────────────────────────────────────────────────────┘    │
│                                                                   │
│  ┌──────────────────────┐                                        │
│  │  mock/todos.js        │     ← Dev phase data source            │
│  │  { url:'/api/todos',  │                                        │
│  │    method:'get',      │                                        │
│  │    response: {...} }  │                                        │
│  └──────────────────────┘                                        │
└─────────────────────────────────────────────────────────────────┘

                         ↓ After backend is done, change one line of baseURL

┌─────────────────────────────────────────────────────────────────┐
│                    Backend Service (localhost:3000)               │
│  Node + Koa + MySQL → /todos → JSON                             │
└─────────────────────────────────────────────────────────────────┘

9. Summary

This article uses a full-stack Todos project to sort out the complete engineering chain for independent frontend development:

  1. The Troika: React (Components) + React Router (Routing) + useState/useEffect (State Management) form the frontend skeleton
  2. Frontend Interface Engineering: The api/ directory uniformly manages all APIs—config.js manages configuration (axios instance + baseURL + timeout), todos.js manages specific methods (one module per JS file)
  3. MockJS Decoupling: vite-plugin-mock intercepts /api/todos requests at the Vite server layer, returning fake data defined in the mock/ directory; the frontend doesn't need to wait for the backend
  4. timeout Detail: Mock APIs intentionally add a 2-second delay to simulate real network conditions, forcing the frontend to properly handle loading states
  5. One-Click baseURL Switch: Comment out /api, enable http://localhost:3000, and all APIs in the entire application switch from mock to real backend—just one line changed
  6. IIFE + useEffect: useEffect doesn't support direct async; wrapping await in an IIFE is the best practice

The core idea in one sentence: The frontend is not just about drawing pages; it is also an independent engineering discipline—with its own API layer, data layer, and independent development loop. This is the essential meaning of frontend-backend separation.

Comments

Top 2 from juejin.cn, machine-translated. The original thread is authoritative.

亚雷

Defining the API contract first can reduce integration-testing bottlenecks.

卷土的土

Simulating latency can expose state issues early.