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`);
});
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;
}
};
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;
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;
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;
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 测试接口
-
获取所有图书
- 点击 GET /api/books 接口
- 点击 "Try it out" 按钮
- 可以设置分页参数 page 和 limit,以及过滤条件
- 点击 "Execute" 按钮发送请求
- 观察结果
-
创建新图书
- 点击 POST /api/books 接口
- 点击 "Try it out" 按钮
在请求体中填入图书信息,例如:
{ "title": "RESTful API设计", "author": "Martin Fowler", "publishDate": "2020-01-01", "isbn": "978-1234567890", "category": "编程" }
- 点击 "Execute" 按钮发送请求
- 观察结果
-
获取、更新和删除
- 使用类似方法测试其他接口
常见问题排查
跨域问题
如果前端应用和 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));
origin: ['http://localhost:8080', 'https://your-frontend-domain.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true
};
app.use(cors(corsOptions));
认证配置错误
认证问题通常出现在以下几个方面:
-
JWT 配置错误
确保
JWT_SECRET
环境变量已正确设置:// 在应用启动时检查 if (!process.env.JWT_SECRET) { console.warn('警告: JWT_SECRET环境变量未设置,认证可能不安全'); }
-
认证中间件使用错误
确保在需要认证的路由上正确应用中间件:
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) => { /* ... */ });
-
Swagger文档中的认证配置
更新Swagger配置以支持Bearer Token认证:
// swagger/swagger.js const options = { definition: { // ... 其他配置 ... components: { securitySchemes: { bearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' } } }, security: [ { bearerAuth: [] } ] }, // ... 其他配置 ... };
-
请求头配置
确保客户端请求时正确设置了认证头:
// 前端示例代码 - 使用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 文档没有更新,尝试以下解决方案:
-
确保路径配置正确
// swagger/swagger.js const options = { // ... 其他配置 ... apis: ['./routes/*.js', './models/*.js'] // 确保包含所有需要生成文档的文件 };
-
重启服务器
有时候需要重启服务器才能看到新的Swagger文档。
-
清除浏览器缓存
使用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) => {
// ... 创建图书的代码 ...
});
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); // ... 其他代码 ...
点我分享笔记