A Front-End API Layer That Decouples UI Work From Back-End Readiness
Tech stack: React 19 + React Router 7 + Zustand 5 + Axios + Vite + Mock.js + Koa 3 + MySQL
1. What exactly is separated in front-end and back-end separation?
Many people's understanding of "front-end and back-end separation" stops at the level of "front-end uses React, back-end uses Node." But anyone who has actually worked on a project knows that the core difficulty of separation is not in technology selection, but in the collaboration rhythm.
If the back-end API isn't ready, does the front-end just have to wait?
This Todos project provides a classic answer: front-end API engineering. The front-end doesn't just write pages; it also needs to establish its own independent API layer, allowing the front-end to run completely without depending on the back-end.
todos-fullstack/
├── fronted/todos/ # Front-end project
│ ├── src/
│ │ ├── api/ # ← Core of front-end API engineering
│ │ │ ├── config.js # Axios instance configuration
│ │ │ └── todos.js # API module
│ │ ├── components/ # Components
│ │ ├── pages/ # Page-level route components
│ │ └── App.jsx # Route entry point
│ ├── mock/ # Mock data
│ │ └── todos.js
│ └── vite.config.js # Vite + Mock configuration
├── backend/ # Back-end project (Koa)
└── readme.md
2. The Front-end Troika: Components + Routing + State Management
Components (reactive) + Routing + State Management (the bank) — the troika for independent front-end project development
2.1 Components: The Reactive Cornerstone
React components are declarative—you describe what the UI looks like, and React handles updating the DOM when data changes. The Todos.jsx in the project is a typical function component:
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 (
<>
<h1>Todos</h1>
</>
);
}
export default Todos;
Note the key design here: the component is only responsible for rendering, and data is fetched from the api/ layer. The component doesn't care whether the data comes from Mock or the back-end; it just calls getTodos(), gets the data, and renders it. This decoupling keeps the component pure.
2.2 Routing: Independent Front-end Navigation
App.jsx uses react-router-dom to take over the entire application's routing:
import { lazy, Suspense } from 'react';
import { Routes, Route, BrowserRouter as Router } from 'react-router-dom';
import Nav from './components/Nav';
const Home = lazy(() => import('./pages/Home'));
const Todos = lazy(() => import('./pages/Todos'));
function App() {
return (
<Router>
<Nav />
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/todos" element={<Todos />} />
</Routes>
</Suspense>
</Router>
);
}
Two details are worth noting:
lazy+Suspensefor code splitting: Page-level components are loaded on demand, so the initial screen doesn't load all page code.Suspense'sfallbackdisplays a Loading state while the component is loading.- Routing as architecture:
Routerwraps everything,Navis outside the routes (shared by all pages), andRoutesis inside (switching content by path). Front-end routing is completely independent of the back-end and requires no back-end cooperation.
2.3 State Management: Zustand (The Bank)
The project's package.json already includes zustand:
"dependencies": {
"axios": "^1.19.0",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"react-router-dom": "^7.18.2",
"zustand": "^5.0.14"
}
Although the current Todos.jsx is still using useState for local state, the inclusion of Zustand indicates the project's design intent: when state needs to be shared across multiple components, use Zustand for global state management. Zustand's philosophy is lightweight and boilerplate-free—unlike Redux, which requires a whole ritual of actions, reducers, and dispatch, a single create call does the job.
The readme compares state management to a "bank"—all shared state for components is stored here; whoever needs it comes to get it, and whoever changes it, everyone sees the update. This is one of the foundations for the front-end's independence from the back-end: state lives on the front-end, and the UI's reactivity is driven by the front-end itself.
3. Front-end API Engineering: The Core of This Article
This is the most worthwhile part of the entire project to discuss.
3.1 Why a Front-end API Layer?
The traditional front-end and back-end collaboration model looks like this:
Front-end writes pages → Waits for back-end API → Integration testing → Issues found during integration → Fix → More integration testing
The problem is: the front-end is held hostage by the back-end's rhythm. If the back-end API isn't ready, the front-end can only write static pages and cannot run the complete data flow.
This project's solution is: establish the front-end's own API engineering in the src/api/ directory.
3.2 Axios Instance: Unified Configuration, One-Click Switch
api/config.js is the foundation of the entire API engineering:
import axios from 'axios';
const instance = axios.create({
baseURL: '/api', // Development stage uses Mock
// baseURL: 'http://localhost:3000', // Integration stage switches to back-end
timeout: 5000,
})
export default instance;
A few lines of code accomplish three things:
- Instantiate axios: Instead of using
axios.get()directly,axios.create()creates an instance where all requests share configuration. - Unified baseURL management: During development,
baseURL: '/api', and requests are intercepted by Vite's Mock; during integration, simply change it tohttp://localhost:3000, and all API requests automatically switch to the back-end. One line of code completes the switch. - Timeout configuration: A unified 5-second timeout prevents a single API call from freezing the entire application.
Why axios instead of fetch? fetch has limited functionality. Axios provides interceptors, timeouts, automatic JSON conversion, error handling, and other out-of-the-box capabilities, making it the standard choice for front-end API engineering.
3.3 API Modules: One File Per Module
api/todos.js is the API definition for the Todos module:
import axios from './config';
export const getTodos = async () => {
const res = await axios.get('/todos');
return res.data;
}
Design principles:
- One file per module: If the project has modules like users, posts, etc., create
api/users.js,api/posts.jsrespectively. - Only expose business functions: The component calls
getTodos(), notaxios.get('/todos'). The API's URL and parameter format are encapsulated inside the function, so the component doesn't need to know the request details. - Return data, not the response object:
return res.datainstead ofreturn res, so the component directly receives the business data.
The benefit is: if the back-end changes the API path or the return data structure, only the api/ layer needs modification; the component code doesn't need a single line changed.
4. Mock.js: The Front-end's "Fake Back-end"
4.1 Vite Plugin Configuration
vite.config.js integrates vite-plugin-mock:
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, // Enabled in development environment
})
],
})
mockPath: 'mock' tells the plugin to look for Mock configurations in the mock/ folder at the project root. localEnabled: true ensures Mock is active in the development environment.
4.2 Mock File: Intercept Requests, Return Fake Data
mock/todos.js intercepts the GET request for /api/todos:
export default [
{
url: '/api/todos',
method: 'GET',
timeout: 2000, // Simulate network latency
response: (req, res) => {
return {
code: 0,
todos: [
{ id: 1, title: 'Learn front-end API engineering', completed: true },
{ id: 2, title: 'Watch Dragon Restaurant', completed: false },
]
}
}
}
]
When axios initiates a GET /api/todos request, Vite's Mock middleware intercepts this request and directly returns the data defined above, without ever sending it to the back-end.
timeout: 2000 is a detail—it simulates a 2-second network delay, allowing the front-end to test Loading states. Good Mock should not only return data but also simulate the uncertainty of a real network.
4.3 How Does the Front-end "One-Click Switch" to the Real Back-end?
This is the most elegant part of the entire architecture. The switching flow:
Development stage: axios baseURL '/api' → Vite Mock intercepts → Returns mock data
↓
Integration stage: axios baseURL 'http://localhost:3000' → Request sent to Koa back-end → Returns real data
Just change the baseURL in api/config.js—one line of code—and all API requests switch from Mock to the back-end. Component code, API functions, routing logic—all unchanged.
5. Back-end: Koa's First Steps
The back-end is still in its early stages; backend/package.json has already introduced Koa 3:
{
"name": "backend",
"type": "commonjs",
"dependencies": {
"koa": "^3.2.1"
}
}
Koa is the next-generation Node.js framework created by the Express team, characterized by an onion-model middleware based on async/await. It has no built-in routing, template engines, etc.; everything is assembled on demand through middleware. The back-end responsibilities planned in the readme are:
- Provide a
/todosAPI that returns a JSON array - Data storage uses MySQL
Once the back-end has the /todos endpoint ready, the front-end only needs to change baseURL to http://localhost:3000, and the entire data flow switches from front-end Mock to the real back-end—this is the value of front-end API engineering.
6. Architecture Panorama
Stringing all the parts together, the data flow looks like this:
User visits /todos
↓
React Router matches route → Lazy loads Todos.jsx
↓
Todos component mounts → useEffect triggers
↓
Calls getTodos() from api/todos.js
↓
Axios instance from api/config.js initiates request
↓
┌── baseURL: '/api' → Vite Mock intercepts → mock/todos.js returns fake data
│
└── baseURL: 'http://localhost:3000' → Request reaches Koa back-end → MySQL query → Returns real data
↓
Component setState → React re-renders → Page updates
7. Summary: The Three Layers of Front-end API Engineering
Although this project is a basic full-stack exercise, the engineering concept it conveys is important. Front-end API engineering is divided into three layers:
| Layer | Directory | Responsibility |
|---|---|---|
| Configuration Layer | api/config.js |
Axios instance, baseURL, timeout, interceptors |
| Module Layer | api/todos.js |
Encapsulate API functions by business module |
| Mock Layer | mock/todos.js |
Intercept requests, return simulated data |
Each of the three layers has its own role:
- The configuration layer manages "how to send requests" (unified configuration, one-click switching)
- The module layer manages "what requests to send" (business API encapsulation)
- The Mock layer manages "what to do when the back-end isn't ready" (independent front-end development)
This architecture allows the front-end team to independently complete development from routing to components to data flow when the back-end API is not yet ready. When the back-end is ready, changing one line of baseURL completes the switch. This isn't the entirety of "front-end and back-end separation," but it is the foundation of "independent front-end development."
Front-end and back-end separation is not a technical problem; it's an engineering problem. For the front-end to become an independent engineering system, it can't just know how to write components; it must also know how to manage APIs, simulate data, and control data flow. Although this Todos project is small, it explains this principle clearly.
Front-end starts with pnpm dev, back-end is to be completed.
Top 2 from juejin.cn, machine-translated. The original thread is authoritative.
Personally, for a few simple interfaces mocking is fine, but during development interface fields and even structures will change — I've almost never seen them stay the same. And mock data is static, it can't satisfy flexible page interactions. The fake mock data will most likely never be used again after the first time you use it yourself. The moment the back end changes a single field you'll just use the ready-made interface directly — it's a one-time disposable. Better to let the back end develop first, say when they're 50% done the front end steps in. Sometimes when the interface is complex, fabricating the data takes ages, and you still have to debug against this pile of fake data that may or may not change later. With that time you might as well do something else — it's a complete waste of productivity.
This approach is good, but the biggest problem is: before the back-end interface documentation is out, the front end doesn't know whether to use one interface or two, or what format the request and response parameters will take. If you add a mapping layer outside each request and between the interface calls, it's doable — just replace it when the back end has the real interface. But that also adds complexity.