Как стать автором
Обновить
1853.77

Node.js: документирование и визуализация API с помощью Swagger

Время на прочтение 8 мин
Количество просмотров 22K



Привет, друзья!


В этой небольшой заметке я расскажу вам о том, как генерировать и визуализировать документацию к API с помощью Swagger.


Мы разработаем простой Express-сервер, способный обрабатывать стандартные CRUD-запросы, с фиктивной базой данных, реализованной с помощью lowdb.


Затем мы подробно опишем наше API, сгенерируем JSON-файл с описанием и визуализируем его.


Так, например, будет выглядеть описание POST-запроса к нашему API:





Исходный код проекта.


Если вам это интересно, прошу под кат.


Подготовка и настройка проекта


Создаем директорию, переходим в нее и инициализируем Node.js-проект:


mkdir express-swagger
cd express-swagger

yarn init -yp
# or
npm init -y

Устанавливаем зависимости:


yarn add express lowdb cross-env nodemon
# or
npm i ...

  • cross-env — утилита для платформонезависимой установки значений переменных среды окружения;
  • nodemon — утилита для запуска сервера для разработки, который автоматически перезапускается при изменении файлов, за которыми ведется наблюдение.

Структура проекта:


- db
 - data.json - фиктивные данные
 - index.js - инициализация БД
- routes
 - todo.routes.js - роуты
- swagger - этой директорией мы займемся позже
- server.js - код сервера

Определяем тип сервера (модуль) и команды для его запуска в package.json:


"type": "module",
"scripts": {
 "dev": "cross-env NODE_ENV=development nodemon server.js",
 "start": "cross-env NODE_ENV=production node server.js"
}

Команда dev запускает сервер для разработки, а start — для продакшна.


База данных, роуты и сервер


Наши фиктивные данные будут выглядеть так (db/data.json):


[
 {
   "id": "1",
   "text": "Eat",
   "done": true
 },
 {
   "id": "2",
   "text": "Code",
   "done": true
 },
 {
   "id": "3",
   "text": "Sleep",
   "done": true
 },
 {
   "id": "4",
   "text": "Repeat",
   "done": false
 }
]

Структура данных — массив объектов. Каждый объект состоит из идентификатора (строка), текста (строка) и индикатора выполнения (логическое значение) задачи.


Инициализация БД (db/index.js):


import { join, dirname } from 'path'
import { fileURLToPath } from 'url'
import { Low, JSONFile } from 'lowdb'

// путь к текущей директории
const _dirname = dirname(fileURLToPath(import.meta.url))

// путь к файлу с фиктивными данными
const file = join(_dirname, 'data.json')

const adapter = new JSONFile(file)
const db = new Low(adapter)

export default db

Давайте определимся с архитектурой API.


Реализуем следующие конечные точки:


  • GET / — получение всех задач
  • GET /:id — получение определенной задачи по ее идентификатору. Запрос должен содержать параметр — id существующей задачи
  • POST / — создание новой задачи. Тело запроса (req.body) должно содержать объект с текстом новой задачи ({ text: 'test' })
  • PUT /:id — обновление определенной задачи по ее идентификатору. Тело запроса должно содержать объект с изменениями ({ changes: { done: true } }). Запрос должен содержать параметр — id существующей задачи
  • DELETE /:id — удаление определенной задачи по ее идентификатору. Запрос должен содержать параметр — id существующей задачи

Приступаем к реализации (routes/todo.routes.js):


import { Router } from 'express'
import db from '../db/index.js'

const router = Router()

// роуты

export default router

GET /


router.get('/', async (req, res, next) => {
 try {
   // инициализируем БД
   await db.read()

   if (db.data.length) {
     // отправляем данные клиенту
     res.status(200).json(db.data)
   } else {
     // сообщаем об отсутствии задач
     res.status(200).json({ message: 'There are no todos.' })
   }
 } catch (e) {
   // фиксируем локацию возникновения ошибки
   console.log('*** Get all todos')
   // передаем ошибку обработчику ошибок
   next(e)
 }
})

GET /:id


router.get('/:id', async (req, res, next) => {
 // извлекаем id из параметров запроса
 const id = req.params.id

 try {
   await db.read()

   if (!db.data.length) {
     return res.status(400).json({ message: 'There are no todos' })
   }

   // ищем задачу с указанным id
   const todo = db.data.find((t) => t.id === id)

   // если не нашли
   if (!todo) {
     return res
       .status(400)
       .json({ message: 'There is no todo with provided ID' })
   }

   // если нашли
   res.status(200).json(todo)
 } catch (e) {
   console.log('*** Get todo by ID')
   next(e)
 }
})

POST /


