Skip to content

Node.js + Express 开发指南

🚀 项目初始化

1. 创建项目结构

bash
mkdir my-express-app
cd my-express-app
npm init -y

2. 安装依赖

bash
# 核心依赖
npm install express cors helmet morgan dotenv

# 开发依赖
npm install -D nodemon eslint prettier

# 数据库相关(可选)
npm install mongoose # MongoDB
# 或者
npm install mysql2 sequelize # MySQL

3. 推荐的项目结构

my-express-app/
├── src/
│   ├── controllers/     # 控制器
│   ├── models/         # 数据模型
│   ├── routes/         # 路由
│   ├── middleware/     # 中间件
│   ├── utils/          # 工具函数
│   ├── config/         # 配置文件
│   └── app.js          # 应用入口
├── tests/              # 测试文件
├── .env                # 环境变量
├── .gitignore
└── package.json

🏗️ 基础应用搭建

1. 创建基础应用 (src/app.js)

javascript
const express = require('express')
const cors = require('cors')
const helmet = require('helmet')
const morgan = require('morgan')
require('dotenv').config()

const app = express()
const PORT = process.env.PORT || 3000

// 中间件
app.use(helmet()) // 安全头
app.use(cors()) // 跨域
app.use(morgan('combined')) // 日志
app.use(express.json({ limit: '10mb' })) // JSON解析
app.use(express.urlencoded({ extended: true })) // URL编码解析

// 静态文件
app.use('/public', express.static('public'))

// 路由
app.get('/', (req, res) => {
  res.json({
    message: '欢迎使用我的API',
    version: '1.0.0',
    timestamp: new Date().toISOString()
  })
})

// 404处理
app.use('*', (req, res) => {
  res.status(404).json({
    error: '路由不存在',
    path: req.originalUrl
  })
})

// 全局错误处理
app.use((err, req, res, next) => {
  console.error(err.stack)
  res.status(500).json({
    error: '服务器内部错误',
    message: process.env.NODE_ENV === 'development' ? err.message : '请稍后重试'
  })
})

app.listen(PORT, () => {
  console.log(`🚀 服务器运行在 http://localhost:${PORT}`)
})

module.exports = app

2. 环境配置 (.env)

env
# 服务器配置
PORT=3000
NODE_ENV=development

# 数据库配置
DB_HOST=localhost
DB_PORT=27017
DB_NAME=myapp
DB_USER=
DB_PASSWORD=

# JWT配置
JWT_SECRET=your-super-secret-jwt-key
JWT_EXPIRES_IN=7d

# 其他配置
API_VERSION=v1
UPLOAD_PATH=./uploads

🛣️ 路由设计

1. 用户路由 (src/routes/users.js)

javascript
const express = require('express')
const router = express.Router()
const userController = require('../controllers/userController')
const authMiddleware = require('../middleware/auth')
const validationMiddleware = require('../middleware/validation')

// 用户注册
router.post('/register', 
  validationMiddleware.validateUserRegistration,
  userController.register
)

// 用户登录
router.post('/login',
  validationMiddleware.validateUserLogin,
  userController.login
)

// 获取用户信息(需要认证)
router.get('/profile',
  authMiddleware.requireAuth,
  userController.getProfile
)

// 更新用户信息
router.put('/profile',
  authMiddleware.requireAuth,
  validationMiddleware.validateUserUpdate,
  userController.updateProfile
)

// 获取所有用户(管理员权限)
router.get('/',
  authMiddleware.requireAuth,
  authMiddleware.requireAdmin,
  userController.getAllUsers
)

// 删除用户
router.delete('/:id',
  authMiddleware.requireAuth,
  authMiddleware.requireAdmin,
  userController.deleteUser
)

module.exports = router

2. 控制器 (src/controllers/userController.js)

javascript
const bcrypt = require('bcryptjs')
const jwt = require('jsonwebtoken')
const User = require('../models/User')
const { validationResult } = require('express-validator')

