16.4 使用 Express 搭建 REST API

Express 基础与项目初始化

1. 项目设置与基本结构

# 创建项目目录并初始化
mkdir express-api
cd express-api
npm init -y

# 安装依赖
npm install express body-parser cors morgan
npm install --save-dev nodemon

2. 基础 Express 应用

// src/app.js
const express = require('express');
const morgan = require('morgan');
const cors = require('cors');

const app = express();

// 中间件
app.use(cors());
app.use(morgan('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// 健康检查路由
app.get('/health', (req, res) => {
  res.json({ status: 'UP' });
});

// 错误处理中间件
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ error: 'Internal Server Error' });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

module.exports = app;

RESTful 路由设计

1. 资源路由结构

// src/routes/products.js
const express = require('express');
const router = express.Router();
const Product = require('../models/product');

// GET /products - 获取所有产品
router.get('/', async (req, res, next) => {
  try {
    const products = await Product.find();
    res.json(products);
  } catch (err) {
    next(err);
  }
});

// GET /products/:id - 获取单个产品
router.get('/:id', async (req, res, next) => {
  try {
    const product = await Product.findById(req.params.id);
    if (!product) {
      return res.status(404).json({ error: 'Product not found' });
    }
    res.json(product);
  } catch (err) {
    next(err);
  }
});

// POST /products - 创建新产品
router.post('/', async (req, res, next) => {
  try {
    const product = new Product(req.body);
    const savedProduct = await product.save();
    res.status(201).json(savedProduct);
  } catch (err) {
    next(err);
  }
});

// PUT /products/:id - 更新产品
router.put('/:id', async (req, res, next) => {
  try {
    const updatedProduct = await Product.findByIdAndUpdate(
      req.params.id,
      req.body,
      { new: true, runValidators: true }
    );
    if (!updatedProduct) {
      return res.status(404).json({ error: 'Product not found' });
    }
    res.json(updatedProduct);
  } catch (err) {
    next(err);
  }
});

// DELETE /products/:id - 删除产品
router.delete('/:id', async (req, res, next) => {
  try {
    const deletedProduct = await Product.findByIdAndDelete(req.params.id);
    if (!deletedProduct) {
      return res.status(404).json({ error: 'Product not found' });
    }
    res.status(204).end();
  } catch (err) {
    next(err);
  }
});

module.exports = router;

2. 路由注册

// src/app.js
const productsRouter = require('./routes/products');
const usersRouter = require('./routes/users');

// 添加路由前缀
app.use('/api/products', productsRouter);
app.use('/api/users', usersRouter);

数据验证与中间件

1. 使用 Joi 进行请求验证

// src/middlewares/validation.js
const Joi = require('joi');

const productSchema = Joi.object({
  name: Joi.string().min(3).max(100).required(),
  price: Joi.number().min(0).precision(2).required(),
  description: Joi.string().max(500),
  inStock: Joi.boolean().default(true)
});

function validateProduct(req, res, next) {
  const { error } = productSchema.validate(req.body);
  if (error) {
    return res.status(400).json({ error: error.details[0].message });
  }
  next();
}

module.exports = { validateProduct };

2. 认证中间件

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

function authenticate(req, res, next) {
  const token = req.header('Authorization')?.replace('Bearer ', '');
  
  if (!token) {
    return res.status(401).json({ error: 'Authentication required' });
  }

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded;
    next();
  } catch (err) {
    res.status(401).json({ error: 'Invalid token' });
  }
}

function authorize(role) {
  return (req, res, next) => {
    if (req.user.role !== role) {
      return res.status(403).json({ error: 'Forbidden' });
    }
    next();
  };
}

module.exports = { authenticate, authorize };

数据库集成

1. Mongoose 模型定义

// src/models/product.js
const mongoose = require('mongoose');

const productSchema = new mongoose.Schema({
  name: {
    type: String,
    required: true,
    minlength: 3,
    maxlength: 100
  },
  price: {
    type: Number,
    required: true,
    min: 0
  },
  description: {
    type: String,
    maxlength: 500
  },
  inStock: {
    type: Boolean,
    default: true
  },
  createdAt: {
    type: Date,
    default: Date.now
  }
});

// 添加索引
productSchema.index({ name: 'text', description: 'text' });

