Node.js+Express+Mongoose 增删改查实战:分页排序与分类查询的接口设计
1. 从零搭一个草药接口为什么分页排序和分类查询最容易翻车如果你正在用 Node.js Express Mongoose 写第一个带数据库的接口大概率会遇到这样一个场景前端要一个列表页既要按分类筛选又要分页还要能按名称排序。听起来就是三个参数的事但真正写起来坑一个接一个——总数算错了、翻到第二页数据重复了、分类传空字符串结果查不到数据、排序字段传错类型直接报错。这篇就围绕一个「草药列表」的实战项目把增删改查、分类查询、分页和排序完整走一遍。核心检索词是 Node.js Express Mongoose 增删改查适合刚入门后端、想搞清楚路由分层和模型设计的同学。我会给出可以直接复制的 Schema、路由、控制器代码然后用 curl 逐项验证分页参数、排序字段和分类过滤的结果。先说清楚这个项目能做什么一个草药管理接口支持新增草药、删除草药、修改草药、根据 ID 查详情、按分类查列表列表支持分页和按名称排序。技术栈就是 Express 做路由、Mongoose 做模型和数据库操作、MongoDB 存数据。适合谁适合已经会写app.get(/)但还没系统整理过接口分层的人。我试过把分页逻辑写在路由里结果路由文件越来越长后来拆成 routes controllers models 三层维护起来舒服很多。下面按这个结构来。先看目录结构这是后面所有代码的落点project/ ├── app.js ├── models/ │ └── herbals.js ├── controllers/ │ └── herbals.js └── routes/ └── herbals.jsapp.js负责启动服务和挂载路由models放 Schemacontrollers放业务逻辑routes只做路径映射。这样分层之后分页、排序、分类过滤这些逻辑都集中在 controller 里改起来不会牵一发动全身。2. TaoToken 前置准备把模型调用和接口调试串起来写接口的过程中有两件事经常需要外部工具帮忙一是调试请求二是如果你想让接口里接入大模型能力比如自动生成草药描述需要一个稳定的模型调用入口。TaoToken 在这里的角色就是后者——它提供统一的 API 入口兼容常见的模型调用格式你可以在 Node.js 里直接发请求。先说清楚它是什么TaoToken 是一个模型调用服务平台提供 API 接口支持对话、编码等场景。适合谁适合需要在后端项目里集成模型能力、又不想自己维护多套 SDK 的开发者。官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 入口是 https://taotoken.net/api 。如果你只是做本文的增删改查其实不接模型也能跑通。但如果你想让「新增草药」的时候自动补一段描述就可以在 controller 里加一个模型调用。下面给出前置准备步骤。第一步拿到 API Key。进入控制台页面 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 创建一个 Key。这个 Key 后面要放到环境变量里不要硬编码进代码。第二步确认你要用的模型 ID。不同场景对应不同模型对话类可以去模型对话页面 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 看可用列表。如果你要做长期编码或 Agent 类任务可以了解 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。第三步在项目根目录建一个.env文件把 Key 和 Base URL 写进去TAOTOKEN_API_KEY你的Key TAOTOKEN_BASE_URLhttps://taotoken.net/api然后在app.js里用dotenv加载require(dotenv).config();这样后面 controller 里要用的时候直接process.env.TAOTOKEN_API_KEY就能取到。注意 Base URL 用 https://taotoken.net/api 不要加多余的路径。如果你用的是 Claude Code 这类工具做辅助开发可以参考接入文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里的配置说明。API Keys 管理页面在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。前置准备到这里就够了。核心还是接口本身模型调用是可选增强。下面进入正题。3. 可复制配置Schema、路由分层与分页排序控制器这一节是全文的技术核心给出可以直接复制的配置。先建模型。3.1 模型设计models/herbals.jsconst mongoose require(mongoose); const Schema mongoose.Schema; const herbalSchema new Schema({ name: { type: String, required: true }, herbalTypeId: { type: String, default: }, description: { type: String, default: }, cover: { type: String, default: }, details: { type: String, default: } }, { timestamps: true }); module.exports mongoose.model(Herbal, herbalSchema);这里比原始版本多了required和default避免字段缺失导致查询时出现 undefined。timestamps会自动加createdAt和updatedAt排序时多一个选择。3.2 数据库连接app.jsconst express require(express); const mongoose require(mongoose); require(dotenv).config(); const herbalRouter require(./routes/herbals); const app express(); app.use(express.json()); mongoose.connect(mongodb://127.0.0.1:27017/herbal); mongoose.connection.on(connected, () { console.log(数据库连接成功); }); mongoose.connection.on(error, (err) { console.log(连接失败, err.message); }); mongoose.connection.on(disconnected, () { console.log(连接断开); }); app.use(/herbals, herbalRouter); app.listen(3000, () { console.log(服务启动在 3000 端口); });注意express.json()必须加否则 POST 和 PUT 的req.body是空的这是新手最常见的坑之一。3.3 路由层routes/herbals.jsconst express require(express); const router express.Router(); const controller require(../controllers/herbals); router.get(/, controller.getList); router.get(/detail, controller.getDetail); router.post(/, controller.create); router.put(/, controller.update); router.delete(/, controller.remove); module.exports router;路由层只做映射不写业务逻辑。这样后面加中间件、加权限校验都很方便。3.4 控制器controllers/herbals.js这是分页、排序、分类查询的核心。const Herbals require(../models/herbals); // 列表分类查询 分页 排序 exports.getList async (req, res) { try { let pn parseInt(req.query.pn) || 1; let ps parseInt(req.query.ps) || 10; let herbalTypeId req.query.herbalTypeId; let sort parseInt(req.query.sort) || 1; if (pn 1) pn 1; if (ps 1) ps 10; const skip (pn - 1) * ps; let params {}; if (herbalTypeId herbalTypeId ! 0 herbalTypeId ! ) { params.herbalTypeId herbalTypeId; } const count await Herbals.countDocuments(params); const pageCount Math.ceil(count / ps); const pageList await Herbals.find(params) .sort({ name: sort }) .skip(skip) .limit(ps); res.json({ code: 0, message: , data: { pageCount, pageNum: pn, total: count, pageList } }); } catch (err) { res.json({ code: 1, message: err.message }); } }; // 详情 exports.getDetail async (req, res) { try { const id req.query.id; const doc await Herbals.findById(id); if (!doc) { return res.json({ code: 1, message: 未找到该草药 }); } res.json({ code: 0, message: , result: doc }); } catch (err) { res.json({ code: 1, message: err.message }); } }; // 新增 exports.create async (req, res) { try { const newHerbal { name: req.body.name, herbalTypeId: req.body.herbalTypeId, description: req.body.description, cover: req.body.cover, details: req.body.details }; await Herbals.create(newHerbal); res.json({ code: 0, msg: 添加成功, result: {} }); } catch (err) { res.json({ code: 1, msg: err.message }); } }; // 修改 exports.update async (req, res) { try { const condition { _id: req.body.id }; const query { $set: { name: req.body.name, herbalTypeId: req.body.herbalTypeId, description: req.body.description, cover: req.body.cover, details: req.body.details } }; await Herbals.updateOne(condition, query); res.json({ code: 0, msg: 修改成功, result: {} }); } catch (err) { res.json({ code: 1, msg: err.message }); } }; // 删除 exports.remove async (req, res) { try { await Herbals.deleteOne({ _id: req.body.id }); res.json({ code: 0, msg: 删除成功, result: {} }); } catch (err) { res.json({ code: 1, msg: err.message }); } };几个关键点说明。第一分页用countDocuments单独查总数不要像原始版本那样先find再取doc.length数据量大时后者会把整表拉进内存。第二sort参数用parseInt转成数字Mongoose 的sort({ name: 1 })里 1 是升序、-1 是降序传字符串会出问题。第三分类参数做了空值判断herbalTypeId为0、空字符串或 undefined 时都不加过滤条件返回全部。如果你需要把模型调用接进来比如新增时自动生成描述可以在create里加一段const axios require(axios); async function generateDescription(name) { const resp await axios.post( ${process.env.TAOTOKEN_BASE_URL}/v1/chat/completions, { model: 你的模型ID, messages: [ { role: user, content: 用一句话描述草药${name} } ] }, { headers: { Authorization: Bearer ${process.env.TAOTOKEN_API_KEY}, Content-Type: application/json } } ); return resp.data.choices[0].message.content; }这段是可选的不影响增删改查主流程。模型 ID 去模型对话页面确认。4. 验证请求用 curl 逐项测分页、排序和分类过滤代码写完必须验证。下面用 curl 逐项测。假设服务跑在localhost:3000。先插几条测试数据curl -X POST http://localhost:3000/herbals \ -H Content-Type: application/json \ -d {name:人参,herbalTypeId:1,description:补气,cover:,details:} curl -X POST http://localhost:3000/herbals \ -H Content-Type: application/json \ -d {name:黄芪,herbalTypeId:1,description:补气固表,cover:,details:} curl -X POST http://localhost:3000/herbals \ -H Content-Type: application/json \ -d {name:当归,herbalTypeId:2,description:补血,cover:,details:}测分页取第一页每页两条curl http://localhost:3000/herbals?pn1ps2返回里pageCount应该是 2pageList长度是 2total是 3。再取第二页curl http://localhost:3000/herbals?pn2ps2pageList长度应该是 1。如果两页数据有重复检查skip计算是不是(pn - 1) * ps。测排序按名称降序curl http://localhost:3000/herbals?pn1ps10sort-1返回的pageList里名称应该从大到小排列。升序把sort改成1。测分类过滤只看herbalTypeId1curl http://localhost:3000/herbals?pn1ps10herbalTypeId1total应该是 2pageList里只有人参和黄芪。传空分类curl http://localhost:3000/herbals?pn1ps10herbalTypeId应该返回全部 3 条。测详情、修改、删除# 详情把 ID 换成实际返回的 _id curl http://localhost:3000/herbals/detail?id你的ID # 修改 curl -X PUT http://localhost:3000/herbals \ -H Content-Type: application/json \ -d {id:你的ID,name:人参改,herbalTypeId:1} # 删除 curl -X DELETE http://localhost:3000/herbals \ -H Content-Type: application/json \ -d {id:你的ID}每步都看返回的code字段0是成功1是失败失败时message会带原因。用 Postman 的话把上面的 URL 和 body 对应填进去就行注意 POST/PUT 选rawJSON。验证通过的标准分页总数对、翻页不重复、排序方向对、分类过滤准、增删改查都有正确返回。这五项都过了接口就算跑通了。5. 本篇常见错排查401、local proxy failed、reading choices 这些报错怎么解接口跑起来之后报错是常态。这一节把几个高频错误对照着说。401 Unauthorized。如果你在 controller 里接了模型调用这个错误基本是 Key 的问题。检查.env里的TAOTOKEN_API_KEY有没有值请求头是不是Authorization: Bearer 你的Key。注意 Bearer 后面有一个空格。如果 Key 是从控制台复制的确认没有多余换行。API Keys 管理页面 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 可以重新生成。local proxy failed。这个报错通常出现在你本地网络环境有额外代理设置的时候。先检查系统环境变量里有没有HTTP_PROXY、HTTPS_PROXY如果有临时清掉再试。Node.js 里 axios 默认会读这些环境变量可以在请求配置里显式设置proxy: falseconst resp await axios.post(url, data, { headers: { ... }, proxy: false });reading choices。这个报错是resp.data.choices取不到说明返回结构和你预期的不一样。先打印完整返回console.log(JSON.stringify(resp.data, null, 2));常见原因是模型 ID 写错了或者请求体格式不对。确认model字段用的是模型对话页面里列出的 IDmessages是数组且每个元素有role和content。OAuth 相关报错。如果你用 Claude Code 或类似工具接入遇到 OAuth 报错检查接入文档里的配置项。Base URL 用 https://taotoken.net/api Key 和 Model ID 三件套要齐全。Claude Code 的配置可以参考 https://taotoken.net/claude-code?utm_sourcetaotoken_aicg_blog_endutm_contentclaudecodeutm_campaignrewrite 。Mongoose 报 CastError。比如Cast to ObjectId failed for value xxx说明你传的 ID 不是合法的 ObjectId。检查req.query.id或req.body.id是不是空字符串或格式不对。可以在查询前加判断const mongoose require(mongoose); if (!mongoose.Types.ObjectId.isValid(id)) { return res.json({ code: 1, message: ID 格式不正确 }); }分页返回 pageCount 为 0。检查countDocuments的查询条件是不是和find一致。如果分类参数处理逻辑在两处写得不一样就会出现总数和列表对不上的情况。把params抽成一个函数两处共用。排序不生效。检查sort参数有没有被parseInt转成数字。如果传的是字符串1Mongoose 可能不按预期排序。另外sort({ name: sort })里的sort必须是 1 或 -1其他值会被忽略。POST 请求 req.body 为空。九成是没加app.use(express.json())或者 Postman 里没选 JSON 格式。检查中间件顺序express.json()要在路由挂载之前。这些错误覆盖了大部分新手会遇到的场景。遇到报错先看message再去对照上面的条目基本能定位。6. 语义一致 CTA把接口调试和模型调用接起来接口跑通之后下一步通常是两件事一是把调试流程固定下来二是按需接入模型能力。调试流程方面建议把常用的 curl 命令存成一个test.sh每次改完代码跑一遍比手动点 Postman 快。分页、排序、分类这三个参数组合起来测确保边界情况都覆盖到。模型调用方面如果你要在项目里集成对话或编码能力可以从模型对话页面 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 选合适的模型然后在 controller 里按第 3 节的示例发请求。长期做编码或 Agent 类任务的话Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 会更合适。接入细节看文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite Key 管理在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。最后说一个实用技巧把分页参数封装成一个工具函数pn、ps、sort的默认值和边界处理都放进去controller 里只调一次。这样以后加新的列表接口直接复用不用每次重写一遍分页逻辑。接口分层的好处就在这里——模型、路由、控制器各管各的改一处不影响其他。