class UserController {
  // 用户注册
  async register(req, res) {
    try {
      // 验证输入
      const errors = validationResult(req)
      if (!errors.isEmpty()) {
        return res.status(400).json({
          error: '输入验证失败',
          details: errors.array()
        })
      }

      const { username, email, password } = req.body

      // 检查用户是否已存在
      const existingUser = await User.findOne({
        $or: [{ email }, { username }]
      })

      if (existingUser) {
        return res.status(409).json({
          error: '用户已存在',
          message: '邮箱或用户名已被使用'
        })
      }

      // 加密密码
      const saltRounds = 12
      const hashedPassword = await bcrypt.hash(password, saltRounds)

      // 创建用户
      const user = new User({
        username,
        email,
        password: hashedPassword
      })

      await user.save()

      // 生成JWT
      const token = jwt.sign(
        { userId: user._id, email: user.email },
        process.env.JWT_SECRET,
        { expiresIn: process.env.JWT_EXPIRES_IN }
      )

      res.status(201).json({
        message: '注册成功',
        user: {
          id: user._id,
          username: user.username,
          email: user.email,
          createdAt: user.createdAt
        },
        token
      })
    } catch (error) {
      console.error('注册错误:', error)
      res.status(500).json({
        error: '注册失败',
        message: '服务器内部错误'
      })
    }
  }

  // 用户登录
  async login(req, res) {
    try {
      const errors = validationResult(req)
      if (!errors.isEmpty()) {
        return res.status(400).json({
          error: '输入验证失败',
          details: errors.array()
        })
      }

      const { email, password } = req.body

      // 查找用户
      const user = await User.findOne({ email }).select('+password')
      if (!user) {
        return res.status(401).json({
          error: '登录失败',
          message: '邮箱或密码错误'
        })
      }

      // 验证密码
      const isPasswordValid = await bcrypt.compare(password, user.password)
      if (!isPasswordValid) {
        return res.status(401).json({
          error: '登录失败',
          message: '邮箱或密码错误'
        })
      }

      // 生成JWT
      const token = jwt.sign(
        { userId: user._id, email: user.email },
        process.env.JWT_SECRET,
        { expiresIn: process.env.JWT_EXPIRES_IN }
      )

      // 更新最后登录时间
      user.lastLoginAt = new Date()
      await user.save()

      res.json({
        message: '登录成功',
        user: {
          id: user._id,
          username: user.username,
          email: user.email,
          lastLoginAt: user.lastLoginAt
        },
        token
      })
    } catch (error) {
      console.error('登录错误:', error)
      res.status(500).json({
        error: '登录失败',
        message: '服务器内部错误'
      })
    }
  }

  // 获取用户信息
  async getProfile(req, res) {
    try {
      const user = await User.findById(req.user.userId)
      if (!user) {
        return res.status(404).json({
          error: '用户不存在'
        })
      }

      res.json({
        user: {
          id: user._id,
          username: user.username,
          email: user.email,
          createdAt: user.createdAt,
          lastLoginAt: user.lastLoginAt
        }
      })
    } catch (error) {
      console.error('获取用户信息错误:', error)
      res.status(500).json({
        error: '获取用户信息失败'
      })
    }
  }

  // 更新用户信息
  async updateProfile(req, res) {
    try {
      const errors = validationResult(req)
      if (!errors.isEmpty()) {
        return res.status(400).json({
          error: '输入验证失败',
          details: errors.array()
        })
      }

      const { username } = req.body
      const userId = req.user.userId

      // 检查用户名是否已被使用
      if (username) {
        const existingUser = await User.findOne({
          username,
          _id: { $ne: userId }
        })

        if (existingUser) {
          return res.status(409).json({
            error: '用户名已被使用'
          })
        }
      }

      // 更新用户信息
      const user = await User.findByIdAndUpdate(
        userId,
        { username, updatedAt: new Date() },
        { new: true }
      )

      res.json({
        message: '更新成功',
        user: {
          id: user._id,
          username: user.username,
          email: user.email,
          updatedAt: user.updatedAt
        }
      })
    } catch (error) {
      console.error('更新用户信息错误:', error)
      res.status(500).json({
        error: '更新失败'
      })
    }
  }
}

