跪拜 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.