Практическое руководство по Swagger: API управления книгами
Это руководство проведет вас через реализацию API системы управления книгами на основе Swagger. Основные возможности включают:
- CRUD-операции с книгами
- Постраничные запросы
- Фильтрация по условиям
- Автоматическая генерация документации API
- Онлайн-тестирование API
Технологический стек:
- Node.js + Express
- Swagger UI Express
- Swagger JSDoc
Подготовка окружения
Установите Node.js
Убедитесь, что Node.js установлен в вашей системе (рекомендуется версия 14.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 cors = require('cors');
const swaggerUi = require('swagger-ui-express');
const swaggerSpec = require('swagger/swagger');
const booksRouter⟧require = 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Создайте простую модель данных в памяти:
Пример
let books = [
{ id: 1, title: 'Глубокое понимание JavaScript', author: 'Douglas Crockford', publishDate: '2008-01-01', isbn: '978-0596517748', category: 'Программирование' },
{ id: 2, title: 'Node.js в действии', author: 'Алекс Янг', 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 deleted = books[index];
books.splice(index, 1);
return deletedBook;
}
return null;
}
};
Реализуйте контроллер маршрута
Вroutes/books.jsРеализуйте маршруты API:
Пример
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: Фильтр по category
* - in: query
* name: author
* schema:
* type: string
* description: Фильтр по author
* - in: query
* name: title
* schema:
* type: string
* description: Фильтр по title
* 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: 'Book not found' });
}
});
/**
* @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: 'Title, author и 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: 'Book not found' });
}
});
/**
* @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: 'Book not found' });
}
});
module.exports = router;
Конфигурация документации Swagger
Настройка Swagger
Создайтеswagger/swagger.jsФайл:
Пример
const swaggerJsdoc = require('swagger-jsdoc');
const options = {
определение: {
openapi: '3.0.0',
информация: {
title: 'API управления книгами',
version: '1.0.0',
description: 'API системы управления книгами, созданный с помощью Express и Swagger',
contact: {
name: 'Разработчик',
email: '[email protected]'
}
},
servers: [
{
url: 'http://localhost:3000',
description: 'Сервер разработки'
}
]
},
// Сканировать все JS-файлы, содержащие аннотации
apis: ['./routes/*.js']
};
const swaggerSpec = swaggerJsdoc(options);
module.exports = swaggerSpec;
Добавить промежуточное ПО аутентификации
Создайте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' };
Далее();
} catch (error) {
return res.status(401).json({ message: 'Недействительный токен аутентификации' });
}
};
module.exports = authMiddleware;
Функциональное тестирование API
Запуск сервера
Используйте следующую команду для запуска сервера:
# 如果已配置了package.json中的scripts,执行: npm start # 或者直接使用nodemon npx nodemon app.js
6.2 Доступ к документации Swagger
Откройте браузер и перейдите по адресу: http://localhost:3000/api-docs
Использование Swagger для тестирования API
-
Получить все книги
- Нажмите на эндпоинт 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());
Если требуется более тонкая настройка, вы можете настроить это так:
Пример
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
Затем используйте его в маршруте:
Пример
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() });
}
Далее();
}
];
// Используйте валидацию в маршрутах
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);
// ... 其他代码 ... Другие расширения