Практическое руководство по 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 express = require('express');
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Создайте простую модель данных в памяти:

Пример

// 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:

Пример

// 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: Фильтр по 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Файл:

Пример

// 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:

Пример

// 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

  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() });
    }
    Далее();
  }
];

// Используйте валидацию в маршрутах
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);

// ... 其他代码 ...
Другие расширения