module.exports = new UserController()

🛡️ 中间件

1. 认证中间件 (src/middleware/auth.js)

javascript
const jwt = require('jsonwebtoken')
const User = require('../models/User')

class AuthMiddleware {
  // 验证JWT token
  async requireAuth(req, res, next) {
    try {
      const authHeader = req.headers.authorization
      if (!authHeader || !authHeader.startsWith('Bearer ')) {
        return res.status(401).json({
          error: '未授权',
          message: '请提供有效的访问令牌'
        })
      }

      const token = authHeader.substring(7) // 移除 'Bearer ' 前缀

      // 验证token
      const decoded = jwt.verify(token, process.env.JWT_SECRET)
      
      // 检查用户是否存在
      const user = await User.findById(decoded.userId)
      if (!user) {
        return res.status(401).json({
          error: '未授权',
          message: '用户不存在'
        })
      }

      // 将用户信息添加到请求对象
      req.user = {
        userId: user._id,
        email: user.email,
        role: user.role
      }

      next()
    } catch (error) {
      if (error.name === 'JsonWebTokenError') {
        return res.status(401).json({
          error: '未授权',
          message: '无效的访问令牌'
        })
      }
      if (error.name === 'TokenExpiredError') {
        return res.status(401).json({
          error: '未授权',
          message: '访问令牌已过期'
        })
      }
      
      console.error('认证错误:', error)
      res.status(500).json({
        error: '认证失败'
      })
    }
  }

  // 验证管理员权限
  requireAdmin(req, res, next) {
    if (req.user.role !== 'admin') {
      return res.status(403).json({
        error: '权限不足',
        message: '需要管理员权限'
      })
    }
    next()
  }

  // 可选认证(不强制要求登录)
  async optionalAuth(req, res, next) {
    try {
      const authHeader = req.headers.authorization
      if (authHeader && authHeader.startsWith('Bearer ')) {
        const token = authHeader.substring(7)
        const decoded = jwt.verify(token, process.env.JWT_SECRET)
        const user = await User.findById(decoded.userId)
        
        if (user) {
          req.user = {
            userId: user._id,
            email: user.email,
            role: user.role
          }
        }
      }
      next()
    } catch (error) {
      // 可选认证失败时不返回错误,继续执行
      next()
    }
  }
}

module.exports = new AuthMiddleware()

2. 验证中间件 (src/middleware/validation.js)

javascript
const { body } = require('express-validator')

class ValidationMiddleware {
  // 用户注册验证
  validateUserRegistration = [
    body('username')
      .isLength({ min: 3, max: 20 })
      .withMessage('用户名长度必须在3-20个字符之间')
      .matches(/^[a-zA-Z0-9_]+$/)
      .withMessage('用户名只能包含字母、数字和下划线'),
    
    body('email')
      .isEmail()
      .withMessage('请提供有效的邮箱地址')
      .normalizeEmail(),
    
    body('password')
      .isLength({ min: 8 })
      .withMessage('密码长度至少8个字符')
      .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
      .withMessage('密码必须包含至少一个小写字母、一个大写字母和一个数字')
  ]

  // 用户登录验证
  validateUserLogin = [
    body('email')
      .isEmail()
      .withMessage('请提供有效的邮箱地址')
      .normalizeEmail(),
    
    body('password')
      .notEmpty()
      .withMessage('密码不能为空')
  ]

  // 用户信息更新验证
  validateUserUpdate = [
    body('username')
      .optional()
      .isLength({ min: 3, max: 20 })
      .withMessage('用户名长度必须在3-20个字符之间')
      .matches(/^[a-zA-Z0-9_]+$/)
      .withMessage('用户名只能包含字母、数字和下划线')
  ]
}

module.exports = new ValidationMiddleware()

🗄️ 数据模型

