Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Привет, друзья!
В этой небольшой заметке я расскажу вам о том, как генерировать и визуализировать документацию к 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('