router.post('/', async (req, res, next) => {
 // извлекаем текст из тела запроса
 const text = req.body.text

 if (!text) {
   return res.status(400).json({ message: 'New todo text must be provided' })
 }

 try {
   await db.read()

   // создаем новую задачу
   const newTodo = {
     id: String(db.data.length + 1),
     text,
     done: false
   }

   // помещаем ее в массив
   db.data.push(newTodo)
   // фиксируем изменения
   await db.write()

   // возвращаем обновленный массив
   res.status(201).json(db.data)
 } catch (e) {
   console.log('*** Create todo')
   next(e)
 }
})

PUT /:id


router.put('/:id', async (req, res, next) => {
 // извлекаем id Из параметров запроса
 const id = req.params.id

 if (!id) {
   return res
     .status(400)
     .json({ message: 'Existing todo ID must be provided' })
 }

 // извлекаем изменения из тела запроса
 const changes = req.body.changes

 if (!changes) {
   return res.status(400).json({ message: 'Changes must be provided' })
 }

 try {
   await db.read()

   // ищем задачу
   const todo = db.data.find((t) => t.id === id)

   // если не нашли
   if (!todo) {
     return res
       .status(400)
       .json({ message: 'There is no todo with provided ID' })
   }

   // обновляем задачу
   const updatedTodo = { ...todo, ...changes }

   // обновляем массив
   const newTodos = db.data.map((t) => (t.id === id ? updatedTodo : t))

   // перезаписываем массив
   db.data = newTodos
   // фиксируем изменения
   await db.write()

   res.status(201).json(db.data)
 } catch (e) {
   console.log('*** Update todo')
   next(e)
 }
})

DELETE /:id


router.delete('/:id', async (req, res, next) => {
 // извлекаем id из параметров запроса
 const id = req.params.id

 if (!id) {
   return res
     .status(400)
     .json({ message: 'Existing todo ID must be provided' })
 }

 try {
   await db.read()

   const todo = db.data.find((t) => t.id === id)

   if (!todo) {
     return res
       .status(400)
       .json({ message: 'There is no todo with provided ID' })
   }

   // фильтруем массив
   const newTodos = db.data.filter((t) => t.id !== id)

   db.data = newTodos

   await db.write()

   res.status(201).json(db.data)
 } catch (e) {
   console.log('*** Remove todo')
   next(e)
 }
})

Сервер (server.js):


import express from 'express'
import router from './routes/todo.routes.js'

// экземпляр Express-приложения
const app = express()

// парсинг JSON, содержащегося в теле запроса
app.use(express.json())
// обработка роутов
app.use('/todos', router)

app.get('*', (req, res) => {
 res.send('Only /todos endpoint is available.')
})

// обработка ошибок
app.use((err, req, res, next) => {
 console.log(err)
 const status = err.status || 500
 const message = err.message || 'Something went wrong. Try again later'
 res.status(status).json({ message })
})

// запуск сервера
app.listen(3000, () => {
 console.log('🚀 Server ready')
})

Запускаем сервер для разработки:


yarn dev
# or
npm run dev




Адрес нашего APIhttp://localhost:3000/todos


Проверяем работоспособность сервера. Для этого я воспользуюсь Postman.


GET /





GET /:id





POST /





PUT /:id





DELETE /:id





Отлично. С этой задачей мы справились. Теперь сделаем работу с API доступной (и поэтому легкой) для любого пользователя посредством описания конечных точек, принимаемых параметров, тел запросов и возвращаемых ответов (частично мы это уже сделали при проектировании архитектуры API).


Описание и визуализация API


Для генерации документации к API мы будем использовать библиотеку swagger-autogen, а для визуализации — swagger-ui-express. Устанавливаем эти пакеты:


yarn add swagger-autogen swagger-ui-express
# or
npm i ...

Приступаем к реализации генерации описания (swagger/index.js):


import { join, dirname } from 'path'
import { fileURLToPath } from 'url'
import swaggerAutogen from 'swagger-autogen'

const _dirname = dirname(fileURLToPath(import.meta.url))

// const doc = ...

// путь и название генерируемого файла
const outputFile = join(_dirname, 'output.json')
// массив путей к роутерам
const endpointsFiles = [join(_dirname, '../server.js')]

swaggerAutogen(/*options*/)(outputFile, endpointsFiles, doc).then(({ success }) => {
 console.log(`Generated: ${success}`)
})

Документация генерируется на основе значения переменной doc и специальных комментариев в коде роутов.


Описываем API с помощью doc:


const doc = {
 // общая информация
 info: {
   title: 'Todo API',
   description: 'My todo API'
 },
 // что-то типа моделей
 definitions: {
   // модель задачи
   Todo: {
     id: '1',
     text: 'test',
     done: false
   },
   // модель массива задач
   Todos: [
     {
       // ссылка на модель задачи
       $ref: '#/definitions/Todo'
     }
   ],
   // модель объекта с текстом новой задачи
   Text: {
     text: 'test'
   },
   // модель объекта с изменениями существующей задачи
   Changes: {
     changes: {
       text: 'test',
       done: true
     }
   }
 },
 host: 'localhost:3000',
 schemes: ['http']
}

