Next.js提供的API路由是它最受欢迎的特性之一:在项目的api目录下新建一个文件,框架就会自动把它注册成一个HTTP接口,前后端代码共享同一个项目结构,开发体验非常顺滑。但很多团队维护的是纯React SPA项目,受限于部署架构或历史原因无法直接迁移到Next.js。其实通过一些工程手段,完全可以在普通React应用中复刻出类似的统一API路由体验,本文就围绕这个目标展开,介绍两种可落地的实现思路。

先想清楚:React应用里的API路由到底是什么
Next.js的API路由本质上是两部分能力的组合:一是基于文件系统的约定式注册,文件路径即接口路径;二是运行时的请求分发,框架根据请求的URL和方法把流量转给对应的handler。要在一个纯React项目中模拟这套机制,需要分别解决这两个问题。
对于纯客户端SPA来说,情况又分两种。第一种是项目里确实有一个配套的后端服务(Node.js中间层),那么所谓统一API路由更多是指后端侧的路由组织方式,让后端路由也像Next.js那样按目录自动生成。第二种是希望完全在前端模拟请求响应,比如在开发环境拦截fetch,把请求转发给本地handler,适合做演示项目或离线原型。两种场景的技术方案差异较大,下面分别展开。
还需要澄清一个容易混淆的概念:React Router的路由loader和API路由不是一回事。loader解决的是“页面加载前取数据”的问题,而API路由解决的是“提供一个可以被任何客户端调用的HTTP接口”。两者可以配合使用,但不能互相替代。
方案一:基于Vite的import.meta.glob实现文件系统约定路由
Vite提供了import.meta.glob这个利器,它可以批量导入匹配某个目录模式的所有模块。利用它,我们可以让src/api目录下的每个文件自动变成一个API端点,文件路径直接映射为接口路径,例如src/api/user/list.ts对应/api/user/list。这正是Next.js约定式路由的核心思想。
首先定义一个统一的handler格式,让每个API文件都导出一个默认的处理函数,接收请求上下文并返回响应:
// src/api/user/list.ts
import type { ApiHandler } from '../../types';
const handler: ApiHandler = async (ctx) => {
const { method, query } = ctx;
if (method !== 'GET') {
return { status: 405, body: { message: 'Method Not Allowed' } };
}
// 模拟从数据库查询用户列表
const users = [{ id: 1, name: '张三' }, { id: 2, name: '李四' }];
return { status: 200, body: { users, page: Number(query.page ?? 1) } };
};
export default handler;接着编写一个注册中心,用import.meta.glob扫描整个api目录,把所有handler收集到一个Map里:
// src/api-router/registry.ts
const modules = import.meta.glob('../api/**/*.ts');
export const apiRegistry = new Map<string, () => Promise<{ default: ApiHandler }>>();
for (const path in modules) {
// 把文件路径转换为接口路径
// ../api/user/list.ts -> /api/user/list
const route = path
.replace('../api', '/api')
.replace(/\.ts$/, '')
.replace(/\/index$/, '');
apiRegistry.set(route, modules[path] as any);
}注意这里import.meta.glob默认返回的是懒加载函数,也就是说handler只有在第一次被调用时才会真正下载执行,天然实现了代码分割,这一点和Next.js的动态行为很接近。如果你的接口调用非常频繁,也可以传入{ eager: true }选项让模块在启动时全部加载,牺牲首屏体积换取更快的响应速度。
方案二:在客户端拦截fetch,模拟统一请求分发
注册中心建好之后,还差一个“请求分发器”。SPA运行在浏览器里没有真正的HTTP服务器,最常见的做法是重写全局的window.fetch,当请求URL命中/api前缀时,走本地的模拟分发逻辑,否则透传给真实网络。配合中间件链的设计,甚至可以做出类似Express或Hono的请求处理模型。
// src/api-router/dispatcher.ts
import { apiRegistry } from './registry';
export function installMockApi() {
const originalFetch = window.fetch;
window.fetch = async (input, init) => {
const url = typeof input === 'string' ? input : input.url;
const parsed = new URL(url, location.origin);
if (!parsed.pathname.startsWith('/api/')) {
return originalFetch(input, init);
}
const loader = apiRegistry.get(parsed.pathname);
if (!loader) {
return new Response(JSON.stringify({ message: 'Not Found' }), {
status: 404,
headers: { 'Content-Type': 'application/json' },
});
}
const mod = await loader();
const ctx = {
method: (init?.method ?? 'GET').toUpperCase(),
query: Object.fromEntries(parsed.searchParams),
body: init?.body ? JSON.parse(String(init.body)) : null,
};
const result = await mod.default(ctx);
return new Response(JSON.stringify(result.body), {
status: result.status ?? 200,
headers: { 'Content-Type': 'application/json' },
});
};
}在应用入口调用一次installMockApi(),之后业务代码里就可以像调用真实接口一样使用fetch('/api/user/list'),完全感知不到请求是本地处理的。这种方案特别适合做产品演示、离线可用的小工具,或者在后端尚未就绪时并行开发前端。
有几个细节值得注意。第一,重写fetch要尽早执行,最好放在main.tsx的第一行,避免其他模块先拿到原始引用。第二,如果项目里用了axios,axios在Node适配器之外最终也会走XHR或fetch,需要额外适配;更稳妥的做法是统一封装一个request函数,在函数内部做分流。第三,动态路由参数(如/api/user/:id)需要自己实现路径匹配,可以引入path-to-regexp这类成熟的匹配库,把注册表里的路径编译成正则再做提取。
方案三:有真实Node后端时用文件扫描自动注册路由
如果你的项目本来就有一个Express或Koa的中间层,那更合理的做法是在后端复刻Next.js的文件式路由,而不是在浏览器里模拟。以Express为例,借助fs模块递归扫描routes目录,把每个文件自动挂载到对应的路径上:
// server/auto-routes.js
const fs = require('fs');
const path = require('path');
const express = require('express');
function registerRoutes(app, dir, prefix = '/api') {
const files = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of files) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
registerRoutes(app, full, `${prefix}/${entry.name}`);
} else if (entry.name.endsWith('.js')) {
const base = entry.name.replace(/\.js$/, '');
const route = base === 'index' ? prefix : `${prefix}/${base}`;
const router = require(full);
app.use(route, router);
console.log(`[route] ${route}`);
}
}
}
// 使用:registerRoutes(app, path.join(__dirname, 'routes'))每个路由文件只需要导出一个标准的Express Router,例如routes/user.js导出处理GET和POST的router,最终会被挂载到/api/user。开发阶段还可以配合chokidar监听文件变化,新增接口文件时自动重新扫描注册,配合nodemon或ts-node的热重启,开发体验和Next.js基本一致。
这种方式的优势是真实可用的HTTP服务,支持中间件、鉴权、限流等完整的服务端能力,浏览器端不需要任何hack,适合要上生产的项目。它的缺点是路由文件必须遵循统一导出规范,一旦有人不按约定写文件就会注册失败,建议在启动扫描时对导出内容做类型校验,不符合规范的直接抛出明确错误,避免排查困难。
三种方案如何选择以及常见的坑
三套方案对应三种不同的项目形态:fetch拦截适合纯前端原型和演示;Vite glob注册是前者的工程化升级版,路由维护成本更低;后端文件扫描则是最接近Next.js API路由本质的方案,适合生产环境。如果项目允许,推荐优先采用第三种,前两种更多是过渡手段。
实施过程中有几个常见的坑。一是路径大小写问题,Windows本地开发时路径不区分大小写,但Linux服务器区分,文件名和请求路径务必统一小写。二是懒加载导致的首次请求延迟,可以在路由预加载策略上做优化,比如鼠标悬停在按钮上时预热对应接口模块。三是错误处理要统一,建议在分发器外层包一层try-catch,把未捕获异常统一转换成500响应,并在开发模式打印完整堆栈,生产模式只返回脱敏信息。
最后提醒一点,如果你发现团队对这套模拟API的需求越来越强,接口数量越来越多、逻辑越来越复杂,那可能就是该认真评估是否直接迁移到Next.js、Remix这类全栈框架的时候了。自己维护的约定式路由终究是模拟,框架原生方案在类型推导、缓存策略、流式渲染等方面的完整度是手工实现难以追上的。技术选型没有银弹,适合自己的团队规模和项目阶段才是最好的。
React API路由Next.js路由React Router修改时间:2026-09-14 01:59:04