相关新闻

管道缺陷数据库管理系统:工业无损检测数据结构化方案

管道缺陷数据库管理系统:工业无损检测数据结构化方案

简介:这是一套面向计算机及相关专业(如计科、人工智能、通信工程等)在校学生与初学者的管道内检测缺陷数据库管理系统完整开发资源,解决工业检测数据规范化存储、查询与可视化管理的实际需求,适用于课程设计、毕业设计…

2026/10/11 7:48:39 阅读更多 →
微信小程序开发实战:从零构建到云开发上线全流程指南

微信小程序开发实战:从零构建到云开发上线全流程指南

1. 项目整体设计与核心思路拆解1.1 从标题看本质:这是一条完整的微信生态开发链路拿到“基于微信小程序的的设计及实现”这个标题时,我第一反应是——这大概率是某高校的毕业设计、课程设计,或者开发者的练手项目。标题里少了一个关键定语&am…

2026/10/9 20:23:42 阅读更多 →
C++键值对容器选型与性能优化实战:从map到unordered_map

C++键值对容器选型与性能优化实战:从map到unordered_map

1. 从一次性能翻车说起:为什么键值对值得单独拎出来聊前阵子帮一个做实时数据采集的朋友排查问题,他的程序跑着跑着内存就飙到十几个G,最后被系统直接干掉。代码逻辑不复杂,就是不断把采集到的传感器数据塞进一个容器里&#xff0…

