Привет, друзья!
В этой небольшой заметке я расскажу вам о том, как генерировать и визуализировать документацию к 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 ...
Структура проекта:
- 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.
Реализуем следующие конечные точки:
Приступаем к реализации (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
Адрес нашего API — http://localhost:3000/todos
Проверяем работоспособность сервера. Для этого я воспользуюсь [Postman]().
GET /
GET /:id
POST /
PUT /:id
DELETE /:id
Отлично. С этой задачей мы справились. Теперь сделаем работу с 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!
В этой небольшой заметке я расскажу вам о том, как генерировать и визуализировать документацию к 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
Адрес нашего API — http://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!
Node.js: документирование и визуализация API с помощью Swagger
Привет, друзья! В этой небольшой заметке я расскажу вам о том, как генерировать и визуализировать документацию к API с помощью Swagger . Мы разработаем простой Express-сервер , способный обрабатывать...
habr.com