Swagger 实战教程:图书管理 API

本教程将带您实现一个基于Swagger的图书管理系统API,主要功能包括:

  • 图书的增删改查操作
  • 分页查询
  • 条件过滤
  • API文档自动生成
  • 在线接口测试

技术栈:

  • Node.js + Express
  • Swagger UI Express
  • Swagger JSDoc

环境准备

安装Node.js

确保您的系统已安装 Node.js(建议使用v14.0.0或更高版本)。

检查环境

node -v
npm -v

项目初始化

创建项目目录

mkdir book-management-api
cd book-management-api
npm init -y

安装依赖包

npm install express cors swagger-ui-express swagger-jsdoc nodemon --save

创建基础项目结构

book-management-api/
├── node_modules/
├── models/
│   └── book.js
├── routes/
│   └── books.js
├── middleware/
│   └── auth.js
├── swagger/
│   └── swagger.js
├── app.js
└── package.json

创建入口文件 app.js

实例

const express = require('express');
const cors = require('cors');
const swaggerUi = require('swagger-ui-express');
const swaggerSpec = require('./swagger/swagger');
const booksRouter = require('./routes/books');

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

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

// API路由
app.use('/api/books', booksRouter);

// Swagger文档
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));

// 启动服务器
app.listen(PORT, () => {
  console.log(`服务已启动,访问 http://localhost:${PORT}`);
  console.log(`API文档地址: http://localhost:${PORT}/api-docs`);
});

API 设计与实现

创建图书模型

models/book.js 中创建一个简单的内存数据模型:

实例

// models/book.js
let books = [
  { id: 1, title: '深入理解JavaScript', author: 'Douglas Crockford', publishDate: '2008-01-01', isbn: '978-0596517748', category: '编程' },
  { id: 2, title: 'Node.js实战', author: 'Alex Young', publishDate: '2014-08-01', isbn: '978-1617290572', category: '编程' },
  { id: 3, title: '三体', author: '刘慈欣', publishDate: '2008-01-01', isbn: '978-7536692387', category: '科幻' }
];

let nextId = 4;

module.exports = {
  // 获取所有图书
  getAll: (page = 1, limit = 10, filter = {}) => {
    let result = [...books];
   
    // 应用过滤条件
    if (filter.category) {
      result = result.filter(book => book.category === filter.category);
    }
   
    if (filter.author) {
      result = result.filter(book => book.author.includes(filter.author));
    }
   
    if (filter.title) {
      result = result.filter(book => book.title.includes(filter.title));
    }
   
    // 计算分页
    const startIndex = (page - 1) * limit;
    const endIndex = page * limit;
   
    return {
      total: result.length,
      page: page,
      limit: limit,
      data: result.slice(startIndex, endIndex)
    };
  },
 
  // 获取单本图书
  getById: (id) => {
    return books.find(book => book.id === id);
  },
 
  // 创建图书
  create: (book) => {
    const newBook = { ...book, id: nextId++ };
    books.push(newBook);
    return newBook;
  },
 
  // 更新图书
  update: (id, bookData) => {
    const index = books.findIndex(book => book.id === id);
    if (index !== -1) {
      books[index] = { ...books[index], ...bookData };
      return books[index];
    }
    return null;
  },
 
  // 删除图书
  delete: (id) => {
    const index = books.findIndex(book => book.id === id);
    if (index !== -1) {
      const deletedBook = books[index];
      books.splice(index, 1);
      return deletedBook;
    }
    return null;
  }
};

实现路由控制器

routes/books.js 中实现 API 路由:

实例

// routes/books.js
const express = require('express');
const router = express.Router();
const Book = require('../models/book');

/**
 * @swagger
 * components:
 *   schemas:
 *     Book:
 *       type: object
 *       required:
 *         - title
 *         - author
 *         - isbn
 *       properties:
 *         id:
 *           type: integer
 *           description: 图书ID
 *         title:
 *           type: string
 *           description: 图书标题
 *         author:
 *           type: string
 *           description: 作者
 *         publishDate:
 *           type: string
 *           format: date
 *           description: 出版日期
 *         isbn:
 *           type: string
 *           description: ISBN编号
 *         category:
 *           type: string
 *           description: 图书分类
 */


