跪拜 Guibai
← Back to the summary

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:

  1. lazy + Suspense for code splitting: Page-level components are loaded on demand, so the initial screen doesn't load all page code. Suspense's fallback displays a Loading state while the component is loading.
  2. Routing as architecture: Router wraps everything, Nav is outside the routes (shared by all pages), and Routes is 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:

  1. Instantiate axios: Instead of using axios.get() directly, axios.create() creates an instance where all requests share configuration.
  2. Unified baseURL management: During development, baseURL: '/api', and requests are intercepted by Vite's Mock; during integration, simply change it to http://localhost:3000, and all API requests automatically switch to the back-end. One line of code completes the switch.
  3. 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:

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:

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:

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.

Comments

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

zyj军

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.

起风了8866

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.