Node.js+MongoDB搭建RESTful API实战示例
作者:夜雨hiyeyu.com
第一章:引言与概述
1.1 为什么选择 Node.js 和 MongoDB
在当今的 Web 开发领域,Node.js 和 MongoDB 已经成为构建现代应用程序的首选技术组合。Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时环境,它采用事件驱动、非阻塞 I/O 模型,使其轻量且高效。MongoDB 是一个基于分布式文件存储的 NoSQL 数据库,由 C++ 语言编写,旨在为 Web 应用提供可扩展的高性能数据存储解决方案。
这个技术栈的优势体现在多个方面。首先,JavaScript 全栈开发使得前后端开发人员能够使用同一种语言,降低了学习成本和上下文切换的开销。其次,JSON 数据格式在两者之间的无缝流转——Node.js 使用 JSON 作为数据交换格式,MongoDB 使用 BSON(Binary JSON)存储数据——这种一致性大大简化了开发流程。第三,非阻塞异步特性让 Node.js 特别适合处理高并发的 I/O 密集型应用,而 MongoDB 的横向扩展能力能够很好地支持这种应用场景。
1.2 RESTful API 设计原则
REST(Representational State Transfer)是一种软件架构风格,而不是标准或协议。它由 Roy Fielding 在 2000 年的博士论文中提出,定义了一组约束和原则,用于创建可扩展、可靠和高效的 Web 服务。
RESTful API 的核心原则包括:
- 无状态性(Stateless):每个请求都包含处理该请求所需的所有信息,服务器不存储客户端的状态信息
- 统一接口(Uniform Interface):使用标准的 HTTP 方法(GET、POST、PUT、DELETE 等)和状态码
- 资源导向(Resource-Based):所有内容都被抽象为资源,每个资源有唯一的标识符(URI)
- 表述性(Representation):客户端与服务器交换的是资源的表述,而不是资源本身
- 可缓存性(Cacheable):响应应该被标记为可缓存或不可缓存,以提高性能
- 分层系统(Layered System):客户端不需要知道是否直接连接到最后端的服务器
1.3 教程目标与内容概述
本教程将带领您从零开始构建一个完整的博客平台 API,实现文章的增删改查、用户认证、文件上传、分页查询等核心功能。通过这个实践项目,您将掌握:
- Node.js 和 Express 框架的核心概念和用法
- MongoDB 数据库的设计和操作
- Mongoose ODM 库的高级用法
- RESTful API 的设计原则和最佳实践
- JWT 身份认证和授权机制
- 错误处理、日志记录和性能优化
- API 测试和文档编写
- 项目部署和运维考虑
第二章:环境搭建与项目初始化
2.1 开发环境要求
在开始之前,请确保您的系统满足以下要求:
操作系统要求:
- Windows 7 或更高版本
- macOS 10.10 或更高版本
- Ubuntu 16.04 或更高版本(推荐 LTS 版本)
软件依赖: - Node.js 版本 14.0.0 或更高版本(推荐 LTS 版本)
- MongoDB 版本 4.0 或更高版本
- npm 版本 6.0.0 或更高版本
- Git 版本控制工具
开发工具推荐: - 代码编辑器:Visual Studio Code(推荐)、WebStorm、Sublime Text
- API 测试工具:Postman、Insomnia、Thunder Client(VSCode 扩展)
- 数据库管理工具:MongoDB Compass、Studio 3T
- 命令行工具:Windows Terminal、iTerm2(macOS)、Git Bash
2.2 安装和配置 Node.js
Windows 系统安装:
访问 Node.js 官网(https://nodejs.org/)
下载 LTS 版本的安装程序
运行安装程序,按照向导完成安装
安装完成后,打开命令提示符或 PowerShell,验证安装:
node --version npm --version
macOS 系统安装:
推荐使用 Homebrew 包管理器:
# 安装 Homebrew(如果尚未安装) /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" # 使用 Homebrew 安装 Node.js brew install node
Linux(Ubuntu)系统安装:
# 使用 NodeSource 安装脚本 curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - sudo apt-get install -y nodejs
2.3 安装和配置 MongoDB
本地安装 MongoDB:
- 访问 MongoDB 官网(https://www.mongodb.com/try/download/community)
- 选择适合您操作系统的版本下载
- 按照官方文档完成安装和配置
使用 MongoDB Atlas(云数据库): - 访问 https://www.mongodb.com/atlas/database
- 注册账号并创建免费集群
- 配置网络访问和白名单
- 获取连接字符串
使用 Docker 运行 MongoDB:
# 拉取 MongoDB 镜像 docker pull mongo:latest # 运行 MongoDB 容器 docker run --name mongodb -d -p 27017:27017 -v ~/mongo/data:/data/db mongo:latest # 带认证的启动方式 docker run --name mongodb -d -p 27017:27017 -e MONGO_INITDB_ROOT_USERNAME=admin -e MONGO_INITDB_ROOT_PASSWORD=password -v ~/mongo/data:/data/db mongo:latest
2.4 项目初始化与结构设计
创建项目目录并初始化:
# 创建项目目录 mkdir blog-api cd blog-api # 初始化 npm 项目 npm init -y # 创建项目目录结构 mkdir -p src/ mkdir -p src/controllers mkdir -p src/models mkdir -p src/routes mkdir -p src/middleware mkdir -p src/utils mkdir -p src/config mkdir -p tests mkdir -p docs # 创建基础文件 touch src/app.js touch src/server.js touch .env touch .gitignore touch README.md
安装项目依赖:
# 生产依赖 npm install express mongoose dotenv bcryptjs jsonwebtoken cors helmet morgan multer express-rate-limit express-validator # 开发依赖 npm install --save-dev nodemon eslint prettier eslint-config-prettier eslint-plugin-prettier jest supertest mongodb-memory-server
配置 package.json 脚本:
{ "scripts": { "start": "node src/server.js", "dev": "nodemon src/server.js", "test": "jest", "test:watch": "jest --watch", "lint": "eslint src/**/*.js", "lint:fix": "eslint src/**/*.js --fix", "format": "prettier --write src/**/*.js" } }
配置 .gitignore 文件:
# 依赖目录 node_modules/ # 环境变量文件 .env .env.local .env.development.local .env.test.local .env.production.local # 日志文件 logs/ *.log npm-debug.log* yarn-debug.log* yarn-error.log* # 运行时数据 pids/ *.pid *.seed *.pid.lock # 覆盖率目录 coverage/ .nyc_output # 系统文件 .DS_Store Thumbs.db # IDE文件 .vscode/ .idea/ *.swp *.swo # 操作系统文件 *.DS_Store Thumbs.db
第三章:Express 服务器基础搭建
3.1 创建基本的 Express 服务器
首先创建主要的应用文件 src/app.js:
const express = require('express'); const cors = require('cors'); const helmet = require('helmet'); const morgan = require('morgan'); const rateLimit = require('express-rate-limit'); require('dotenv').config(); // 导入路由 const postRoutes = require('./routes/posts'); const authRoutes = require('./routes/auth'); const userRoutes = require('./routes/users'); // 导入中间件 const errorHandler = require('./middleware/errorHandler'); const notFound = require('./middleware/notFound'); const app = express(); // 安全中间件 app.use(helmet()); // CORS 配置 app.use(cors({ origin: process.env.NODE_ENV === 'production' ? process.env.FRONTEND_URL : 'http://localhost:3000', credentials: true })); // 速率限制 const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15分钟 max: 100, // 限制每个IP每15分钟最多100个请求 message: { error: '请求过于频繁,请稍后再试。', status: 429 } }); app.use(limiter); // 日志记录 app.use(morgan(process.env.NODE_ENV === 'production' ? 'combined' : 'dev')); // 解析请求体 app.use(express.json({ limit: '10mb' })); app.use(express.urlencoded({ extended: true, limit: '10mb' })); // 静态文件服务 app.use('/uploads', express.static('uploads')); // 路由配置 app.use('/api/posts', postRoutes); app.use('/api/auth', authRoutes); app.use('/api/users', userRoutes); // 健康检查端点 app.get('/api/health', (req, res) => { res.status(200).json({ status: 'OK', timestamp: new Date().toISOString(), uptime: process.uptime(), environment: process.env.NODE_ENV || 'development' }); }); // 404处理 app.use(notFound); // 错误处理 app.use(errorHandler); module.exports = app;
创建服务器启动文件 src/server.js:
const app = require('./app'); const connectDB = require('./config/database'); // 环境变量配置 const PORT = process.env.PORT || 5000; const NODE_ENV = process.env.NODE_ENV || 'development'; // 优雅关闭处理 const gracefulShutdown = (signal) => { console.log(`收到 ${signal},开始优雅关闭服务器...`); process.exit(0); }; // 启动服务器 const startServer = async () => { try { // 连接数据库 await connectDB(); // 启动Express服务器 const server = app.listen(PORT, () => { console.log(` 🚀 服务器已启动! 📍 环境: ${NODE_ENV} 📍 端口: ${PORT} 📍 时间: ${new Date().toLocaleString()} 📍 健康检查: http://localhost:${PORT}/api/health `); }); // 优雅关闭处理 process.on('SIGINT', () => gracefulShutdown('SIGINT')); process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); // 处理未捕获的异常 process.on('uncaughtException', (error) => { console.error('未捕获的异常:', error); gracefulShutdown('uncaughtException'); }); process.on('unhandledRejection', (reason, promise) => { console.error('未处理的Promise拒绝:', reason); gracefulShutdown('unhandledRejection'); }); } catch (error) { console.error('服务器启动失败:', error); process.exit(1); } }; // 启动应用 startServer();
3.2 环境变量配置
创建 .env 文件:
# 服务器配置 NODE_ENV=development PORT=5000 FRONTEND_URL=http://localhost:3000 # 数据库配置 MONGODB_URI=mongodb://localhost:27017/blog_api MONGODB_URI_TEST=mongodb://localhost:27017/blog_api_test # JWT配置 JWT_SECRET=your_super_secret_jwt_key_here_change_in_production JWT_EXPIRE=7d JWT_COOKIE_EXPIRE=7 # 文件上传配置 MAX_FILE_UPLOAD=5 FILE_UPLOAD_PATH=./uploads # 速率限制配置 RATE_LIMIT_WINDOW_MS=900000 RATE_LIMIT_MAX=100 # 邮件配置(可选) SMTP_HOST=smtp.gmail.com SMTP_PORT=587 SMTP_EMAIL=your_email@gmail.com SMTP_PASSWORD=your_app_password FROM_EMAIL=noreply@blogapi.com FROM_NAME=Blog API
创建环境配置工具 src/config/env.js:
const Joi = require('joi'); // 环境变量验证规则 const envVarsSchema = Joi.object({ NODE_ENV: Joi.string() .valid('development', 'production', 'test') .default('development'), PORT: Joi.number().default(5000), MONGODB_URI: Joi.string().required().description('MongoDB连接字符串'), JWT_SECRET: Joi.string().required().description('JWT密钥'), JWT_EXPIRE: Joi.string().default('7d').description('JWT过期时间'), }).unknown().required(); // 验证环境变量 const { value: envVars, error } = envVarsSchema.validate(process.env); if (error) { throw new Error(`环境变量配置错误: ${error.message}`); } // 导出配置对象 module.exports = { env: envVars.NODE_ENV, port: envVars.PORT, mongoose: { url: envVars.MONGODB_URI + (envVars.NODE_ENV === 'test' ? '_test' : ''), options: { useNewUrlParser: true, useUnifiedTopology: true, }, }, jwt: { secret: envVars.JWT_SECRET, expire: envVars.JWT_EXPIRE, }, };
第四章:MongoDB 数据库连接与配置
4.1 数据库连接配置
创建数据库连接文件 src/config/database.js:
const mongoose = require('mongoose'); const config = require('./env'); const connectDB = async () => { try { const conn = await mongoose.connect(config.mongoose.url, config.mongoose.options); console.log(` ✅ MongoDB连接成功! 📍 主机: ${conn.connection.host} 📍 数据库: ${conn.connection.name} 📍 状态: ${conn.connection.readyState === 1 ? '已连接' : '断开'} 📍 时间: ${new Date().toLocaleString()} `); // 监听连接事件 mongoose.connection.on('connected', () => { console.log('Mongoose已连接到数据库'); }); mongoose.connection.on('error', (err) => { console.error('Mongoose连接错误:', err); }); mongoose.connection.on('disconnected', () => { console.log('Mongoose已断开数据库连接'); }); // 进程关闭时关闭数据库连接 process.on('SIGINT', async () => { await mongoose.connection.close(); console.log('Mongoose连接已通过应用终止关闭'); process.exit(0); }); } catch (error) { console.error('❌ MongoDB连接失败:', error.message); process.exit(1); } }; module.exports = connectDB;
4.2 数据库连接优化
创建高级数据库配置 src/config/databaseAdvanced.js:
const mongoose = require('mongoose'); const config = require('./env'); class DatabaseManager { constructor() { this.isConnected = false; this.connection = null; this.retryAttempts = 0; this.maxRetryAttempts = 5; this.retryDelay = 5000; // 5秒 } async connect() { try { // 连接选项配置 const options = { ...config.mongoose.options, poolSize: 10, // 连接池大小 bufferMaxEntries: 0, // 禁用缓冲 connectTimeoutMS: 10000, // 10秒连接超时 socketTimeoutMS: 45000, // 45秒套接字超时 family: 4, // 使用IPv4 useCreateIndex: true, useFindAndModify: false }; this.connection = await mongoose.connect(config.mongoose.url, options); this.isConnected = true; this.retryAttempts = 0; this.setupEventListeners(); return this.connection; } catch (error) { console.error('数据库连接失败:', error.message); if (this.retryAttempts < this.maxRetryAttempts) { this.retryAttempts++; console.log(`尝试重新连接 (${this.retryAttempts}/${this.maxRetryAttempts})...`); await new Promise(resolve => setTimeout(resolve, this.retryDelay)); return this.connect(); } else { throw new Error(`数据库连接失败,已达到最大重试次数: ${this.maxRetryAttempts}`); } } } setupEventListeners() { mongoose.connection.on('connected', () => { console.log('Mongoose已连接到数据库'); this.isConnected = true; }); mongoose.connection.on('error', (error) => { console.error('Mongoose连接错误:', error); this.isConnected = false; }); mongoose.connection.on('disconnected', () => { console.log('Mongoose已断开数据库连接'); this.isConnected = false; }); mongoose.connection.on('reconnected', () => { console.log('Mongoose已重新连接到数据库'); this.isConnected = true; }); } async disconnect() { if (this.isConnected) { await mongoose.disconnect(); this.isConnected = false; console.log('Mongoose连接已关闭'); } } getConnectionStatus() { return { isConnected: this.isConnected, readyState: mongoose.connection.readyState, host: mongoose.connection.host, name: mongoose.connection.name, retryAttempts: this.retryAttempts }; } } // 创建单例实例 const databaseManager = new DatabaseManager(); module.exports = databaseManager;
4.3 数据库健康检查中间件
创建数据库健康检查中间件 src/middleware/dbHealthCheck.js:
const mongoose = require('mongoose'); const dbHealthCheck = async (req, res, next) => { try { const dbState = mongoose.connection.readyState; // readyState 值说明: // 0 = disconnected // 1 = connected // 2 = connecting // 3 = disconnecting if (dbState !== 1) { return res.status(503).json({ success: false, error: '数据库连接异常', details: { status: dbState, statusText: ['断开连接', '已连接', '连接中', '断开中'][dbState], timestamp: new Date().toISOString() } }); } // 执行简单的查询来验证数据库响应 await mongoose.connection.db.admin().ping(); next(); } catch (error) { res.status(503).json({ success: false, error: '数据库健康检查失败', details: { message: error.message, timestamp: new Date().toISOString() } }); } }; module.exports = dbHealthCheck;
第五章:数据模型设计与 Mongoose 进阶
5.1 用户模型设计
创建用户模型 src/models/User.js:
const mongoose = require('mongoose'); const bcrypt = require('bcryptjs'); const jwt = require('jsonwebtoken'); const config = require('../config/env'); const userSchema = new mongoose.Schema({ username: { type: String, required: [true, '用户名不能为空'], unique: true, trim: true, minlength: [3, '用户名至少3个字符'], maxlength: [20, '用户名不能超过20个字符'], match: [/^[a-zA-Z0-9_]+$/, '用户名只能包含字母、数字和下划线'] }, email: { type: String, required: [true, '邮箱不能为空'], unique: true, lowercase: true, trim: true, match: [/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/, '请输入有效的邮箱地址'] }, password: { type: String, required: [true, '密码不能为空'], minlength: [6, '密码至少6个字符'], select: false // 默认不返回密码字段 }, role: { type: String, enum: ['user', 'author', 'admin'], default: 'user' }, avatar: { type: String, default: 'default-avatar.png' }, bio: { type: String, maxlength: [500, '个人简介不能超过500个字符'], default: '' }, website: { type: String, match: [/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/, '请输入有效的网址'] }, isVerified: { type: Boolean, default: false }, isActive: { type: Boolean, default: true }, lastLogin: { type: Date, default: Date.now } }, { timestamps: true, toJSON: { virtuals: true }, toObject: { virtuals: true } }); // 虚拟字段:用户的文章 userSchema.virtual('posts', { ref: 'Post', localField: '_id', foreignField: 'author', justOne: false }); // 索引优化 userSchema.index({ email: 1 }); userSchema.index({ username: 1 }); userSchema.index({ createdAt: 1 }); // 密码加密中间件 userSchema.pre('save', async function(next) { if (!this.isModified('password')) return next(); try { const salt = await bcrypt.genSalt(12); this.password = await bcrypt.hash(this.password, salt); next(); } catch (error) { next(error); } }); // 比较密码方法 userSchema.methods.comparePassword = async function(candidatePassword) { return await bcrypt.compare(candidatePassword, this.password); }; // 生成JWT令牌方法 userSchema.methods.generateAuthToken = function() { return jwt.sign( { userId: this._id, role: this.role }, config.jwt.secret, { expiresIn: config.jwt.expire, issuer: 'blog-api', audience: 'blog-api-users' } ); }; // 获取用户基本信息(不包含敏感信息) userSchema.methods.getPublicProfile = function() { const userObject = this.toObject(); delete userObject.password; delete userObject.__v; return userObject; }; // 静态方法:通过邮箱查找用户 userSchema.statics.findByEmail = function(email) { return this.findOne({ email: email.toLowerCase() }); }; // 静态方法:通过用户名查找用户 userSchema.statics.findByUsername = function(username) { return this.findOne({ username: new RegExp('^' + username + '$', 'i') }); }; // 查询中间件:自动过滤已删除的用户 userSchema.pre(/^find/, function(next) { this.find({ isActive: { $ne: false } }); next(); }); module.exports = mongoose.model('User', userSchema);
5.2 文章模型设计
创建文章模型 src/models/Post.js:
const mongoose = require('mongoose'); const slugify = require('slugify'); const postSchema = new mongoose.Schema({ title: { type: String, required: [true, '文章标题不能为空'], trim: true, minlength: [5, '文章标题至少5个字符'], maxlength: [200, '文章标题不能超过200个字符'] }, slug: { type: String, unique: true, lowercase: true }, content: { type: String, required: [true, '文章内容不能为空'], minlength: [50, '文章内容至少50个字符'], maxlength: [20000, '文章内容不能超过20000个字符'] }, excerpt: { type: String, maxlength: [300, '文章摘要不能超过300个字符'] }, coverImage: { type: String, default: 'default-cover.jpg' }, author: { type: mongoose.Schema.ObjectId, ref: 'User', required: true }, tags: [{ type: String, trim: true, lowercase: true }], category: { type: String, required: [true, '文章分类不能为空'], trim: true, enum: [ 'technology', 'programming', 'design', 'business', 'lifestyle', 'travel', 'food', 'health', 'education' ] }, status: { type: String, enum: ['draft', 'published', 'archived'], default: 'draft' }, isFeatured: { type: Boolean, default: false }, viewCount: { type: Number, default: 0 }, likeCount: { type: Number, default: 0 }, commentCount: { type: Number, default: 0 }, readingTime: { type: Number, // 阅读时间(分钟) default: 0 }, meta: { title: String, description: String, keywords: [String] }, publishedAt: Date }, { timestamps: true, toJSON: { virtuals: true }, toObject: { virtuals: true } }); // 虚拟字段:评论 postSchema.virtual('comments', { ref: 'Comment', localField: '_id', foreignField: 'post', justOne: false }); // 虚拟字段:点赞用户 postSchema.virtual('likes', { ref: 'Like', localField: '_id', foreignField: 'post', justOne: false }); // 索引优化 postSchema.index({ title: 'text', content: 'text' }); postSchema.index({ author: 1, createdAt: -1 }); postSchema.index({ category: 1, status: 1 }); postSchema.index({ tags: 1 }); postSchema.index({ status: 1, publishedAt: -1 }); postSchema.index({ slug: 1 }); // 生成slug中间件 postSchema.pre('save', function(next) { if (this.isModified('title') && this.title) { this.slug = slugify(this.title, { lower: true, strict: true, remove: /[*+~.()'"!:@]/g }); } next(); }); // 计算阅读时间和摘要中间件 postSchema.pre('save', function(next) { if (this.isModified('content')) { // 计算阅读时间(按每分钟200字计算) const wordCount = this.content.trim().split(/\s+/).length; this.readingTime = Math.ceil(wordCount / 200); // 自动生成摘要 if (!this.excerpt) { this.excerpt = this.content .replace(/[#*`~>]/g, '') // 移除Markdown标记 .substring(0, 200) .trim() + '...'; } } next(); }); // 发布文章时设置发布时间 postSchema.pre('save', function(next) { if (this.isModified('status') && this.status === 'published' && !this.publishedAt) { this.publishedAt = new Date(); } next(); }); // 静态方法:获取已发布文章 postSchema.statics.getPublishedPosts = function() { return this.find({ status: 'published' }); }; // 静态方法:按分类获取文章 postSchema.statics.getPostsByCategory = function(category) { return this.find({ category: category.toLowerCase(), status: 'published' }); }; // 静态方法:搜索文章 postSchema.statics.searchPosts = function(query) { return this.find({ status: 'published', $text: { $search: query } }, { score: { $meta: 'textScore' } }) .sort({ score: { $meta: 'textScore' } }); }; // 实例方法:增加浏览量 postSchema.methods.incrementViews = function() { this.viewCount += 1; return this.save(); }; // 查询中间件:自动填充作者信息 postSchema.pre(/^find/, function(next) { this.populate({ path: 'author', select: 'username avatar bio' }); next(); }); module.exports = mongoose.model('Post', postSchema);
5.3 评论和点赞模型
创建评论模型 src/models/Comment.js:
const mongoose = require('mongoose'); const commentSchema = new mongoose.Schema({ content: { type: String, required: [true, '评论内容不能为空'], trim: true, minlength: [1, '评论内容至少1个字符'], maxlength: [1000, '评论内容不能超过1000个字符'] }, author: { type: mongoose.Schema.ObjectId, ref: 'User', required: true }, post: { type: mongoose.Schema.ObjectId, ref: 'Post', required: true }, parentComment: { type: mongoose.Schema.ObjectId, ref: 'Comment', default: null }, likes: { type: Number, default: 0 }, isEdited: { type: Boolean, default: false }, isApproved: { type: Boolean, default: true } }, { timestamps: true, toJSON: { virtuals: true }, toObject: { virtuals: true } }); // 虚拟字段:回复评论 commentSchema.virtual('replies', { ref: 'Comment', localField: '_id', foreignField: 'parentComment', justOne: false }); // 索引优化 commentSchema.index({ post: 1, createdAt: -1 }); commentSchema.index({ author: 1 }); commentSchema.index({ parentComment: 1 }); // 保存后更新文章的评论计数 commentSchema.post('save', async function() { const Post = mongoose.model('Post'); await Post.findByIdAndUpdate(this.post, { $inc: { commentCount: 1 } }); }); // 删除后更新文章的评论计数 commentSchema.post('findOneAndDelete', async function(doc) { if (doc) { const Post = mongoose.model('Post'); await Post.findByIdAndUpdate(doc.post, { $inc: { commentCount: -1 } }); } }); // 查询中间件:自动填充作者信息 commentSchema.pre(/^find/, function(next) { this.populate({ path: 'author', select: 'username avatar' }).populate({ path: 'replies', populate: { path: 'author', select: 'username avatar' } }); next(); }); module.exports = mongoose.model('Comment', commentSchema);
创建点赞模型 src/models/Like.js:
const mongoose = require('mongoose'); const likeSchema = new mongoose.Schema({ user: { type: mongoose.Schema.ObjectId, ref: 'User', required: true }, post: { type: mongoose.Schema.ObjectId, ref: 'Post', required: true }, type: { type: String, enum: ['like', 'love', 'laugh', 'wow', 'sad', 'angry'], default: 'like' } }, { timestamps: true }); // 复合唯一索引,确保一个用户只能对一篇文章点一次赞 likeSchema.index({ user: 1, post: 1 }, { unique: true }); // 索引优化 likeSchema.index({ post: 1 }); likeSchema.index({ user: 1 }); // 保存后更新文章的点赞计数 likeSchema.post('save', async function() { const Post = mongoose.model('Post'); await Post.findByIdAndUpdate(this.post, { $inc: { likeCount: 1 } }); }); // 删除后更新文章的点赞计数 likeSchema.post('findOneAndDelete', async function(doc) { if (doc) { const Post = mongoose.model('Post'); await Post.findByIdAndUpdate(doc.post, { $inc: { likeCount: -1 } }); } }); module.exports = mongoose.model('Like', likeSchema);
到此这篇关于Node.js+MongoDB搭建RESTful API实战示例的文章就介绍到这了,更多相关Node.js搭建RESTful API内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!