/**
 * @swagger
 * /api/books:
 *   get:
 *     summary: 获取图书列表
 *     description: 返回所有图书,支持分页和过滤
 *     parameters:
 *       - in: query
 *         name: page
 *         schema:
 *           type: integer
 *           default: 1
 *         description: 页码
 *       - in: query
 *         name: limit
 *         schema:
 *           type: integer
 *           default: 10
 *         description: 每页数量
 *       - in: query
 *         name: category
 *         schema:
 *           type: string
 *         description: 按分类过滤
 *       - in: query
 *         name: author
 *         schema:
 *           type: string
 *         description: 按作者过滤
 *       - in: query
 *         name: title
 *         schema:
 *           type: string
 *         description: 按标题过滤
 *     responses:
 *       200:
 *         description: 成功获取图书列表
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 total:
 *                   type: integer
 *                 page:
 *                   type: integer
 *                 limit:
 *                   type: integer
 *                 data:
 *                   type: array
 *                   items:
 *                     $ref: '#/components/schemas/Book'
 */

router.get('/', (req, res) => {
  const page = parseInt(req.query.page) || 1;
  const limit = parseInt(req.query.limit) || 10;
  const filter = {};
 
  if (req.query.category) filter.category = req.query.category;
  if (req.query.author) filter.author = req.query.author;
  if (req.query.title) filter.title = req.query.title;
 
  const result = Book.getAll(page, limit, filter);
  res.json(result);
});

/**
 * @swagger
 * /api/books/{id}:
 *   get:
 *     summary: 获取单本图书
 *     description: 通过ID获取特定图书的详细信息
 *     parameters:
 *       - in: path
 *         name: id
 *         required: true
 *         schema:
 *           type: integer
 *         description: 图书ID
 *     responses:
 *       200:
 *         description: 成功获取图书
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/Book'
 *       404:
 *         description: 图书不存在
 */

router.get('/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const book = Book.getById(id);
 
  if (book) {
    res.json(book);
  } else {
    res.status(404).json({ message: '图书不存在' });
  }
});

/**
 * @swagger
 * /api/books:
 *   post:
 *     summary: 创建新图书
 *     description: 添加一本新图书到系统
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             type: object
 *             required:
 *               - title
 *               - author
 *               - isbn
 *             properties:
 *               title:
 *                 type: string
 *               author:
 *                 type: string
 *               publishDate:
 *                 type: string
 *                 format: date
 *               isbn:
 *                 type: string
 *               category:
 *                 type: string
 *     responses:
 *       201:
 *         description: 图书创建成功
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/Book'
 *       400:
 *         description: 输入数据无效
 */

router.post('/', (req, res) => {
  // 简单验证
  if (!req.body.title || !req.body.author || !req.body.isbn) {
    return res.status(400).json({ message: '标题、作者和ISBN是必填字段' });
  }
 
  const newBook = Book.create(req.body);
  res.status(201).json(newBook);
});

/**
 * @swagger
 * /api/books/{id}:
 *   put:
 *     summary: 更新图书
 *     description: 通过ID更新特定图书的信息
 *     parameters:
 *       - in: path
 *         name: id
 *         required: true
 *         schema:
 *           type: integer
 *         description: 图书ID
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             type: object
 *             properties:
 *               title:
 *                 type: string
 *               author:
 *                 type: string
 *               publishDate:
 *                 type: string
 *                 format: date
 *               isbn:
 *                 type: string
 *               category:
 *                 type: string
 *     responses:
 *       200:
 *         description: 图书更新成功
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/Book'
 *       404:
 *         description: 图书不存在
 */

router.put('/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const updatedBook = Book.update(id, req.body);
 
  if (updatedBook) {
    res.json(updatedBook);
  } else {
    res.status(404).json({ message: '图书不存在' });
  }
});

