今天我们来做一个不一样的东西——之前 20 篇实践课都在写命令行工具,这次我们用 Node.js + Express 搭建一个完整的 RESTful 书签管理 API,支持增删改查、标签搜索和 JSON 文件持久化。最终代码约 100 行,可以直接用 curl 或 Postman 测试。
这是最关键的一步——我们不给 OpenCode 写具体代码,只描述想要什么。
我对 OpenCode 说:
> 帮我创建一个 Node.js 项目,使用 Express 框架,做一个书签管理的 RESTful API 服务,数据用本地 JSON 文件存储。先搭好项目骨架,包含 GET /bookmarks 和 POST /bookmarks 两个基础接口。
OpenCode 的响应:
OpenCode 自动执行了 npm init、npm install express,然后生成了 index.js,包含 Express 服务器启动代码、JSON 文件读写函数,以及列表查询和新增书签两个路由。
关键要点: 不用告诉它"先 npm init 再 npm install",你只需要说清楚最终目标,OpenCode 会自己推断出所需的中间步骤。这比手敲命令快得多。
有了骨架之后,让 OpenCode 完善剩余接口。
我对 OpenCode 说:
> 补全 PUT /bookmarks/:id 更新书签和 DELETE /bookmarks/:id 删除书签两个接口。同时添加输入校验——title 和 url 为必填项,缺少时返回 400 错误。
OpenCode 的响应:
OpenCode 在现有代码上追加了 PUT 和 DELETE 路由,并为 POST 接口添加了参数校验逻辑。ID 的查找使用了 findIndex,找不到时返回 404。
关键要点: 注意我的提示词包含了边界条件("找不到返回 404"),这样 OpenCode 生成的代码会更健壮。写得越具体,结果越符合预期。
基础 CRUD 完成后,增强搜索能力。
我对 OpenCode 说:
> 在 GET /bookmarks 接口中支持查询参数 q 进行模糊搜索(匹配 title 和 url),再支持 tag 参数按标签精确过滤。书签结构增加 tags 字段(字符串数组)和 createdAt 时间戳。另外再加一个 GET /bookmarks/tags 接口,返回所有不重复的标签列表。
OpenCode 的响应:
OpenCode 修改了 GET /bookmarks 的路由逻辑,加入了 q 和 tag 两个查询参数的处理;在 POST 接口中增加了 tags 和 createdAt 字段;新增了 /bookmarks/tags 路由,用 flatMap + Set 去重返回标签列表。
关键要点: 一次提多个需求是可以的,但要结构清晰——先说搜索参数,再说数据结构,最后说新接口。混乱的提示词会得到混乱的结果。
代码完成后,让 OpenCode 负责测试。
我对 OpenCode 说:
> 帮我测试这个 API,依次执行以下操作并验证结果:
> 1. 新增一个书签(title=GitHub, url=https://github.com, tags=[开发, 开源])
> 2. 再新增一个书签(title=掘金, url=https://juejin.cn, tags=[前端, 博客])
> 3. 查询全部书签
> 4. 按标签 "开发" 搜索
> 5. 更新第一个书签的 title
> 6. 删除第二个书签
> 7. 查看所有标签
> 用 node index.js 启动服务后,生成完整的 curl 测试命令。
OpenCode 的响应:
OpenCode 生成了一套完整的 curl 测试脚本,并说明了每个请求的预期返回。我复制粘贴运行后,7 个测试全部通过。
关键要点: 让 OpenCode 写测试命令而不是你自己手写 curl,能节省大量调试时间。它生成的命令可以直接用,基本不需要修改。
const express = require('express');
const fs = require('fs');
const path = require('path');
const app = express();
const PORT = 3000;
const DATA_FILE = path.join(__dirname, 'bookmarks.json');
app.use(express.json());
function readBookmarks() {
try {
return JSON.parse(fs.readFileSync(DATA_FILE, 'utf-8'));
} catch {
return [];
}
}
function writeBookmarks(bookmarks) {
fs.writeFileSync(DATA_FILE, JSON.stringify(bookmarks, null, 2));
}
app.get('/bookmarks', (req, res) => {
let bookmarks = readBookmarks();
const { q, tag } = req.query;
if (q) {
const kw = q.toLowerCase();
bookmarks = bookmarks.filter(b =>
b.title.toLowerCase().includes(kw) || b.url.toLowerCase().includes(kw)
);
}
if (tag) {
bookmarks = bookmarks.filter(b => b.tags && b.tags.includes(tag));
}
res.json(bookmarks);
});
app.get('/bookmarks/tags', (req, res) => {
const bookmarks = readBookmarks();
const tags = [...new Set(bookmarks.flatMap(b => b.tags || []))];
res.json(tags);
});
app.get('/bookmarks/:id', (req, res) => {
const bookmarks = readBookmarks();
const bookmark = bookmarks.find(b => b.id === req.params.id);
if (!bookmark) return res.status(404).json({ error: '书签不存在' });
res.json(bookmark);
});
app.post('/bookmarks', (req, res) => {
const { title, url } = req.body;
if (!title || !url) {
return res.status(400).json({ error: 'title 和 url 为必填项' });
}
const bookmarks = readBookmarks();
const bookmark = {
id: Date.now().toString(36) + Math.random().toString(36).slice(2, 8),
title,
url,
tags: req.body.tags || [],
createdAt: new Date().toISOString(),
};
bookmarks.push(bookmark);
writeBookmarks(bookmarks);
res.status(201).json(bookmark);
});
app.put('/bookmarks/:id', (req, res) => {
const bookmarks = readBookmarks();
const index = bookmarks.findIndex(b => b.id === req.params.id);
if (index === -1) return res.status(404).json({ error: '书签不存在' });
const { title, url, tags } = req.body;
if (title !== undefined) bookmarks[index].title = title;
if (url !== undefined) bookmarks[index].url = url;
if (tags !== undefined) bookmarks[index].tags = tags;
bookmarks[index].updatedAt = new Date().toISOString();
writeBookmarks(bookmarks);
res.json(bookmarks[index]);
});
app.delete('/bookmarks/:id', (req, res) => {
let bookmarks = readBookmarks();
const index = bookmarks.findIndex(b => b.id === req.params.id);
if (index === -1) return res.status(404).json({ error: '书签不存在' });
const [deleted] = bookmarks.splice(index, 1);
writeBookmarks(bookmarks);
res.json({ message: '删除成功', deleted });
});
app.listen(PORT, () => {
console.log(`书签管理 API 运行在 http://localhost:${PORT}`);
});
启动方式:
npm install express node index.js
测试命令:
# 新增书签
curl -X POST http://localhost:3000/bookmarks \
-H "Content-Type: application/json" \
-d '{"title":"GitHub","url":"https://github.com","tags":["开发","开源"]}'
# 查询全部
curl http://localhost:3000/bookmarks
# 按标签搜索
curl http://localhost:3000/bookmarks?tag=开发
# 更新书签
curl -X PUT http://localhost:3000/bookmarks/<id> \
-H "Content-Type: application/json" \
-d '{"title":"GitHub 代码托管"}'
# 删除书签
curl -X DELETE http://localhost:3000/bookmarks/<id>
# 获取所有标签
curl http://localhost:3000/bookmarks/tags
用 OpenCode 完成这个 API 项目,我全程没有手动写一行 JavaScript 代码,只用了 4 句自然语言指令:
"帮我创建一个 Express 书签 API"
"补全 CRUD 和输入校验"
"添加搜索和标签功能"
"帮我测试"
实际用时约 5 分钟,手动开发的话至少需要 20-30 分钟(包括查文档、写 curl 测试)。更关键的是,代码质量——OpenCode 生成的校验逻辑、错误处理和 ID 生成方案都比较规范,比自己随手写的更健壮。
这次实践的另一个体会是:OpenCode 不止能做 CLI 工具,像 RESTful API 这种有路由、有中间件、有数据持久化的项目同样能胜任。下次我打算试试让它写一个带数据库的 CRUD 应用,看看效果如何。