Node.js RESTful API 开发实战:从零搭建企业级后端服务

项目初始化与目录结构

一个良好的项目结构是可维护性的基础。下面是一个经过实践验证的 Express.js 项目结构:

mkdir my-api && cd my-api
npm init -y
npm install express cors helmet morgan dotenv
npm install -D nodemon
my-api/
├── src/
│   ├── config/          # 配置文件
│   │   └── database.js
│   ├── controllers/     # 控制器(业务逻辑)
│   │   └── userController.js
│   ├── middleware/       # 中间件
│   │   ├── auth.js
│   │   ├── errorHandler.js
│   │   └── validate.js
│   ├── models/          # 数据模型
│   │   └── User.js
│   ├── routes/          # 路由定义
│   │   ├── index.js
│   │   └── userRoutes.js
│   ├── utils/           # 工具函数
│   │   └── ApiError.js
│   └── app.js           # Express 应用入口
├── .env                 # 环境变量
├── .gitignore
└── package.json

Express 应用基础搭建

// src/app.js
const express = require('express')
const cors = require('cors')
const helmet = require('helmet')
const morgan = require('morgan')
const routes = require('./routes')
const errorHandler = require('./middleware/errorHandler')

const app = express()

// 安全中间件
app.use(helmet())
app.use(cors({ origin: process.env.CORS_ORIGIN || '*' }))

// 请求解析
app.use(express.json({ limit: '10mb' }))
app.use(express.urlencoded({ extended: true }))

// 日志
app.use(morgan('dev'))

// API 路由
app.use('/api', routes)

// 健康检查
app.get('/health', (req, res) => {
  res.json({ status: 'ok', uptime: process.uptime() })
})

// 404 处理
app.use((req, res) => {
  res.status(404).json({ error: '接口不存在' })
})

// 全局错误处理
app.use(errorHandler)

module.exports = app

路由设计 —— RESTful 规范

RESTful API 的核心是用 HTTP 方法表示操作,用 URL 表示资源:

方法路径说明状态码
GET/api/users获取用户列表200
GET/api/users/:id获取单个用户200 / 404
POST/api/users创建用户201
PUT/api/users/:id更新用户(全量)200 / 404
PATCH/api/users/:id更新用户(部分)200 / 404
DELETE/api/users/:id删除用户204 / 404
// src/routes/userRoutes.js
const router = require('express').Router()
const ctrl = require('../controllers/userController')
const auth = require('../middleware/auth')
const validate = require('../middleware/validate')
const { createUserSchema, updateUserSchema } = require('../validations/user')

router.get('/', ctrl.list)
router.get('/:id', ctrl.getById)
router.post('/', validate(createUserSchema), ctrl.create)
router.put('/:id', auth, validate(updateUserSchema), ctrl.update)
router.delete('/:id', auth, ctrl.remove)

module.exports = router

// src/routes/index.js
const router = require('express').Router()
router.use('/users', require('./userRoutes'))
module.exports = router

控制器 —— 业务逻辑层

// src/controllers/userController.js
const User = require('../models/User')
const ApiError = require('../utils/ApiError')

exports.list = async (req, res, next) => {
  try {
    const { page = 1, limit = 20, keyword } = req.query
    const filter = {}

    if (keyword) {
      filter.$or = [
        { name: new RegExp(keyword, 'i') },
        { email: new RegExp(keyword, 'i') }
      ]
    }

    const [users, total] = await Promise.all([
      User.find(filter)
        .skip((page - 1) * limit)
        .limit(Number(limit))
        .sort({ createdAt: -1 })
        .select('-password'),
      User.countDocuments(filter)
    ])

    res.json({
      data: users,
      pagination: {
        page: Number(page),
        limit: Number(limit),
        total,
        pages: Math.ceil(total / limit)
      }
    })
  } catch (err) {
    next(err)
  }
}

exports.getById = async (req, res, next) => {
  try {
    const user = await User.findById(req.params.id).select('-password')
    if (!user) throw new ApiError(404, '用户不存在')
    res.json({ data: user })
  } catch (err) {
    next(err)
  }
}

exports.create = async (req, res, next) => {
  try {
    const exists = await User.findOne({ email: req.body.email })
    if (exists) throw new ApiError(409, '邮箱已注册')

    const user = await User.create(req.body)
    const { password, ...data } = user.toObject()
    res.status(201).json({ data })
  } catch (err) {
    next(err)
  }
}

exports.update = async (req, res, next) => {
  try {
    const user = await User.findByIdAndUpdate(
      req.params.id,
      { $set: req.body },
      { new: true, runValidators: true }
    ).select('-password')

    if (!user) throw new ApiError(404, '用户不存在')
    res.json({ data: user })
  } catch (err) {
    next(err)
  }
}

exports.remove = async (req, res, next) => {
  try {
    const user = await User.findByIdAndDelete(req.params.id)
    if (!user) throw new ApiError(404, '用户不存在')
    res.status(204).send()
  } catch (err) {
    next(err)
  }
}

中间件系统

统一错误处理