/**
 * @swagger
 * /api/books/{id}:
 *   delete:
 *     summary: 删除图书
 *     description: 通过ID删除特定图书
 *     parameters:
 *       - in: path
 *         name: id
 *         required: true
 *         schema:
 *           type: integer
 *         description: 图书ID
 *     responses:
 *       200:
 *         description: 图书删除成功
 *       404:
 *         description: 图书不存在
 */

router.delete('/:id', (req, res) => {
  const id = parseInt(req.params.id);
  const deletedBook = Book.delete(id);
 
  if (deletedBook) {
    res.json({ message: '图书删除成功', book: deletedBook });
  } else {
    res.status(404).json({ message: '图书不存在' });
  }
});

module.exports = router;

Swagger 文档配置

配置 Swagger

创建 swagger/swagger.js 文件:

实例

// swagger/swagger.js
const swaggerJsdoc = require('swagger-jsdoc');

const options = {
  definition: {
    openapi: '3.0.0',
    info: {
      title: '图书管理API',
      version: '1.0.0',
      description: '使用Express和Swagger构建的图书管理系统API',
      contact: {
        name: '开发者',
        email: 'dev@example.com'
      }
    },
    servers: [
      {
        url: 'http://localhost:3000',
        description: '开发服务器'
      }
    ]
  },
  // 扫描所有包含注解的JS文件
  apis: ['./routes/*.js']
};

const swaggerSpec = swaggerJsdoc(options);

module.exports = swaggerSpec;

添加认证中间件

创建 middleware/auth.js

实例

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

// 简单JWT验证中间件
const authMiddleware = (req, res, next) => {
  const token = req.header('Authorization')?.replace('Bearer ', '');
 
  if (!token) {
    return res.status(401).json({ message: '未提供认证令牌' });
  }
 
  try {
    // 在实际应用中,这里需要验证JWT令牌
    // const decoded = jwt.verify(token, process.env.JWT_SECRET);
    // req.user = decoded.user;
   
    // 为简化教程,我们跳过验证
    req.user = { id: 1, role: 'admin' };
    next();
  } catch (error) {
    return res.status(401).json({ message: '无效的认证令牌' });
  }
};

module.exports = authMiddleware;

接口功能测试

启动服务

使用以下命令启动服务:

# 如果已配置了package.json中的scripts,执行:
npm start

# 或者直接使用nodemon
npx nodemon app.js

6.2 访问Swagger文档

打开浏览器访问: http://localhost:3000/api-docs

使用 Swagger 测试接口

  1. 获取所有图书

    • 点击 GET /api/books 接口
    • 点击 "Try it out" 按钮
    • 可以设置分页参数 page 和 limit,以及过滤条件
    • 点击 "Execute" 按钮发送请求
    • 观察结果
  2. 创建新图书

    • 点击 POST /api/books 接口
    • 点击 "Try it out" 按钮
    • 在请求体中填入图书信息,例如:

      {
          "title": "RESTful API设计",
          "author": "Martin Fowler",
          "publishDate": "2020-01-01",
          "isbn": "978-1234567890",
          "category": "编程"
      }
      
    • 点击 "Execute" 按钮发送请求
    • 观察结果
  3. 获取、更新和删除

    • 使用类似方法测试其他接口

常见问题排查

跨域问题

如果前端应用和 API 不在同一域下运行,可能会遇到跨域问题。我们已经在应用入口配置了 CORS 中间件:

app.use(cors());

如果需要更精细的控制,可以这样配置:

实例

const corsOptions = {
  origin: ['http://localhost:8080', 'https://your-frontend-domain.com'],
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true
};

app.use(cors(corsOptions));

认证配置错误

