1. 从零跑通 Vue.js + Node.js 直连 MongoDB 的最小全栈项目
很多刚接触全栈的朋友会卡在一个地方:前端页面写好了,后端接口也写了,但数据到底有没有真的进到 MongoDB,心里没底。这篇就带你做一个最小可运行的项目,Vue.js 负责前端展示和提交,Node.js 用官方 mongodb 驱动直连 MongoDB,不绕弯子,跑完你就能看到数据从表单一路写进数据库,再被读回页面。
核心检索词先摆出来:Vue.js 全栈开发、Node.js 直连 MongoDB、mongodb 驱动 CRUD。这套组合适合谁?适合已经会一点 JavaScript、想搞明白前后端数据链路到底怎么串起来的入门者。不需要你先精通 TypeScript,也不需要你先学一堆框架封装,我们就用最朴素的写法,把「前端发请求 → 后端收请求 → 驱动操作数据库 → 结果返回前端」这条链路走通。
为什么强调「直连」?因为很多人一上来就用 Mongoose,模型、Schema、中间件一层套一层,出了问题根本不知道是哪一层挂的。而 node-mongodb-native 这个官方驱动,API 更贴近数据库本身,你写的每一行都能对应到一次真实的数据库操作。等你把这条链路摸熟了,再去用 Mongoose 或者别的 ORM,心里就有底了。
项目结构我建议这样,简单清晰,前后端分开放:
fullstack-mongo-demo/ ├── server/ │ ├── package.json │ ├── db.js │ └── app.js └── client/ ├── package.json ├── vite.config.js ├── index.html └── src/ ├── main.js └── App.vue后端只做三件事:连数据库、暴露 CRUD 接口、返回 JSON。前端只做两件事:调接口拿列表、提交表单新增数据。没有多余的东西,跑通之后再往上加功能。
依赖清单也很克制。后端需要express和mongodb,前端用 Vite 起一个 Vue 项目,需要vue和axios。版本不用太纠结,装当前稳定版就行。下面进入具体操作。
2. TaoToken 前置准备:把模型调用和 Key 管理先理顺
在写代码之前,有个前置环节值得先处理掉:你后面调试接口、让 AI 帮你补全 CRUD 路由、排查报错的时候,会频繁用到模型对话和 API Key。与其每次到处翻,不如先把 TaoToken 这边的入口理清楚。
TaoToken 官网是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 地址是 https://taotoken.net/api ,注意 API 这个地址后面不加 UTM 参数,直接访问就行。你可以先注册登录,然后在控制台里把 Key 建好。
具体几个入口,我按用途分一下:
模型对话入口在 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite ,你调试接口遇到reading choices这类返回结构报错时,可以在这里直接问,把报错原文贴进去,让它帮你判断是响应体解析问题还是请求根本没发出去。
API Key 管理在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite ,这里生成的 Key 就是你后面写进.env或者配置片段里的那个。注意 Key 只显示一次,建完立刻复制存好。
控制台在 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,可以看调用记录和额度情况。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,里面写了 Base URL 怎么填、请求头怎么带。
如果你后面要长期做编码或者跑 Agent,可以看 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。Claude Code 相关的接入说明在 https://taotoken.net/claudecode?utm_source=taotoken_aicg_blog_end&utm_content=claudecode&utm_campaign=rewrite ,Anthropic 兼容入口在 https://taotoken.net/anthropic?utm_source=taotoken_aicg_blog_end&utm_content=anthropic&utm_campaign=rewrite 。
这里要提醒一句:TaoToken 是模型调用和 Key 管理的入口,不是用来替代你本地编辑器或者数据库工具的。MongoDB 还是装在你本机或者你用的云服务上,Node.js 还是跑在你的终端里。两者是配合关系,别搞混。
把 Key 拿到手之后,建议先放到一个临时环境变量里,别硬编码进代码。后面第三节的配置片段里我会写清楚怎么放。这一步花不了几分钟,但能让你后面调试顺畅很多。
3. 可复制配置:连接字符串、依赖清单与 CRUD 路由
这一节是重点,所有配置我都给成可以直接复制的形式。先建目录,然后分别初始化前后端。
后端初始化:
mkdir -p fullstack-mongo-demo/server cd fullstack-mongo-demo/server npm init -y npm install express mongodb dotenv前端用 Vite 起 Vue 项目:
cd .. npm create vite@latest client -- --template vue cd client npm install npm install axios后端package.json里建议加一个启动脚本,方便后面直接npm run dev:
{ "name": "server", "version": "1.0.0", "type": "module", "scripts": { "dev": "node app.js" }, "dependencies": { "dotenv": "^16.4.5", "express": "^4.19.2", "mongodb": "^6.8.0" } }注意"type": "module"这一行,它让你可以用import语法,和前端保持一致,少切换心智。如果你习惯 CommonJS,把这一行去掉,后面代码里的import换成require即可。
连接字符串放在.env文件里,别提交到仓库:
# server/.env MONGO_URI=mongodb://127.0.0.1:27017 MONGO_DB=fullstack_demo PORT=3000如果你用的是带账号密码的连接串,格式是mongodb://用户名:密码@主机:端口/?authSource=admin,具体以你的数据库实际配置为准。本地默认安装的 MongoDB 一般不需要账号密码,用上面那个就行。
数据库连接模块db.js:
// server/db.js import { MongoClient } from 'mongodb'; import dotenv from 'dotenv'; dotenv.config(); const uri = process.env.MONGO_URI || 'mongodb://127.0.0.1:27017'; const dbName = process.env.MONGO_DB || 'fullstack_demo'; const client = new MongoClient(uri); let db; export async function connectDB() { await client.connect(); db = client.db(dbName); console.log('MongoDB connected:', dbName); return db; } export function getDB() { if (!db) { throw new Error('Database not initialized. Call connectDB first.'); } return db; } export async function closeDB() { await client.close(); }这里有个细节:官方驱动每次操作完理论上要 close,但在 Web 服务里我们不会每个请求都开关一次连接,而是启动时连一次,进程退出时关一次。这样性能更好,也避免频繁开关带来的问题。closeDB留给进程退出钩子用。
主服务app.js,包含 CRUD 路由:
// server/app.js import express from 'express'; import { connectDB, getDB, closeDB } from './db.js'; const app = express(); app.use(express.json()); // 允许前端跨域访问 app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Headers', 'Content-Type'); res.header('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE'); next(); }); // 查询全部 app.get('/api/students', async (req, res) => { try { const list = await getDB().collection('students').find().toArray(); res.json({ ok: true, data: list }); } catch (err) { res.status(500).json({ ok: false, message: err.message }); } }); // 新增一条 app.post('/api/students', async (req, res) => { try { const { name, age } = req.body; const result = await getDB().collection('students').insertOne({ name, age }); res.json({ ok: true, insertedId: result.insertedId }); } catch (err) { res.status(500).json({ ok: false, message: err.message }); } }); // 更新一条 app.put('/api/students/:id', async (req, res) => { try { const { ObjectId } = await import('mongodb'); const { name, age } = req.body; const result = await getDB().collection('students').updateOne( { _id: new ObjectId(req.params.id) }, { $set: { name, age } } ); res.json({ ok: true, modified: result.modifiedCount }); } catch (err) { res.status(500).json({ ok: false, message: err.message }); } }); // 删除一条 app.delete('/api/students/:id', async (req, res) => { try { const { ObjectId } = await import('mongodb'); const result = await getDB().collection('students').deleteOne( { _id: new ObjectId(req.params.id) } ); res.json({ ok: true, deleted: result.deletedCount }); } catch (err) { res.status(500).json({ ok: false, message: err.message }); } }); const PORT = process.env.PORT || 3000; connectDB().then(() => { app.listen(PORT, () => { console.log(`Server running at http://127.0.0.1:${PORT}`); }); }); process.on('SIGINT', async () => { await closeDB(); process.exit(0); });前端App.vue里做列表展示和新增表单:
<!-- client/src/App.vue --> <script setup> import { ref, onMounted } from 'vue'; import axios from 'axios'; const API = 'http://127.0.0.1:3000/api/students'; const list = ref([]); const name = ref(''); const age = ref(''); async function load() { const res = await axios.get(API); list.value = res.data.data; } async function add() { if (!name.value) return; await axios.post(API, { name: name.value, age: Number(age.value) }); name.value = ''; age.value = ''; await load(); } async function remove(id) { await axios.delete(`${API}/${id}`); await load(); } onMounted(load); </script> <template> <div style="padding: 24px; font-family: sans-serif;"> <h2>学生列表</h2> <div> <input v-model="name" placeholder="姓名" /> <input v-model="age" placeholder="年龄" /> <button @click="add">新增</button> </div> <ul> <li v-for="item in list" :key="item._id"> {{ item.name }} - {{ item.age }} <button @click="remove(item._id)">删除</button> </li> </ul> </div> </template>前端vite.config.js保持默认即可,因为我们后端已经手动加了 CORS 头,不需要代理。如果你不想加 CORS 头,也可以在这里配 proxy,但那样多一层配置,入门阶段先用 CORS 更直观。
到这里,配置片段就齐了。Base URL、Key、Model ID 这三件套如果你要接模型辅助调试,Base URL 填https://taotoken.net/api,Key 用你在 api-keys 页面建的那个,Model ID 按文档里写的填。这三样在接入文档里都有对照表。
4. 验证请求:启动服务、调用接口、核对数据库写入
配置写完了,现在动手验证。分三步:启动数据库、启动后端、启动前端,然后看数据有没有真的落库。
先确认 MongoDB 在跑。本地安装的话,Linux/macOS 一般用:
mongod --dbpath /你的数据目录Windows 上如果是安装版,服务通常自动起来了,可以在服务列表里确认。也可以用mongosh连一下:
mongosh能进去就说明数据库正常。
启动后端:
cd fullstack-mongo-demo/server npm run dev看到MongoDB connected: fullstack_demo和Server running at http://127.0.0.1:3000就对了。如果只看到后者没看到前者,说明连接那步有问题,去第五节排查。
启动前端:
cd fullstack-mongo-demo/client npm run devVite 会给你一个本地地址,一般是http://127.0.0.1:5173。打开浏览器,你应该能看到「学生列表」标题和一个空列表。
现在用命令行直接测后端接口,先插一条:
curl -X POST http://127.0.0.1:3000/api/students \ -H "Content-Type: application/json" \ -d '{"name":"jack","age":20}'返回类似{"ok":true,"insertedId":"..."}就说明写入成功。再查一下:
curl http://127.0.0.1:3000/api/students应该能看到刚才那条 jack 的数据,_id是 MongoDB 自动生成的 ObjectId。
最关键的一步:直接去数据库里核对,确认不是接口在骗你。打开mongosh:
mongosh use fullstack_demo db.students.find()如果这里能看到 jack 那条记录,说明数据链路是真的通了:前端/curl → Express 路由 → mongodb 驱动 → MongoDB 落盘。这一步别跳过,很多人只看接口返回就以为成功了,结果数据库里根本没写进去。
再回到浏览器页面,刷新一下,列表里应该出现 jack。然后在页面上新增一条,比如「张三 / 22」,提交后再去mongosh里db.students.find(),能看到张三就说明前端到后端这条链路也通了。
删除和更新也顺手测一下。页面上点删除,再去数据库查,记录没了;用 curl 发 PUT 请求改名字,数据库里对应字段变了。四个操作都验证过,这个最小项目就算跑通了。
5. 本篇常见报错排查:401、local proxy failed、reading choices、OAuth
跑的过程中大概率会遇到几个典型报错,我按实际遇到的顺序列一下,对照着看。
报错一:401 Unauthorized
如果你在接模型辅助调试时看到 401,通常是 Key 没带对或者带错了地方。检查请求头里Authorization是不是Bearer 你的Key,中间有没有多余空格。Key 是不是从 api-keys 页面复制的完整串,有没有复制到一半。另外确认 Base URL 填的是https://taotoken.net/api,不是首页地址。401 基本就是认证信息的问题,和你的 MongoDB 代码无关。
报错二:local proxy failed
这个报错一般出现在你本地起了代理工具或者网络环境有拦截的时候。先检查你的终端环境变量里有没有HTTP_PROXY、HTTPS_PROXY这类设置,有的话临时清掉再试:
unset HTTP_PROXY unset HTTPS_PROXY然后重新跑请求。如果你用的是某些 IDE 插件自带的代理配置,也要去插件设置里关掉。这个报错和 MongoDB 连接本身没关系,是请求发不出去。
报错三:reading choices 或 Cannot read properties of undefined (reading 'choices')
这个报错说明你拿到的响应体结构和预期不一样。常见原因是请求根本没成功,返回的是一个错误对象,但代码里直接去读response.choices[0],就报 undefined。排查方法:先把原始响应打印出来,看response里到底有什么。如果是模型调用场景,确认返回的是不是标准结构;如果是你自己的接口,检查res.json返回的字段名和前端读取的是否一致。我踩过的坑就是后端返回{ data: [...] },前端却读res.list,结果就是 undefined。
报错四:OAuth 相关报错
如果你在配置 Claude Code 或者某些 CLI 工具时看到 OAuth 报错,先确认你用的是 API Key 方式还是 OAuth 方式。两者不能混。用 API Key 的话,在配置里填 Base URL 和 Key 就行,不要走 OAuth 流程。Claude Code 的接入说明在 https://taotoken.net/claudecode?utm_source=taotoken_aicg_blog_end&utm_content=claudecode&utm_campaign=rewrite ,Anthropic 兼容入口在 https://taotoken.net/anthropic?utm_source=taotoken_aicg_blog_end&utm_content=anthropic&utm_campaign=rewrite ,按文档里的方式配,别自己猜。
报错五:MongoServerError: connect ECONNREFUSED 127.0.0.1:27017
这个和上面几个不同,是数据库没起来。确认mongod进程在跑,端口是不是 27017,.env里的MONGO_URI有没有写错。如果是 Docker 起的 MongoDB,确认端口映射对不对。
报错六:ObjectId 转换失败
更新和删除接口里用到了new ObjectId(req.params.id),如果传进来的 id 不是合法的 24 位十六进制字符串,会抛错。前端传的时候确认用的是数据库返回的_id,别自己拼。如果 id 来自 URL 参数,先做个校验再转换。
排查思路统一一下:先看报错原文,判断是网络层、认证层、还是数据层;再用最小请求复现,比如直接用 curl 打后端,排除前端干扰;最后去数据库核对,确认操作到底有没有生效。这三步走下来,大部分问题都能定位。
6. 继续往下走:把这条链路用起来
项目跑通之后,你可以在这个骨架上加东西。比如给列表加分页,后端用find().skip().limit();加搜索,用find({ name: /关键字/ });加字段校验,在插入前判断 name 是否为空。这些都是在这个最小项目上自然长出来的。
如果你想让 AI 帮你补全这些功能,可以把当前的路由代码贴到模型对话里,让它按同样的风格加一个分页接口。模型对话入口在 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite ,把需求说清楚,比如「基于现有 Express + mongodb 驱动的写法,加一个支持 page 和 pageSize 的查询接口,返回总数和当前页数据」,它给的代码基本能直接用。
长期做编码或者跑 Agent 的话,Coding Plan 在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite ,可以看下额度方案。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,遇到配置问题先翻这里。Key 管理和控制台分别在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 和 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite 。
最后说个实用技巧:每次改完后端代码,别只刷新前端页面,一定要去mongosh里db.students.find()看一眼。前端显示对了不代表数据库写对了,数据库写对了才是真的对了。这个习惯能帮你省掉很多「明明页面显示了但数据丢了」的困惑。