Описываем роуты с помощью специальных комментариев.


GET /


router.get('/', async (req, res, next) => {
 // описание роута
 // #swagger.description = 'Get all todos'
 // возвращаемый ответ
 /* #swagger.responses[200] = {
     // описание ответа
     description: 'Array of all todos',
     // схема ответа - ссылка на модель
     schema: { $ref: '#/definitions/Todos' }
 } */

 // код роута
})

GET /:id


router.get('/:id', async (req, res, next) => {
 // #swagger.description = 'Get todo by ID'
 // параметр запроса
 /* #swagger.parameters['id'] = {
   // описание параметра
   description: 'Existing todo ID',
   // тип параметра
   type: 'string',
   // является ли параметр обязательным?
   required: true
 } */
 /* #swagger.responses[200] = {
     description: 'Todo with provided ID',
     schema: { $ref: '#/definitions/Todo' }
 } */

 //  код роута
})

POST /


router.post('/', async (req, res, next) => {
 // #swagger.description = 'Create new todo'
 // тело запроса
 /* #swagger.parameters['text'] = {
   in: 'body',
   description: 'New todo text',
   type: 'object',
   required: true,
   schema: { $ref: '#/definitions/Text' }
 } */
 /* #swagger.responses[201] = {
     description: 'Array of new todos',
     schema: { $ref: '#/definitions/Todos' }
 } */

 // код роута
})

PUT /:id


router.put('/:id', async (req, res, next) => {
 // #swagger.description = 'Update existing todo'
 /* #swagger.parameters['id'] = {
   description: 'Existing todo ID',
   type: 'string',
   required: true
 } */
 /* #swagger.parameters['changes'] = {
   in: 'body',
   description: 'Existing todo changes',
   type: 'object',
   required: true,
   schema: { $ref: '#/definitions/Changes' }
 } */
 /* #swagger.responses[201] = {
   description: 'Array of new todos',
   schema: { $ref: '#/definitions/Todos' }
 } */

 // код роута
})

DELETE /:id


router.delete('/:id', async (req, res, next) => {
 // #swagger.description = 'Remove existing todo'
 /* #swagger.parameters['id'] = {
   description: 'Existing todo ID',
   type: 'string',
   required: true
 } */
 /* #swagger.responses[201] = {
   description: 'Array of new todos or empty array',
   schema: { $ref: '#/definitions/Todos' }
 } */

 // код роута
})

Это лишь небольшая часть возможностей по документированию API, предоставляемых swagger-autogen.


Добавляем в package.json команду для генерации документации:


"gen": "node ./swagger/index.js"

Выполняем ее:


yarn gen
# or
npm run gen

Получаем файл swagger/output.json примерно такого содержания:


{
 "swagger": "2.0",
 "info": {
   "title": "Todo API",
   "description": "My todo API",
   "version": "1.0.0"
 },
 "host": "localhost:3000",
 "basePath": "/",
 "schemes": [
   "http"
 ],
 "paths": {
   "/todos/": {
     "get": {
       "description": "Get all todos",
       "parameters": [],
       "responses": {
         "200": {
           "description": "Array of all todos",
           "schema": {
             "$ref": "#/definitions/Todos"
           }
         }
       }
     },
     // другие роуты
   }
 },
 "definitions": {
   "Todo": {
     "type": "object",
     "properties": {
       "id": {
         "type": "string",
         "example": "1"
       },
       "text": {
         "type": "string",
         "example": "test"
       },
       "done": {
         "type": "boolean",
         "example": false
       }
     }
   },
   // другие модели
 }
}

Круто. Но как нам это нарисовать? Легко.


Возвращаемся к коду сервера:


import fs from 'fs'
import swaggerUi from 'swagger-ui-express'

Определяем путь к файлу с описанием API:


const swaggerFile = JSON.parse(fs.readFileSync('./swagger/output.json'))

Определяем конечную точку /api-doc, при доступе к которой возвращается визуальное представление нашей документации:


app.use('/api-doc', swaggerUi.serve, swaggerUi.setup(swaggerFile))

swagger-ui-express предоставляет широкие возможности по кастомизации визуального представления.


Результат


На всякий случай перезапускаем сервер для разработки и переходим по адресу http://localhost:3000/api-doc.


Общий вид





GET /





GET /:id





POST /





PUT /:id





DELETE /:id





Модели





Пожалуй, это все, чем я хотел поделиться с вами в этой заметке.


Надеюсь, вам было интересно и вы не зря потратили время.


Благодарю за внимание и happy coding!




Теги:
Хабы:
+9
Комментарии 10
Комментарии Комментарии 10

Публикации

Информация

Сайт
timeweb.cloud
Дата регистрации
Дата основания
Численность
201–500 человек
Местоположение
Россия
Представитель
Timeweb Cloud