跪拜 Guibai
← Back to the summary

Stop Waiting on Backend APIs: A Seven-File React Pattern for Independent Frontend Development

Everyone's heard the slogan "separation of frontend and backend," but when it comes to code, how exactly does the frontend "develop independently without relying on the backend"? The answer lies in these seven files. This article uses a Todos project to break down, file by file, how the frontend minimizes coupling with the backend through an API layer + Mock data.


A one-sentence summary of responsibilities:

File Responsibility
main.jsx Start the application
App.jsx Route and assign pages
Nav.jsx Navigation links
Home.jsx / Todos.jsx Page content
api/config.js + api/todos.js Data request layer (the only coupling point between frontend and backend)

① main.jsx: Entry point, does only one thing

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'

createRoot(document.getElementById('root')).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

Line by line:

The entry file is very thin, which is a good thing—it only handles "starting up," leaving all specific logic to App.


② App.jsx: Routing takes over everything (Key point: lazy loading)

import React, { 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 />
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/todos" element={<Todos />} />
      </Routes>
    </Router>
  )
}
export default App

1. <Router> takes over routing

2. lazy + Suspense for on-demand loading (lazy loading)

const Home = lazy(() => import('./pages/Home'));

Small reminder: In the code, <Suspense> is actually not used (the lazy components aren't wrapped in it). Strictly speaking, this is a point to be improved. The complete way to write it should be:

<Suspense fallback={<div>Loading...</div>}>
  <Routes>...</Routes>
</Suspense>

③ Nav.jsx: Page-level navigation

import { Link } from 'react-router-dom';

function Nav() {
  return (
    <nav style={{padding:10, borderBottom:'1px solid #ccc'}}>
      <Link to="/">Home</Link>
      <Link to="/todos">Todos</Link>
    </nav>
  )
}
export default Nav

Key point: Why use <Link> instead of <a>?

This is one of the most basic and important habits in React Router.


④⑤ Home.jsx / Todos.jsx: Page components

Home.jsx is currently minimal:

function Home() {
  return (
    <>
      Home
    </>
  )
}
export default Home

Todos.jsx is the focus—it demonstrates how a page fetches data:

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
    </>
  )
}
export default Todos

Line by line:

The prototype of "frontend-backend separation" is hidden here: Todos.jsx doesn't care where the data comes from, whether it's a real backend or Mock. It only calls the interface function getTodos(). No matter how the data source changes, the page code doesn't need a single line modified.


⑥ api/config.js: Axios instance, one-click baseURL switching

import axios from 'axios';

const instance = axios.create({
  baseURL: '/api',          // Frontend simulated request address /api/todos
  // baseURL: 'http://localhost:3000',  // Change this when switching to a real backend
  timeout: 5000,
})

export default instance;

This is the most valuable file in the entire project, broken down line by line:

Why upgrade to axios instead of using native fetch?

fetch has too few features: no timeout, no interceptors, cumbersome error handling, and requires manual .json(). axios provides instantiation, timeouts, interceptors, etc., representing the standard practice of "upgrading from fetch to axios."


⑦ api/todos.js: Unified interface management

import axios from './config';

export const getTodos = async () => {
  const res = await axios.get('/todos'); 
  return res.data;
}

This file embodies the interface management standard of "one module, one js file":

A small detail here: axios.get('/todos') combined with baseURL: '/api' in config.js results in a final request to /api/todos. This /api/todos is precisely the address intercepted by Mock; the frontend and backend "connect" here.


Appendix: mock/todos.js — The key to letting the frontend "not wait foolishly for the backend"

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 Diner', completed: false }
        ]
      }
    }
  }
]

Paired with vite.config.js:

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { viteMockServe } from 'vite-plugin-mock';

export default defineConfig({
  plugins: [react(), viteMockServe({
    mockPath: 'mock',      // Directory where mock files are located
    localEnabled: true     // Enable mock in the development environment
  })],
})

Its function:

vite-plugin-mock intercepts the /api/todos request in the development server and directly returns the hardcoded fake data from mock/todos.js, bypassing the backend entirely.

Thus, the frontend has its own "fake interface" during the development phase, allowing it to independently complete the UI and get the data flow running first. Once the backend truly finishes writing /api/todos, switching the baseURL in config.js enables a seamless connection.


Tying it together: The complete chain of a single request

  1. User visits /todosApp.jsx route matches <Todos />.
  2. Todos.jsx mounts → useEffect triggers getTodos().
  3. api/todos.js uses the axios instance configured by config.js to request baseURL + '/todos' = /api/todos.
  4. During development, vite-plugin-mock intercepts /api/todos and returns the fake data from mock/todos.js.
  5. Data returns to Todos.jsx, setTodos triggers rendering.

Throughout this entire chain, the page (Todos.jsx) never knows the data is "fake" from start to finish—this is precisely the goal of frontend-backend separation.


Summary: Three things these seven files teach us

Core Idea Implementation Point
The key to decoupling = Interface requests The frontend only recognizes getTodos(), not the backend
Unified interface management in the api directory config.js manages configuration, todos.js manages interfaces
One-click baseURL switching Mock → Real backend, only one line needs changing

Add one more engineering methodology: The frontend should never foolishly wait for backend interfaces. First, establish the API layer + Mock data, and fully build out the UI and interactions. Once the backend interface is ready, switching one line of baseURL allows for joint debugging. This is the true meaning of "frontend-backend separation" at the code level.