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

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.



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


В этой небольшой заметке я расскажу вам о том, как генерировать и визуализировать документацию к 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('						
Источник: https://habr.com/ru/company/timeweb/blog/594081/


Интересные статьи

Интересные статьи

Не смотря на то, что Python был бы предпочтительным инструментом для исследовательского анализа, я хотел посмотреть, смогу ли я провести весь исследовательский анализ с помощью SQL-запросов. Моя цель ...
Спецификация W3C Scroll-linked Animations — это экспериментальное дополнение, которое позволяет связать развитие эффекта анимации с прокруткой. Подробностями делимся под катом, пока у нас начинается к...
Предисловие: — У меня есть небольшой заброшенный паблик (26к подписчиков), раньше там стоял пранк бот от чатуса, это приносило мне 300-800 рублей в день пассивного заработка, если сдела...
Два года назад я начал работать разработчиком ПО. Иногда я рассказывал своим коллегам о студенческом проекте, которым занимался на третьем курсе университета, и они восприняли его н...
Несколько дней назад, я решил провести реверс-инжиниринг прошивки своего роутера используя binwalk. Я купил себе TP-Link Archer C7 home router. Не самый лучший роутер, но для моих нужд вполне ...