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:
- The Troika: React (Components) + React Router (Routing) + useState/useEffect (State Management) form the frontend skeleton
- 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) - MockJS Decoupling:
vite-plugin-mockintercepts/api/todosrequests at the Vite server layer, returning fake data defined in themock/directory; the frontend doesn't need to wait for the backend - timeout Detail: Mock APIs intentionally add a 2-second delay to simulate real network conditions, forcing the frontend to properly handle loading states
- One-Click baseURL Switch: Comment out
/api, enablehttp://localhost:3000, and all APIs in the entire application switch from mock to real backend—just one line changed - IIFE + useEffect:
useEffectdoesn't support directasync; wrappingawaitin 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.
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.