Express搭配Mongoose开发REST API的Schema与路由设计咨询
方案建议
一、Mongoose Schema 设计选型
你的场景选择反规范化(嵌入)设计更适配,理由如下:
- 文档仅包含2个字段,数据量极小,嵌入不会导致用户单条文档体积超出合理范围
- 文档完全从属于用户,没有跨用户查询文档、全局独立查询文档的需求,嵌入后查询用户数据时可以一次性拿到所有关联文档,不需要额外的
populate联表操作,性能更好 - 操作逻辑更简单,增删改文档只需要操作用户对象的
documents数组即可,不需要维护两个模型的关联一致性 - 后续如果需求迭代,文档字段增多、单用户文档量超过数千、或者产生了独立查询文档的需求,再迁移到规范化引用设计的成本也很低
修改后的models/user.js参考代码:
const userSchema = new mongoose.Schema({ username: { type: String, required: true, unique: true }, password: { type: String, required: true }, // 新增嵌入的documents数组 documents: [{ path: { type: String, required: true }, dateCreated: { type: Date, default: Date.now } }] });
额外提示:生产环境请不要明文存储password字段,建议使用bcrypt等加密工具对密码做哈希处理后再存储
二、API 路由设计选型
推荐你在现有用户路由下新增嵌套子资源端点,也就是/api/users/:userId/documents格式的路径,理由如下:
- 符合REST API的语义规范,路径本身就清晰表达了「文档是用户的从属资源」的逻辑,可读性更强
- 权限校验更方便:用户只能操作自己的文档时,直接拿路由参数中的
userId和当前登录用户的ID做对比即可,不需要额外查询文档所属的用户ID - 代码维护成本更低,和用户相关的资源逻辑都收拢在users路由下,不需要额外拆分文件,适合你的基础需求场景
如果后续出现了不需要关联用户的文档操作(比如全局检索所有公开文档),再新增独立的/api/documents路由即可,两者并不冲突。
新增到routes/users.js的参考代码:
// 获取指定用户的所有文档 router.get('/:userId/documents', async (req, res, next) => { try { const user = await User.findById(req.params.userId).select('documents'); if (!user) return res.status(404).json({ message: '用户不存在' }); res.json(user.documents); } catch (err) { next(err); } }); // 获取指定用户的单个文档 router.get('/:userId/documents/:documentId', async (req, res, next) => { try { const user = await User.findById(req.params.userId); if (!user) return res.status(404).json({ message: '用户不存在' }); const document = user.documents.id(req.params.documentId); if (!document) return res.status(404).json({ message: '文档不存在' }); res.json(document); } catch (err) { next(err); } }); // 为指定用户创建新文档 router.post('/:userId/documents', async (req, res, next) => { try { const user = await User.findById(req.params.userId); if (!user) return res.status(404).json({ message: '用户不存在' }); const newDocumentIndex = user.documents.push(req.body); await user.save(); res.status(201).json(user.documents[newDocumentIndex - 1]); } catch (err) { next(err); } }); // 更新指定用户的单个文档 router.put('/:userId/documents/:documentId', async (req, res, next) => { try { const user = await User.findById(req.params.userId); if (!user) return res.status(404).json({ message: '用户不存在' }); const document = user.documents.id(req.params.documentId); if (!document) return res.status(404).json({ message: '文档不存在' }); // 仅允许修改path字段,dateCreated不支持更新 if (req.body.path) document.path = req.body.path; await user.save(); res.json(document); } catch (err) { next(err); } }); // 删除指定用户的单个文档 router.delete('/:userId/documents/:documentId', async (req, res, next) => { try { const user = await User.findById(req.params.userId); if (!user) return res.status(404).json({ message: '用户不存在' }); user.documents.pull(req.params.documentId); await user.save(); res.json({ message: '文档删除成功' }); } catch (err) { next(err); } });
内容的提问来源于stack exchange,提问作者M. Çağlar TUFAN
相关产品推荐
相关产品推荐