2026/10/9 20:23:42 阅读更多 →

最新新闻

AutoCAD各版本怎么装?从PDF清单到安装验证的实操指南

AutoCAD各版本怎么装?从PDF清单到安装验证的实操指南

简介:这是一份AutoCAD各版本下载地址汇总手册,面向需要安装或升级AutoCAD的设计、制图与工程类用户。文档按32位与64位系统分门别类,整理了从AutoCAD 2000到2013的绿色版、精简版、中文破解版及对应补丁,并注明各版本适合的系统环…

2026/10/11 15:50:16 阅读更多 →
MBD三维模型智能标注全指南:从数据集到公差避坑与组合视图管理

MBD三维模型智能标注全指南:从数据集到公差避坑与组合视图管理

简介:这是一份围绕基于MBD的三维模型智能标注技术的专业论文资料,针对传统二维工程图在信息传递中容易遗漏数据、影响设计意图理解的问题,给出基于三维实体模型数字化来完整表达产品信息的解决思路。内容系统介绍了MBD的起源与特点&#xff0…

2026/10/11 15:50:16 阅读更多 →
AutoCAD 2021实用教程:从环境设置到出图避坑的完整操作路径

AutoCAD 2021实用教程:从环境设置到出图避坑的完整操作路径

简介:面向AutoCAD初学者与希望系统提升绘图效率的工程技术人员,这份PDF教程以中文版AutoCAD2021为主题,从软件入门基础讲到简单二维图形绘制,覆盖界面组成、文件管理、命令与系统变量、参数设置等核心环节。资源为单个PDF文档&…