// 虚拟属性和方法
productSchema.virtual('formattedPrice').get(function() {
  return `$${this.price.toFixed(2)}`;
});

// 中间件钩子
productSchema.pre('save', function(next) {
  console.log(`Saving product: ${this.name}`);
  next();
});

module.exports = mongoose.model('Product', productSchema);

2. 数据库连接

// src/db.js
const mongoose = require('mongoose');

const connectDB = async () => {
  try {
    await mongoose.connect(process.env.MONGODB_URI, {
      useNewUrlParser: true,
      useUnifiedTopology: true,
      useCreateIndex: true,
      useFindAndModify: false
    });
    console.log('MongoDB connected');
  } catch (err) {
    console.error('Database connection error:', err);
    process.exit(1);
  }
};

module.exports = connectDB;

高级功能实现

1. 文件上传

// src/routes/uploads.js
const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const router = express.Router();

const uploadDir = path.join(__dirname, '../uploads');
if (!fs.existsSync(uploadDir)) {
  fs.mkdirSync(uploadDir);
}

const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, uploadDir);
  },
  filename: (req, file, cb) => {
    cb(null, `${Date.now()}-${file.originalname}`);
  }
});

const upload = multer({ 
  storage,
  limits: { fileSize: 5 * 1024 * 1024 }, // 5MB
  fileFilter: (req, file, cb) => {
    const filetypes = /jpeg|jpg|png|gif/;
    const extname = filetypes.test(path.extname(file.originalname).toLowerCase());
    const mimetype = filetypes.test(file.mimetype);
    
    if (extname && mimetype) {
      return cb(null, true);
    }
    cb(new Error('Only images are allowed'));
  }
});

router.post('/', upload.single('image'), (req, res) => {
  res.json({
    filename: req.file.filename,
    path: `/uploads/${req.file.filename}`
  });
});

module.exports = router;

2. 分页与过滤

// src/routes/products.js
router.get('/', async (req, res, next) => {
  try {
    // 分页参数
    const page = parseInt(req.query.page) || 1;
    const limit = parseInt(req.query.limit) || 10;
    const skip = (page - 1) * limit;
    
    // 过滤条件
    const filter = {};
    if (req.query.name) {
      filter.name = new RegExp(req.query.name, 'i');
    }
    if (req.query.minPrice) {
      filter.price = { $gte: parseFloat(req.query.minPrice) };
    }
    
    // 排序
    const sort = {};
    if (req.query.sortBy) {
      const parts = req.query.sortBy.split(':');
      sort[parts[0]] = parts[1] === 'desc' ? -1 : 1;
    }
    
    const [products, total] = await Promise.all([
      Product.find(filter)
        .skip(skip)
        .limit(limit)
        .sort(sort),
      Product.countDocuments(filter)
    ]);
    
    res.json({
      data: products,
      meta: {
        page,
        limit,
        total,
        pages: Math.ceil(total / limit)
      }
    });
  } catch (err) {
    next(err);
  }
});

测试与文档

1. API 测试 (使用 Jest 和 Supertest)

// tests/products.test.js
const request = require('supertest');
const app = require('../src/app');
const Product = require('../src/models/product');

describe('Products API', () => {
  beforeAll(async () => {
    await Product.deleteMany();
  });

  describe('POST /api/products', () => {
    it('should create a new product', async () => {
      const response = await request(app)
        .post('/api/products')
        .send({
          name: 'Test Product',
          price: 19.99,
          description: 'Test description'
        })
        .expect(201);
      
      expect(response.body).toHaveProperty('_id');
      expect(response.body.name).toBe('Test Product');
    });
  });

  describe('GET /api/products', () => {
    it('should retrieve all products', async () => {
      await Product.create([
        { name: 'Product 1', price: 10 },
        { name: 'Product 2', price: 20 }
      ]);
      
      const response = await request(app)
        .get('/api/products')
        .expect(200);
      
      expect(response.body.length).toBe(2);
    });
  });
});

2. API 文档 (使用 Swagger)

// src/swagger.js
const swaggerJsdoc = require('swagger-jsdoc');
const swaggerUi = require('swagger-ui-express');

