1. Express4 里 router 与 app 到底谁管什么如果你写过 Express4 项目大概率遇到过这种局面所有接口都堆在app.js里app.get、app.post一路排到几百行想加一个 AI 能力比如统一走一个模型接口做摘要、分类、代码补全结果发现每个路由都要重复写一遍请求逻辑Key 散落在各处改一次配置要翻遍整个文件。这个问题的根源往往不是代码写得不够多而是没把app实例和router对象的职责分清楚。简单说app是应用的总入口负责全局中间件、监听端口、挂载子路由router是一组路由的集合负责某个业务模块内部的路径匹配和请求处理。Express4 引入express.Router()之后官方就推荐把路由按模块拆出去app只做“装配”router做“实现”。这样拆的好处很直接AI 接入这种横切关注点可以做成一个独立的 router 或者一层中间件挂到app上所有子路由自动共享不用每个文件都 import 一遍 SDK。这篇面向的是需要为多路由 Node 后端统一接入 AI 能力的场景。我会给出可复制的app.js与routes目录骨架配一份 TaoToken 统一 Key/API 通道的 config 片段再用 curl 验证路由挂载和请求转发是否真的通了。适合已经会写基础 Express、但项目一变大就乱的人。核心检索词就三个express4、router、app 实例围绕它们把骨架搭起来。2. 前置准备TaoToken 通道与项目依赖在动手拆路由之前先把 AI 通道准备好。TaoToken 在这里扮演的角色是统一的模型调用入口你不需要在业务代码里关心具体走哪个模型只要把 Key 和 base URL 配好router 里发请求就行。官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 入口是 https://taotoken.net/api 这个不加 UTM。第一步去控制台创建一个 API Key。打开 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 登录后在 API Keys 页面新建一个 Key复制出来先存到环境变量里别硬编码进代码。如果你还没想好怎么管理 Key可以看接入文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有 Key 轮换和权限的说明。第二步初始化项目并装依赖。Express4 本身很轻AI 请求用 Node 18 自带的 fetch 就够不需要额外装 axios。命令如下mkdir express4-ai-skeleton cd express4-ai-skeleton npm init -y npm install express4装完确认一下版本npm ls express应该显示 4.x。这里特意锁 4因为 Express5 的 router 行为有变化本篇的骨架是按 4 写的。第三步配置环境变量。在项目根目录建一个.env记得加进.gitignore内容TAOTOKEN_API_KEYsk-你的key TAOTOKEN_BASE_URLhttps://taotoken.net/api PORT3000Node 20.6 可以用--env-file.env直接加载不用 dotenv。启动脚本里加上就行。到这里前置就绪接下来进入骨架搭建。3. 可复制骨架app.js 与 routes 目录先看目录结构这是整个骨架的骨架express4-ai-skeleton/ ├── app.js ├── config/ │ └── taotoken.js ├── routes/ │ ├── index.js │ ├── users.js │ └── ai.js └── .envapp.js只做三件事创建 app、挂全局中间件、挂载 router。注意它不写任何具体业务路径这是职责划分的关键。// app.js const express require(express); const indexRouter require(./routes/index); const usersRouter require(./routes/users); const aiRouter require(./routes/ai); const app express(); app.use(express.json()); // 挂载子路由路径前缀在这里统一声明 app.use(/, indexRouter); app.use(/users, usersRouter); app.use(/ai, aiRouter); // 兜底错误处理放在所有路由之后 app.use((err, req, res, next) { console.error(err.stack); res.status(500).json({ error: internal_error, message: err.message }); }); const PORT process.env.PORT || 3000; app.listen(PORT, () { console.log(server listening on http://localhost:${PORT}); });routes/index.js是根路由只处理首页这类轻量请求// routes/index.js const express require(express); const router express.Router(); router.get(/, (req, res) { res.json({ ok: true, service: express4-ai-skeleton }); }); module.exports router;routes/users.js演示普通业务路由注意它内部路径是相对的/实际对应/users// routes/users.js const express require(express); const router express.Router(); router.get(/, (req, res) { res.json({ users: [{ id: 1, name: alice }] }); }); router.get(/:id, (req, res) { res.json({ id: req.params.id, name: alice }); }); module.exports router;routes/ai.js是 AI 能力的统一出口它调用 config 里的通道不直接碰 Key// routes/ai.js const express require(express); const router express.Router(); const { chat } require(../config/taotoken); router.post(/chat, async (req, res, next) { try { const { prompt } req.body; if (!prompt) { return res.status(400).json({ error: prompt_required }); } const reply await chat(prompt); res.json({ reply }); } catch (err) { next(err); } }); module.exports router;这样拆完app只认前缀router只认自己模块内的路径AI 逻辑集中在routes/ai.js和config/taotoken.js两处。加新模块就是新建一个 router 文件在app.js加一行app.use互不干扰。4. TaoToken 统一通道配置与请求转发config/taotoken.js是整个骨架里唯一接触 Key 和 base URL 的地方。把它做成一个薄封装业务 router 只调chat()将来换模型、加超时、加重试都只改这一个文件。// config/taotoken.js const BASE_URL process.env.TAOTOKEN_BASE_URL || https://taotoken.net/api; const API_KEY process.env.TAOTOKEN_API_KEY; if (!API_KEY) { throw new Error(TAOTOKEN_API_KEY is missing); } async function chat(prompt, options {}) { const { model gpt-4o-mini, timeout 30000 } options; const controller new AbortController(); const timer setTimeout(() controller.abort(), timeout); try { const resp await fetch(${BASE_URL}/v1/chat/completions, { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${API_KEY}, }, body: JSON.stringify({ model, messages: [{ role: user, content: prompt }], }), signal: controller.signal, }); if (!resp.ok) { const text await resp.text(); throw new Error(taotoken_http_${resp.status}: ${text}); } const data await resp.json(); return data.choices?.[0]?.message?.content ?? ; } finally { clearTimeout(timer); } } module.exports { chat };几个参数值得说明。model默认给了一个通用对话模型你可以按业务换timeout用AbortController控制避免请求卡死拖垮 Node 事件循环错误里带上 HTTP 状态码和响应体排障时一眼能看出是 Key 问题还是模型问题。如果你更习惯用 SDK 风格调用也可以参考模型对话页面的示例 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 把上面的 fetch 换成对应写法封装层不变。这里有个设计取舍为什么把 AI 调用放在config而不是routes/ai.js因为将来可能不止一个 router 要用 AI比如users.js里想给用户生成摘要直接require(../config/taotoken)就行不用跨 router 引用。config 层做通道router 层做业务边界清晰。5. 验证curl 测路由挂载与请求转发骨架搭完必须验证两件事路由有没有挂对AI 请求有没有真的转发出去。先启动服务node --env-file.env app.js看到server listening on http://localhost:3000就说明 app 实例起来了。第一个 curl 测根路由和 users 路由确认挂载前缀生效curl -s http://localhost:3000/ # {ok:true,service:express4-ai-skeleton} curl -s http://localhost:3000/users/1 # {id:1,name:alice}如果/users/1返回 404八成是app.use(/users, usersRouter)写成了app.use(usersRouter)前缀丢了。第二个 curl 测 AI 转发这是关键动作curl -s -X POST http://localhost:3000/ai/chat \ -H Content-Type: application/json \ -d {prompt:用一句话解释 Express4 的 router}正常会返回类似{reply:Express4 的 router 是一个独立的路由实例可以按模块拆分路径并挂载到 app 上。}。如果返回 500 且 message 里有taotoken_http_401说明 Key 不对或没加载进环境变量如果是taotoken_http_404检查 base URL 是不是漏了/v1或者拼错。实测下来把config/taotoken.js里的错误信息打全排障时间能省一大半。再补一个验证点故意请求一个不存在的 AI 路径确认兜底错误处理生效curl -s -X POST http://localhost:3000/ai/unknown # 404由 Express 默认处理而如果chat()内部抛错会被app.js最后的错误中间件接住返回internal_error。这两层要分清楚404 是路由没匹配上500 是匹配上了但执行出错。6. 本篇常见错排查第一个高频错app.use(/ai, aiRouter)写在app.use(express.json())之前。这样req.body是 undefinedprompt永远取不到返回 400。中间件顺序在 Express 里是从上到下执行的解析 body 的必须放最前面。第二个router 文件里用了绝对路径比如router.get(/users/:id)然后挂载时又写了app.use(/users, usersRouter)实际路径变成/users/users/:id。记住 router 内部的路径是相对于挂载点的写/或/:id就够。第三个Key 没加载。用node app.js而不是node --env-file.env app.jsprocess.env.TAOTOKEN_API_KEY就是 undefinedconfig/taotoken.js会直接 throw。如果你用的是 dotenv确认require(dotenv).config()在 require config 之前执行。第四个AI 请求超时。默认 30 秒长文本生成可能不够。可以在调用时传{ timeout: 60000 }但别设太大Node 的默认 socket 超时和网关超时可能先触发。如果业务是长期编码或 Agent 场景频繁调用建议了解一下 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 按用量规划比单次调更稳。第五个把app和router混用。有人在routes/ai.js里写app.post但app根本没传进来直接 ReferenceError。router 文件里只用routerapp只在app.js里出现。排障时如果拿不准是通道问题还是代码问题可以先去 API Keys 页面 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 确认 Key 状态再对照接入文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 检查请求格式。大部分 401/404 都是 Key 或路径拼写问题跟 router 拆分本身无关。骨架跑通之后你可以按同样的模式继续加 router新建routes/orders.js在app.js加一行挂载AI 能力通过config/taotoken.js复用。项目再大app.js也只是一张装配清单真正的逻辑都在各自的 router 里改哪块都不影响其他模块。