2026/10/11 15:50:16 阅读更多 →
金融级分布式数据库选型指南:安全可靠测评与厂商落地实践

金融级分布式数据库选型指南:安全可靠测评与厂商落地实践

简介:这份报告由沙利文联合头豹研究院发布,聚焦2024年中国金融级分布式数据库市场,面向金融行业从业者、数据库供应商、政策制定者、研究机构及投资者。内容涵盖行业发展背景、安全可靠测评名单分析、厂商技术动态与生态动态、市场容量与份额…

2026/10/11 15:50:16 阅读更多 →
Oracle SCN 与检查点机制详解:从 ORA-01555 到恢复优化

Oracle SCN 与检查点机制详解:从 ORA-01555 到恢复优化

简介:这份PDF资料聚焦Oracle数据库两大核心机制——SCN(System Change Number)与检查点(Checkpoint),面向已具备一定Oracle基础、希望深入理解数据库一致性读与崩溃恢复原理的DBA、开发人员及备考OCP的学习…

2026/10/11 15:50:16 阅读更多 →
AI写论文被标高AI率?从写作到修改的降痕实操指南

AI写论文被标高AI率?从写作到修改的降痕实操指南

最近用豆包、Kimi这类对话式AI辅助写论文的朋友,几乎都会撞上同一个尴尬:AI帮你把段落捋顺了、逻辑也理清了,结果往学校要求的检测系统里一丢,AI率直接飙到40%、50%,甚至更高。看着满屏标红的“疑似AI生成”&#xff0…

2026/10/11 15:49:16 阅读更多 →

日新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/11 0:00:27 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/11 0:00:27 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/11 0:00:27 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 10:45:37 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 14:36:53 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 14:36:54 阅读更多 →