MongoDB + Mongoose 示例

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

const userSchema = new mongoose.Schema({
  username: {
    type: String,
    required: [true, '用户名是必需的'],
    unique: true,
    trim: true,
    minlength: [3, '用户名至少3个字符'],
    maxlength: [20, '用户名最多20个字符']
  },
  email: {
    type: String,
    required: [true, '邮箱是必需的'],
    unique: true,
    lowercase: true,
    trim: true,
    match: [/^\S+@\S+\.\S+$/, '请提供有效的邮箱地址']
  },
  password: {
    type: String,
    required: [true, '密码是必需的'],
    minlength: [8, '密码至少8个字符'],
    select: false // 默认查询时不返回密码
  },
  role: {
    type: String,
    enum: ['user', 'admin'],
    default: 'user'
  },
  avatar: {
    type: String,
    default: null
  },
  isActive: {
    type: Boolean,
    default: true
  },
  lastLoginAt: {
    type: Date,
    default: null
  }
}, {
  timestamps: true, // 自动添加 createdAt 和 updatedAt
  toJSON: {
    transform: function(doc, ret) {
      delete ret.password
      delete ret.__v
      return ret
    }
  }
})

// 索引
userSchema.index({ email: 1 })
userSchema.index({ username: 1 })
userSchema.index({ createdAt: -1 })

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

🧪 测试

使用 Jest 和 Supertest

javascript
// tests/user.test.js
const request = require('supertest')
const app = require('../src/app')
const User = require('../src/models/User')

describe('用户API测试', () => {
  beforeEach(async () => {
    await User.deleteMany({})
  })

  describe('POST /api/users/register', () => {
    it('应该成功注册新用户', async () => {
      const userData = {
        username: 'testuser',
        email: 'test@example.com',
        password: 'Test123456'
      }

      const response = await request(app)
        .post('/api/users/register')
        .send(userData)
        .expect(201)

      expect(response.body.message).toBe('注册成功')
      expect(response.body.user.email).toBe(userData.email)
      expect(response.body.token).toBeDefined()
    })

    it('应该拒绝重复的邮箱', async () => {
      const userData = {
        username: 'testuser',
        email: 'test@example.com',
        password: 'Test123456'
      }

      // 第一次注册
      await request(app)
        .post('/api/users/register')
        .send(userData)
        .expect(201)

      // 第二次注册相同邮箱
      const response = await request(app)
        .post('/api/users/register')
        .send({ ...userData, username: 'testuser2' })
        .expect(409)

      expect(response.body.error).toBe('用户已存在')
    })
  })

  describe('POST /api/users/login', () => {
    beforeEach(async () => {
      // 创建测试用户
      await request(app)
        .post('/api/users/register')
        .send({
          username: 'testuser',
          email: 'test@example.com',
          password: 'Test123456'
        })
    })

    it('应该成功登录', async () => {
      const response = await request(app)
        .post('/api/users/login')
        .send({
          email: 'test@example.com',
          password: 'Test123456'
        })
        .expect(200)

      expect(response.body.message).toBe('登录成功')
      expect(response.body.token).toBeDefined()
    })

    it('应该拒绝错误的密码', async () => {
      const response = await request(app)
        .post('/api/users/login')
        .send({
          email: 'test@example.com',
          password: 'wrongpassword'
        })
        .expect(401)

      expect(response.body.error).toBe('登录失败')
    })
  })
})

📝 总结

这个Node.js + Express指南涵盖了:

  • 🏗️ 项目结构: 清晰的文件组织
  • 🛣️ 路由设计: RESTful API设计
  • 🛡️ 安全性: 认证、授权、输入验证
  • 🗄️ 数据模型: MongoDB/Mongoose集成
  • 🧪 测试: 单元测试和集成测试
  • 📊 错误处理: 统一的错误处理机制

通过这些最佳实践,你可以构建出安全、可维护、可扩展的Node.js应用!


希望这个指南对你的Node.js开发有所帮助! 🚀

Last updated: