🚀 NodeJS从零到一开发一个全栈项目 - express增删改查篇
🏗️ 项目架构概览
在上一篇文章中,我们完成了基础环境的搭建。现在让我们深入了解如何构建一个完整的Express增删改查(CRUD)系统。本项目采用分层架构设计,遵循单一职责原则,具备良好的可维护性和扩展性。
📁 项目目录结构解析
project/
├── app.js # 应用程序入口文件
├── models/ # 数据模型层(Sequelize ORM)
│ ├── index.js # 模型初始化和关联配置
│ └── article.js # 文章数据模型
├── routes/ # 路由控制层
│ ├── index.js # 基础路由
│ ├── users.js # 用户路由
│ └── admin/
│ └── articles.js # 文章管理路由(CRUD)
├── services/ # 业务逻辑层
│ ├── index.js # 服务层统一导出
│ └── articleService.js # 文章业务逻辑处理
└── utils/ # 工具函数层
├── response.js # 统一响应格式处理
└── middleware.js # 通用中间件函数🎯 分层架构设计思想
mermaid
graph TD
A[前端请求] --> B[Routes 路由层]
B --> C[Middleware 中间件层]
C --> D[Services 业务逻辑层]
D --> E[Models 数据模型层]
E --> F[Database 数据库]
F --> E
E --> D
D --> G[Utils 响应工具]
G --> B
B --> A各层职责分工:
| 层级 | 职责 | 主要功能 |
|---|---|---|
| Routes | 路由控制 | 接收HTTP请求,参数验证,调用业务逻辑 |
| Services | 业务逻辑 | 核心业务处理,数据操作协调 |
| Models | 数据模型 | 数据结构定义,数据库操作 |
| Utils | 工具函数 | 公共功能,中间件,响应格式化 |
🗄️ 数据模型层(Models)
📋 Article模型设计
数据模型是整个应用的基础,我们的文章模型包含了完整的字段验证规则:
javascript
// models/article.js
'use strict';
const { Model } = require('sequelize');
module.exports = (sequelize, DataTypes) => {
class Article extends Model {
static associate(models) {
// 在这里定义模型关联关系
// 例如:Article.belongsTo(models.User, { foreignKey: 'userId' });
}
}
Article.init({
title: {
type: DataTypes.STRING,
allowNull: false,
validate: {
notNull: { msg: 'Title is required' },
notEmpty: { msg: 'Title is required' },
len: {
args: [2, 30],
msg: 'Title must be between 2 and 30 characters'
}
}
},
content: {
type: DataTypes.TEXT,
allowNull: false,
validate: {
notNull: { msg: 'Content is required' },
notEmpty: { msg: 'Content is required' },
len: {
args: [1, 1000],
msg: 'Content must be between 1 and 1000 characters'
}
}
}
}, {
sequelize,
modelName: 'Article',
// 自动添加 createdAt 和 updatedAt 字段
timestamps: true
});
return Article;
};模型设计要点:
- 数据验证:在模型层进行数据验证,确保数据完整性
- 字段约束:合理设置字段类型和长度限制
- 错误信息:提供友好的错误提示信息
- 时间戳:自动管理创建时间和更新时间
🔄 模型初始化机制
javascript
// models/index.js
'use strict';
const fs = require('fs');
const path = require('path');
const Sequelize = require('sequelize');
const process = require('process');
const basename = path.basename(__filename);
const env = process.env.NODE_ENV || 'development';
const config = require(__dirname + '/../config/config.json')[env];
const db = {};
// 创建Sequelize实例
let sequelize;
if (config.use_env_variable) {
sequelize = new Sequelize(process.env[config.use_env_variable], config);
} else {
sequelize = new Sequelize(config.database, config.username, config.password, config);
}
// 自动加载所有模型文件
fs.readdirSync(__dirname)
.filter(file => {
return (
file.indexOf('.') !== 0 &&
file !== basename &&
file.slice(-3) === '.js' &&
file.indexOf('.test.js') === -1
);
})
.forEach(file => {
const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes);
db[model.name] = model;
});
// 建立模型关联关系
Object.keys(db).forEach(modelName => {
if (db[modelName].associate) {
db[modelName].associate(db);
}
});
db.sequelize = sequelize;
db.Sequelize = Sequelize;
module.exports = db;🛠️ 业务逻辑层(Services)
Service层是整个应用的核心,负责处理所有业务逻辑。让我们详细分析文章服务的实现:
📄 ArticleService类设计
javascript
// services/articleService.js
const { Article } = require('../models');
const { Op } = require('sequelize');
const { NotFoundError } = require('../utils/response');
class ArticleService {
/**
* 获取文章列表(支持搜索和分页)
* @param {Object} params - 查询参数
* @param {string} params.title - 标题搜索关键词
* @param {string} params.content - 内容搜索关键词
* @param {Object} params.pagination - 分页参数
* @returns {Object} 文章列表和分页信息
*/
static async getArticles({ title, content, pagination }) {
const { currentPage, pageSize } = pagination;
// 构建查询条件
const condition = {
where: {},
order: [['id', 'DESC']], // 按ID降序排列
limit: pageSize, // 限制返回数量
offset: (currentPage - 1) * pageSize // 计算偏移量
};
// 动态构建搜索条件
if (title || content) {
condition.where = { [Op.or]: [] };
if (title) {
condition.where[Op.or].push({
title: { [Op.like]: `%${title}%` }
});
}
if (content) {
condition.where[Op.or].push({
content: { [Op.like]: `%${content}%` }
});
}
}
// 执行查询并获取总数
const { rows: articles, count: total } = await Article.findAndCountAll(condition);
return {
articles,
pagination: {
currentPage,
pageSize,
total,
totalPages: Math.ceil(total / pageSize)
}
};
}
/**
* 根据ID获取单个文章
* @param {number} id - 文章ID
* @returns {Object} 文章对象
* @throws {NotFoundError} 文章不存在时抛出错误
*/
static async getArticleById(id) {
const article = await Article.findByPk(id);
if (!article) {
throw new NotFoundError('Article not found');
}
return article;
}
/**
* 创建新文章
* @param {Object} articleData - 文章数据
* @returns {Object} 创建的文章对象
*/
static async createArticle(articleData) {
// 过滤只允许的字段,防止恶意注入
const filteredData = this.filterAllowedFields(articleData);
return await Article.create(filteredData);
}
/**
* 更新文章
* @param {number} id - 文章ID
* @param {Object} articleData - 更新的文章数据
* @returns {Object} 更新后的文章对象
*/
static async updateArticle(id, articleData) {
const article = await this.getArticleById(id);
const filteredData = this.filterAllowedFields(articleData);
await article.update(filteredData);
return article;
}
/**
* 删除文章
* @param {number} id - 文章ID
* @returns {Object} 被删除的文章对象
*/
static async deleteArticle(id) {
const article = await this.getArticleById(id);
await article.destroy();
return article;
}
/**
* 字段过滤器 - 安全性考虑
* @param {Object} data - 原始数据
* @returns {Object} 过滤后的数据
*/
static filterAllowedFields(data) {
const allowedFields = ['title', 'content'];
return Object.fromEntries(
Object.entries(data).filter(([key]) => allowedFields.includes(key))
);
}
}
module.exports = ArticleService;💡 Service层设计原则
- 单一职责:每个方法只处理一种业务逻辑
- 错误处理:统一的错误抛出和处理机制
- 数据安全:字段过滤防止恶意数据注入
- 代码复用:公共逻辑抽取为独立方法
- 参数验证:对输入参数进行必要的验证
🛡️ 工具函数层(Utils)
工具层提供了整个应用的公共功能,包括响应格式化、错误处理和中间件。
📤 统一响应格式
javascript
// utils/response.js
// 自定义错误类
class NotFoundError extends Error {
constructor(message) {
super(message);
this.name = 'NotFoundError';
this.statusCode = 404;
}
}
class ValidationError extends Error {
constructor(message, errors = []) {
super(message);
this.name = 'ValidationError';
this.statusCode = 400;
this.errors = errors;
}
}
/**
* 成功响应格式
* @param {Object} res - Express响应对象
* @param {string} message - 响应消息
* @param {*} data - 响应数据
* @param {number} code - HTTP状态码
*/
function success(res, message, data = {}, code = 200) {
res.status(code).json({
status: true,
message,
data,
timestamp: new Date().toISOString()
});
}
/**
* 分页响应格式
* @param {Object} res - Express响应对象
* @param {string} message - 响应消息
* @param {Array} data - 数据数组
* @param {Object} pagination - 分页信息
*/
function successWithPagination(res, message, data, pagination) {
res.status(200).json({
status: true,
message,
data,
pagination,
timestamp: new Date().toISOString()
});
}
/**
* 错误响应处理
* @param {Object} res - Express响应对象
* @param {Error} error - 错误对象
*/
function failure(res, error) {
let statusCode = 500;
let message = error.message;
let errors = [];
// 根据错误类型设置不同的响应
if (error.name === 'SequelizeValidationError') {
statusCode = 400;
message = 'Validation failed';
errors = error.errors.map(e => e.message);
} else if (error.name === 'NotFoundError') {
statusCode = 404;
} else if (error.name === 'ValidationError') {
statusCode = 400;
errors = error.errors || [];
}
res.status(statusCode).json({
status: false,
message,
errors: errors.length > 0 ? errors : undefined,
timestamp: new Date().toISOString()
});
}🔧 中间件函数集合
javascript
// utils/middleware.js
const { failure } = require('./response');
/**
* 异步路由错误处理包装器
* 自动捕获async/await中的错误并传递给错误处理中间件
*/
const asyncHandler = (fn) => {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
};
/**
* 全局错误处理中间件
* 统一处理所有未捕获的错误
*/
const errorHandler = (err, req, res, next) => {
console.error('错误详情:', err);
failure(res, err);
};
/**
* 分页参数处理中间件
* 解析和验证分页参数
*/
const paginationMiddleware = (req, res, next) => {
const currentPage = parseInt(req.query.currentPage) || 1;
const pageSize = parseInt(req.query.pageSize) || 10;
// 设置合理的分页限制
req.pagination = {
currentPage: Math.max(1, currentPage),
pageSize: Math.min(Math.max(1, pageSize), 100), // 最大100条
};
next();
};
/**
* 参数验证中间件生成器
* @param {Object} validationRules - 验证规则对象
* @returns {Function} 中间件函数
*/
const validateParams = (validationRules) => {
return (req, res, next) => {
const errors = [];
for (const [field, rules] of Object.entries(validationRules)) {
const value = req.body[field] || req.query[field] || req.params[field];
// 必填字段验证
if (rules.required && (!value || value.trim() === '')) {
errors.push(`${field} is required`);
continue;
}
// 最小长度验证
if (value && rules.minLength && value.length < rules.minLength) {
errors.push(`${field} must be at least ${rules.minLength} characters`);
}
// 最大长度验证
if (value && rules.maxLength && value.length > rules.maxLength) {
errors.push(`${field} must not exceed ${rules.maxLength} characters`);
}
}
if (errors.length > 0) {
return res.status(400).json({
status: false,
message: 'Validation failed',
errors
});
}
next();
};
};🛣️ 路由控制层(Routes)
路由层是客户端与服务器交互的入口,负责接收HTTP请求并调用相应的业务逻辑。
📝 文章CRUD路由实现
javascript
// routes/admin/articles.js
const express = require('express');
const router = express.Router();
const ArticleService = require('../../services/articleService');
const { success, successWithPagination } = require('../../utils/response');
const { asyncHandler, paginationMiddleware, validateParams } = require('../../utils/middleware');
// 验证规则定义
const createArticleValidation = {
title: { required: true, minLength: 2, maxLength: 30 },
content: { required: true, minLength: 1, maxLength: 1000 }
};
const updateArticleValidation = {
title: { minLength: 2, maxLength: 30 },
content: { minLength: 1, maxLength: 1000 }
};
/**
* 获取文章列表(支持搜索和分页)
* GET /admin/articles?title=xxx&content=xxx¤tPage=1&pageSize=10
*/
router.get('/', paginationMiddleware, asyncHandler(async (req, res) => {
const { title, content } = req.query;
const { pagination } = req;
const result = await ArticleService.getArticles({ title, content, pagination });
successWithPagination(res, 'Articles fetched successfully', result.articles, result.pagination);
}));
/**
* 获取单个文章
* GET /admin/articles/:id
*/
router.get('/:id', asyncHandler(async (req, res) => {
const { id } = req.params;
const article = await ArticleService.getArticleById(id);
success(res, 'Article fetched successfully', article);
}));
/**
* 创建新文章
* POST /admin/articles
*/
router.post('/', validateParams(createArticleValidation), asyncHandler(async (req, res) => {
const article = await ArticleService.createArticle(req.body);
success(res, 'Article created successfully', article, 201);
}));
/**
* 更新文章
* PUT /admin/articles/:id
*/
router.put('/:id', validateParams(updateArticleValidation), asyncHandler(async (req, res) => {
const { id } = req.params;
const article = await ArticleService.updateArticle(id, req.body);
success(res, 'Article updated successfully', article);
}));
/**
* 删除文章
* DELETE /admin/articles/:id
*/
router.delete('/:id', asyncHandler(async (req, res) => {
const { id } = req.params;
await ArticleService.deleteArticle(id);
success(res, 'Article deleted successfully', null);
}));
module.exports = router;🌐 应用程序入口配置
javascript
// app.js
const express = require('express');
const path = require('path');
const cookieParser = require('cookie-parser');
const logger = require('morgan');
// 路由导入
const indexRouter = require('./routes/index');
const usersRouter = require('./routes/users');
const adminArticlesRouter = require('./routes/admin/articles');
// 工具导入
const { errorHandler } = require('./utils/middleware');
const app = express();
// 中间件配置
app.use(logger('dev')); // 请求日志
app.use(express.json()); // JSON解析
app.use(express.urlencoded({ extended: false })); // URL编码解析
app.use(cookieParser()); // Cookie解析
app.use(express.static(path.join(__dirname, 'public'))); // 静态文件服务
// 路由配置
app.use('/', indexRouter);
app.use('/users', usersRouter);
app.use('/admin/articles', adminArticlesRouter);
// 404 处理
app.use('*', (req, res) => {
res.status(404).json({
status: false,
message: 'Route not found',
timestamp: new Date().toISOString()
});
});
// 全局错误处理中间件(必须放在最后)
app.use(errorHandler);
module.exports = app;🔌 API接口使用指南
📋 完整的CRUD接口清单
| 方法 | 路径 | 功能 | 参数 |
|---|---|---|---|
| GET | /admin/articles | 获取文章列表 | title, content, currentPage, pageSize |
| GET | /admin/articles/:id | 获取单个文章 | id (路径参数) |
| POST | /admin/articles | 创建新文章 | title, content (请求体) |
| PUT | /admin/articles/:id | 更新文章 | id (路径参数), title, content (请求体) |
| DELETE | /admin/articles/:id | 删除文章 | id (路径参数) |
🌐 API使用示例
1. 获取文章列表
bash
# 基础查询
GET /admin/articles?currentPage=1&pageSize=10
# 搜索查询
GET /admin/articles?title=NodeJS¤tPage=1&pageSize=5
# 内容搜索
GET /admin/articles?content=教程¤tPage=1&pageSize=10响应示例:
json
{
"status": true,
"message": "Articles fetched successfully",
"data": [
{
"id": 1,
"title": "NodeJS入门教程",
"content": "这是一个详细的NodeJS学习教程...",
"createdAt": "2025-07-11T10:00:00.000Z",
"updatedAt": "2025-07-11T10:00:00.000Z"
}
],
"pagination": {
"currentPage": 1,
"pageSize": 10,
"total": 25,
"totalPages": 3
},
"timestamp": "2025-07-11T10:30:00.000Z"
}2. 创建新文章
bash
POST /admin/articles
Content-Type: application/json
{
"title": "Express框架详解",
"content": "Express是Node.js最流行的Web框架..."
}响应示例:
json
{
"status": true,
"message": "Article created successfully",
"data": {
"id": 26,
"title": "Express框架详解",
"content": "Express是Node.js最流行的Web框架...",
"createdAt": "2025-07-11T10:35:00.000Z",
"updatedAt": "2025-07-11T10:35:00.000Z"
},
"timestamp": "2025-07-11T10:35:00.000Z"
}3. 更新文章
bash
PUT /admin/articles/26
Content-Type: application/json
{
"title": "Express框架完整指南",
"content": "Express是Node.js最流行且功能强大的Web框架..."
}4. 删除文章
bash
DELETE /admin/articles/26响应示例:
json
{
"status": true,
"message": "Article deleted successfully",
"data": null,
"timestamp": "2025-07-11T10:40:00.000Z"
}❌ 错误响应格式
json
{
"status": false,
"message": "Validation failed",
"errors": [
"Title is required",
"Content must be at least 1 characters"
],
"timestamp": "2025-07-11T10:45:00.000Z"
}🎯 核心特性与亮点
1. 分层架构设计 🏗️
- 职责分离:每层都有明确的职责边界
- 松耦合:层与层之间通过接口交互,便于测试和维护
- 高内聚:相关功能聚合在同一层内
2. 统一响应格式 📤
- 标准化:所有API返回统一的JSON格式
- 错误处理:完善的错误分类和错误信息
- 时间戳:每个响应都包含时间戳信息
3. 参数验证机制 🛡️
- 多层验证:路由层和模型层双重验证
- 自定义验证:灵活的验证规则配置
- 友好提示:清晰的错误提示信息
4. 分页和搜索 🔍
- 性能优化:合理的分页机制防止大数据量查询
- 模糊搜索:支持标题和内容的模糊匹配
- 参数限制:防止恶意的大分页请求
5. 安全性考虑 🔒
- 字段过滤:只允许指定字段的更新
- SQL注入防护:使用ORM参数化查询
- 输入验证:严格的输入数据验证
🚀 扩展和优化建议
1. 性能优化
javascript
// 添加数据库索引
// 在migration文件中添加
await queryInterface.addIndex('Articles', ['title']);
await queryInterface.addIndex('Articles', ['createdAt']);
// 添加缓存机制
const Redis = require('redis');
const redis = Redis.createClient();
// 缓存热门文章
static async getPopularArticles() {
const cacheKey = 'popular_articles';
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
const articles = await Article.findAll({
limit: 10,
order: [['views', 'DESC']]
});
await redis.setex(cacheKey, 3600, JSON.stringify(articles));
return articles;
}2. 功能扩展
javascript
// 添加文章分类
// models/category.js
class Category extends Model {
static associate(models) {
Category.hasMany(models.Article, { foreignKey: 'categoryId' });
}
}
// models/article.js
static associate(models) {
Article.belongsTo(models.Category, { foreignKey: 'categoryId' });
}
// 添加软删除
// models/article.js
Article.init({
// ... 其他字段
deletedAt: {
type: DataTypes.DATE,
allowNull: true
}
}, {
sequelize,
modelName: 'Article',
paranoid: true // 启用软删除
});3. 测试和监控
javascript
// 添加API测试
// tests/article.test.js
const request = require('supertest');
const app = require('../app');
describe('Article API', () => {
test('should create a new article', async () => {
const response = await request(app)
.post('/admin/articles')
.send({
title: 'Test Article',
content: 'This is a test article'
});
expect(response.status).toBe(201);
expect(response.body.status).toBe(true);
});
});
// 添加API日志监控
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: 'api.log' })
]
});
// 在路由中使用
router.post('/', asyncHandler(async (req, res) => {
logger.info('Creating article', { body: req.body });
// ... 业务逻辑
}));通过这个完整的Express增删改查项目模板,你可以快速构建具备企业级特性的Web API。项目采用了现代化的架构设计和最佳实践,具备良好的可维护性、扩展性和安全性。