认证问题通常出现在以下几个方面:

  1. JWT 配置错误

    确保 JWT_SECRET 环境变量已正确设置:

    // 在应用启动时检查
    if (!process.env.JWT_SECRET) {
      console.warn('警告: JWT_SECRET环境变量未设置,认证可能不安全');
    }        
    
  2. 认证中间件使用错误

    确保在需要认证的路由上正确应用中间件:

    const auth = require('../middleware/auth');
    
    // 公开路由 - 无需认证
    router.get('/', (req, res) => { /* ... */ });
    
    // 受保护路由 - 需要认证
    router.post('/', auth, (req, res) => { /* ... */ });
    router.put('/:id', auth, (req, res) => { /* ... */ });
    router.delete('/:id', auth, (req, res) => { /* ... */ });
    
  3. Swagger文档中的认证配置

    更新Swagger配置以支持Bearer Token认证:

    // swagger/swagger.js
    const options = {
      definition: {
        // ... 其他配置 ...
        components: {
          securitySchemes: {
            bearerAuth: {
              type: 'http',
              scheme: 'bearer',
              bearerFormat: 'JWT'
            }
          }
        },
        security: [
          {
            bearerAuth: []
          }
        ]
      },
      // ... 其他配置 ...
    };
    
  4. 请求头配置

    确保客户端请求时正确设置了认证头:

    // 前端示例代码 - 使用fetch API
    const token = localStorage.getItem('token');
    
    fetch('http://localhost:3000/api/books', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${token}`
      },
      body: JSON.stringify(bookData)
    })
    .then(response => response.json())
    .then(data => console.log(data))
    .catch(error => console.error('错误:', error));
    

Swagger 文档不更新

如果更新了 JSDoc 注释但 Swagger 文档没有更新,尝试以下解决方案:

  1. 确保路径配置正确

    // swagger/swagger.js
    const options = {
      // ... 其他配置 ...
      apis: ['./routes/*.js', './models/*.js'] // 确保包含所有需要生成文档的文件
    };
    
  2. 重启服务器

    有时候需要重启服务器才能看到新的Swagger文档。

  3. 清除浏览器缓存

    使用Ctrl+F5强制刷新页面。


进阶功能

添加 API 版本控制

修改应用入口文件:

// app.js
// ... 其他导入 ...

// API v1
app.use('/api/v1/books', booksRouter);

// 为未来的API v2预留
// app.use('/api/v2/books', booksRouterV2);

// ... 其他代码 ...

添加请求验证

使用 Express 验证中间件(如express-validator):

npm install express-validator --save

然后在路由中使用:

实例

// routes/books.js
const { body, validationResult } = require('express-validator');

// 创建图书的验证规则
const validateBook = [
  body('title').notEmpty().withMessage('标题不能为空'),
  body('author').notEmpty().withMessage('作者不能为空'),
  body('isbn').notEmpty().withMessage('ISBN不能为空')
    .matches(/^(?=(?:\D*\d){10}(?:(?:\D*\d){3})?$)[\d-]+$/).withMessage('无效的ISBN格式'),
  body('publishDate').optional().isDate().withMessage('出版日期格式无效'),
 
  // 验证结果处理中间件
  (req, res, next) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }
    next();
  }
];

// 在路由中使用验证
router.post('/', validateBook, (req, res) => {
  // ... 创建图书的代码 ...
});

添加响应压缩

使用压缩中间件减小响应体积:

npm install compression --save

然后在应用入口文件中添加:

// app.js
const compression = require('compression');

// ... 其他代码 ...

// 启用压缩
app.use(compression());

// ... 其他代码 ...

添加响应缓存

为了提高性能,可以添加响应缓存:

npm install apicache --save

然后在应用入口中使用:

// app.js
const apicache = require('apicache');
const cache = apicache.middleware;

// ... 其他代码 ...

// 缓存GET请求5分钟
app.use('/api', cache('5 minutes'));

// ... 其他代码 ...

添加请求限流

防止 API 滥用:

npm install express-rate-limit --save

然后在应用入口中使用:

// app.js
const rateLimit = require('express-rate-limit');

// ... 其他代码 ...

// 创建限流中间件
const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15分钟
  max: 100, // 每个IP在windowMs时间内最多100个请求
  message: '来自此IP的请求过多,请稍后再试'
});

// 应用限流中间件
app.use('/api', apiLimiter);

// ... 其他代码 ...