// src/utils/ApiError.js
class ApiError extends Error {
  constructor(statusCode, message) {
    super(message)
    this.statusCode = statusCode
    this.isOperational = true
  }
}

module.exports = ApiError

// src/middleware/errorHandler.js
module.exports = (err, req, res, next) => {
  const statusCode = err.statusCode || 500
  const message = err.isOperational ? err.message : '服务器内部错误'

  // 开发环境打印完整错误
  if (process.env.NODE_ENV !== 'production') {
    console.error('Error:', err)
  }

  res.status(statusCode).json({
    error: message,
    ...(process.env.NODE_ENV !== 'production' && { stack: err.stack })
  })
}

JWT 认证中间件

// src/middleware/auth.js
const jwt = require('jsonwebtoken')

module.exports = (req, res, next) => {
  const header = req.headers.authorization
  if (!header || !header.startsWith('Bearer ')) {
    return res.status(401).json({ error: '未提供认证令牌' })
  }

  try {
    const token = header.split(' ')[1]
    const decoded = jwt.verify(token, process.env.JWT_SECRET)
    req.user = decoded
    next()
  } catch (err) {
    res.status(401).json({ error: '令牌无效或已过期' })
  }
}

请求验证中间件

// src/middleware/validate.js
module.exports = (schema) => (req, res, next) => {
  const { error } = schema.validate(req.body, { abortEarly: false })
  if (error) {
    const messages = error.details.map(d => d.message)
    return res.status(400).json({ error: '参数验证失败', details: messages })
  }
  next()
}

// src/validations/user.js (使用 Joi)
const Joi = require('joi')

exports.createUserSchema = Joi.object({
  name: Joi.string().min(2).max(50).required(),
  email: Joi.string().email().required(),
  password: Joi.string().min(6).max(100).required()
})

exports.updateUserSchema = Joi.object({
  name: Joi.string().min(2).max(50),
  email: Joi.string().email()
}).min(1) // 至少需要一个字段

数据库模型(Mongoose)

// src/models/User.js
const mongoose = require('mongoose')
const bcrypt = require('bcryptjs')

const userSchema = new mongoose.Schema({
  name: {
    type: String,
    required: [true, '用户名不能为空'],
    trim: true,
    minlength: 2,
    maxlength: 50
  },
  email: {
    type: String,
    required: [true, '邮箱不能为空'],
    unique: true,
    lowercase: true,
    match: [/^\S+@\S+\.\S+$/, '请输入有效的邮箱地址']
  },
  password: {
    type: String,
    required: [true, '密码不能为空'],
    minlength: 6,
    select: false // 查询时默认不返回密码
  },
  role: {
    type: String,
    enum: ['user', 'admin'],
    default: 'user'
  }
}, {
  timestamps: true // 自动添加 createdAt, updatedAt
})

// 保存前自动加密密码
userSchema.pre('save', async function(next) {
  if (!this.isModified('password')) return next()
  this.password = await bcrypt.hash(this.password, 12)
  next()
})

// 实例方法:验证密码
userSchema.methods.comparePassword = async function(candidatePassword) {
  return bcrypt.compare(candidatePassword, this.password)
}

module.exports = mongoose.model('User', userSchema)

启动入口

// src/server.js
require('dotenv').config()
const mongoose = require('mongoose')
const app = require('./app')

const PORT = process.env.PORT || 3000
const MONGO_URI = process.env.MONGO_URI || 'mongodb://localhost:27017/myapi'

mongoose.connect(MONGO_URI)
  .then(() => {
    console.log('数据库连接成功')
    app.listen(PORT, () => {
      console.log(`API 服务运行在 http://localhost:${PORT}`)
    })
  })
  .catch(err => {
    console.error('数据库连接失败:', err.message)
    process.exit(1)
  })
// package.json scripts
{
  "scripts": {
    "dev": "nodemon src/server.js",
    "start": "node src/server.js"
  }
}

使用 curl 测试 API

# 创建用户
curl -X POST http://localhost:3000/api/users \
  -H "Content-Type: application/json" \
  -d '{"name":"博豪","email":"bohao@example.com","password":"123456"}'

# 获取用户列表(带分页和搜索)
curl "http://localhost:3000/api/users?page=1&limit=10&keyword=博豪"

# 获取单个用户
curl http://localhost:3000/api/users/60f7b3b5e6b3f32b8c9e4d71

# 更新用户(需要认证)
curl -X PUT http://localhost:3000/api/users/60f7b3b5e6b3f32b8c9e4d71 \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"name":"陈博豪"}'

# 删除用户
curl -X DELETE http://localhost:3000/api/users/60f7b3b5e6b3f32b8c9e4d71 \
  -H "Authorization: Bearer <token>"

总结

本文从零搭建了一个完整的 Node.js RESTful API 服务,涵盖了路由设计、控制器、中间件、数据验证、JWT 认证、错误处理等核心环节。这些模式适用于绝大多数后端项目,可以直接作为新项目的起点。

下一步可以继续学习:Docker 容器化部署、自动化测试(Jest)、日志管理(Winston)、限流中间件、API 文档自动生成(Swagger)等进阶话题。