const options = {
  definition: {
    openapi: '3.0.0',
    info: {
      title: 'Express API',
      version: '1.0.0',
      description: 'API documentation for Express application'
    },
    servers: [
      {
        url: 'http://localhost:3000/api'
      }
    ],
    components: {
      securitySchemes: {
        bearerAuth: {
          type: 'http',
          scheme: 'bearer',
          bearerFormat: 'JWT'
        }
      }
    }
  },
  apis: ['./src/routes/*.js']
};

const specs = swaggerJsdoc(options);

module.exports = (app) => {
  app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs));
};

生产环境优化

1. 安全加固

// src/security.js
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');

const securityMiddleware = (app) => {
  // 设置安全相关的HTTP头
  app.use(helmet());
  
  // 限制请求频率
  const limiter = rateLimit({
    windowMs: 15 * 60 * 1000, // 15分钟
    max: 100 // 每个IP限制100个请求
  });
  app.use(limiter);
  
  // 防止XSS攻击
  app.use((req, res, next) => {
    res.setHeader('X-XSS-Protection', '1; mode=block');
    next();
  });
  
  // 防止MIME嗅探
  app.use((req, res, next) => {
    res.setHeader('X-Content-Type-Options', 'nosniff');
    next();
  });
};

module.exports = securityMiddleware;

2. 性能优化

// src/performance.js
const compression = require('compression');
const responseTime = require('response-time');

const performanceMiddleware = (app) => {
  // 启用Gzip压缩
  app.use(compression());
  
  // 记录响应时间
  app.use(responseTime());
  
  // 静态文件缓存
  app.use('/static', express.static('public', {
    maxAge: '1y',
    immutable: true
  }));
};

module.exports = performanceMiddleware;

完整项目结构

express-api/
├── src/
│   ├── app.js                # 应用入口
│   ├── db.js                 # 数据库连接
│   ├── config/               # 配置文件
│   │   └── index.js
│   ├── controllers/          # 业务逻辑
│   │   └── products.js
│   ├── models/               # 数据模型
│   │   └── product.js
│   ├── routes/               # 路由定义
│   │   ├── products.js
│   │   └── users.js
│   ├── middlewares/          # 自定义中间件
│   │   ├── auth.js
│   │   └── validation.js
│   ├── services/             # 服务层
│   │   └── productService.js
│   ├── utils/                # 工具函数
│   │   └── logger.js
│   └── swagger.js            # API文档
├── tests/                    # 测试文件
│   └── products.test.js
├── uploads/                  # 上传文件目录
├── .env                      # 环境变量
├── package.json
└── README.md

部署与扩展

1. Docker 部署

# Dockerfile
FROM node:16-alpine

WORKDIR /app

COPY package*.json ./
RUN npm install --production

COPY . .

ENV PORT=3000
ENV NODE_ENV=production

EXPOSE 3000
CMD ["node", "src/app.js"]

2. 集群模式

// src/cluster.js
const cluster = require('cluster');
const os = require('os');
const app = require('./app');

if (cluster.isMaster) {
  const numCPUs = os.cpus().length;
  
  console.log(`Master ${process.pid} is running`);
  
  // Fork workers
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }
  
  cluster.on('exit', (worker, code, signal) => {
    console.log(`Worker ${worker.process.pid} died`);
    cluster.fork(); // 重启worker
  });
} else {
  const PORT = process.env.PORT || 3000;
  app.listen(PORT, () => {
    console.log(`Worker ${process.pid} started on port ${PORT}`);
  });
}

总结

通过 Express 构建 RESTful API 时,关键点包括:

  1. 路由组织:按照资源组织路由,遵循 REST 规范
  2. 中间件使用:合理使用中间件处理请求生命周期
  3. 错误处理:统一错误处理机制
  4. 数据验证:请求数据验证确保安全性
  5. 数据库集成:使用 ORM/ODM 简化数据操作
  6. API文档:维护良好的 API 文档
  7. 测试覆盖:编写全面的测试用例
  8. 安全加固:实施必要的安全措施
  9. 性能优化:考虑缓存、压缩等技术
  10. 可扩展性:设计可扩展的架构

Express 的灵活性和丰富的中间件生态使其成为构建 Node.js REST API 的理想选择,结合现代 JavaScript 特性和最佳实践,可以开发出高效、可维护的后端服务。

#前端开发 分享于 2025-03-25

【 内容由 AI 共享,不代表本站观点,请谨慎参考 】