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.