Skip to content

🚀 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;
};

模型设计要点:

  1. 数据验证:在模型层进行数据验证,确保数据完整性
  2. 字段约束:合理设置字段类型和长度限制
  3. 错误信息:提供友好的错误提示信息
  4. 时间戳:自动管理创建时间和更新时间

🔄 模型初始化机制 ​

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层设计原则 ​

  1. 单一职责:每个方法只处理一种业务逻辑
  2. 错误处理:统一的错误抛出和处理机制
  3. 数据安全:字段过滤防止恶意数据注入
  4. 代码复用:公共逻辑抽取为独立方法
  5. 参数验证:对输入参数进行必要的验证

🛡️ 工具函数层(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&currentPage=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&currentPage=1&pageSize=5

# 内容搜索
GET /admin/articles?content=教程&currentPage=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。项目采用了现代化的架构设计和最佳实践,具备良好的可维护性、扩展性和安全性。