Найти в Дзене
Я, Golang-инженер

#64. Руководство по Swagger & OpenAPI в Golang: автоматическая генерация спецификации API на примере сервиса backend+frontend

Оглавление

Это статья об основах программирования на Go. На канале я рассказываю об опыте перехода в IT с нуля, структурирую информацию и делюсь мнением.

Хой, джедаи и амазонки!

Продолжаю осваивать новые навыки, необходимые для джуниор-разработчика. В предыдущей публикации улучшал навык с СУБД SQLite, сейчас хочу наработать навык с проектированием спецификаций по стандарту OpenAPI: однажды часть тестового задания была разработка сервиса по такой документации и требовалось сгенерировать свою. Спецификацию я тогда написал вручную и потратил на это несколько дней.

Практиковать автоматизацию документирования API просто так не интересно - решил немного разобраться, как связаны бэкенд и фронтенд. Для этого напишу простой html-код и буду направлять его клиенту - браузеру; будет возможность на веб-странице нажимать на кнопки, создавая запросы для сервера. Намного интереснее тестировать и видеть свою работу, чем просто при работе с Postman без визуальной составляющей в браузере.

Когда вы прочитаете эту публикацию и повторите шаг за шагом каждое действие, вы разберётесь, как автоматически создавать спецификацию API такого типа:

Пример спецификации по стандарту OpenAPI с визуализацией
Пример спецификации по стандарту OpenAPI с визуализацией

Публикация получилась длинная, вперёд - разбираться.

1. Немного теории и рекламы

Здесь хочу прорекламировать бесплатный курс на Stepik, на котором когда-то давно учился верстать страницы - "Веб-разработка для начинающих: HTML и CSS":

Скриншот курса
Скриншот курса

На курсе мы поэтапно верстали аналог страницы о г.Хьюстон с Википедии.

Вам, конечно, необязательно проходить этот курс, чтобы попрактиковать фронтенд, для добавления к своему бэку визуальную часть. Я, чтобы не вспоминать все эти теги, заголовки и прочее из мира вёрстки сайтов, воспользовался услугами нейросети.

По поводу теории, просто напомню, что такое сваггер.

Swagger - это инструментарий для разработки документации на API по стандарту OpenAPI.

OpenAPI - это правила оформления документа для документирования REST API, выполняется в форматах json или yaml. Такая спецификация позволяет разработчикам четко и последовательно определять структуру API, включая конечные точки, параметры, заголовки, ответы, ошибки и другие вещи.

Пример текстового описания API по стандарту OpenAPI ниже:

Описательная часть
Описательная часть

Согласитесь, вручную писать такое - мягко говоря затруднительно.

Для меня, типичный сценарий, когда в реальной разработке нужна эта документация по стандарту OpenAPI, выглядит так:

Команда разработчиков создаёт веб-сервис и фиксируют документально, какие эндпоинты будут доступны через API, как они организованы, какие запросы и ответы должен поддерживать сервис.
Например, будут эндпоинты: /users, /users/{id} /products/cheese и т.д.
Эндпоинт /users поддерживает запрос GET и ожидает ответ формата yaml с конкретными полями и должны быть предусмотрены определённые статусы ответа сервера; а эндпоинт /products/cheese поддерживает запрос POST и предусматривает передачу формата json со своими полями...
Далее этой информацией будут руководствоваться в процессе разработки веб-сервиса новые программисты, QA-инженеры, сторонние разработчики, хотят интегрировать какое-то своё приложение в этот веб-сайт, или кто-нибудь ещё, глядя на относительно простую и понятную спецификацию API в графическом редакторе

Пример визуализации спецификации по стандарту OpenAPI из шаблона Swagger Editor представлен ниже:

Документация по стандарту OpenAPI в Swagger Editor https://editor.swagger.io/
Документация по стандарту OpenAPI в Swagger Editor https://editor.swagger.io/

Всю эту информацию можно получить и изучая код - но для этого нужно потратить несоразмерно больше времени и обладать способностью "читать чужой код".

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

2. Разрабатываем веб-сервис

2.1. Структура сервиса

Чтобы не переделывать или дорабатывать сервис, продумаю его архитектуру заранее. Какие у меня есть требования к сервису:

  1. Минимализм, чтобы не уходить в дебри в целях обучения;
  2. Наличие CRUD-операций (Create-Read-Update-Delete), чтобы попрактиковать написание спецификации по стандарту OpenAPI в разных вариантах;
  3. Наличие фронтенда - простейшие html-документы, возможно даже без CSS;
  4. Наличие json'а в передаваемых клиентом и/или отправляемых сервером данных.
  5. Без базы данных для минимализма кода сервиса. Хранить входящую информацию будем в файлах.

Исходя из вышеизложенных требований, создал такую структуру:

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

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

Кстати, в целях подготовки к работе с сервером через терминал, выполнил команды по созданию каталогов и файлов через CLI:

Работа в терминале
Работа в терминале

Сперва создал файл go mod, затем одной командой mkdir создал три каталога, и далее одной командой touch создал несколько файлов.

2.2. HTML-файл

Чередой запросов-ответов, нейросеть подготовила для меня подходящую страничку .html. Откроем её в браузере без написания какого-либо бэкенда:

Первая часть веб-страницы
Первая часть веб-страницы
Вторая часть веб страницы
Вторая часть веб страницы

Интерфейс веб-страницы поделён на несколько блоков, указаны CRUD-операции.

В html-файле есть три составляющие:

  1. Сама html-разметка страницы;
  2. Стили элементов страницы;
  3. Код на JavaScript.

Стили обычно пишут в отдельном CSS-файле, как и код на JavaScript - не встраивают в html-документ, а создают отдельный файл с расширением .js. Для наших целей удобнее поместить всё в один файл. Ниже примеры фрагментов каждой из трёх составляющих документа.

Разметка веб-страницы
Разметка веб-страницы
Стили веб-страницы
Стили веб-страницы
Код на JavaScript веб-страницы начинается с тега script
Код на JavaScript веб-страницы начинается с тега script

Если со стилями и разметкой веб-страницы, я думаю, всё понятно, то что делает код JavaScript во фронтенде? В моём коде у него следующие функции:

  1. Обрабатывает клики на кнопки: мы нажимаем не на текстовые ссылки, а на графические элементы;
  2. Формирует url-запроса для отправки на сервер;
  3. Формирует json для тела запроса перед отправкой запроса на сервер;
  4. Обрабатывает ответ сервера: показывает уведомление о статусе операции или полученные данные.

При загрузке веб-страницы в браузере, код JavaScript выполняется уже на стороне клиента: в браузерах есть встроенные движки для обработки и выполнения JavaScript-кода, как и html и css-разметки.

По п.2 из перечня выше: url-запросы содержат параметры для GET, PUT и PATCH методов согласно принципам REST. Для метода GET не принято передавать тела запроса, а какой именно товар нужно получить, передаётся в url в виде параметра ключ-значение либо расширением эндпоинта, например:

  • /home/item/apple - расширение эндпоинта (используется в моём html-документе);
  • /home/item?product=apple - добавление параметра к запросу (product - поле json-структуры для общения клиент-сервер, описанной в html-документе).

Переходим к написанию бэкенда.

2.3. Основа backend'а

Напишем минимальный необходимый код для отображения веб-страницы:

Простой сервер
Простой сервер

Хорошо, сервер запустили, веб-страницу в браузере запустили командой

http://localhost:8080/

Кстати, если вы будете экспериментировать и веб-страница будет отображать не то, что ожидается, проблема может быть с КЭШем, для решения попробуйте:

  1. Запустите режим "инкогнито" в браузере сочетанием клавиш Ctrl+Shift+N и откройте веб-страницу там - посмотрите, что будет;
  2. Перезагрузите страницу вне режима "инкогнито" с очисткой КЭШа сочетанием клавиш Ctrl+Shift+R.

По части кода сервера хочу отдельно проговорить о функции Handle пакета http.

Обработчик http.Handle в строке 9 на иллюстрации будет отдавать контент, находящийся в каталоге templates за счёт функции FileServer того же пакета http при любом запросе из браузера (на соответствующий хост и порт). Сейчас функция отдаёт html-файл, который находится у меня в каталоге:

Содержимое каталога templates
Содержимое каталога templates

Вопрос: что отдаст сервер, если в этом каталоге несколько файлов? Например, мой hmtl файл и несколько иллюстраций или что-то ещё, например, текстовый файл? Вот примерно так:

Добавим пару файлов в каталог
Добавим пару файлов в каталог

Ответ: по-прежнему веб-страницу. Почему? Переходим в документацию, читаем о функции FileServer пакета http:

Документация Go на функцию FileServer
Документация Go на функцию FileServer

Суть в чём - в первую очередь функция FileServer ищет в переданном в неё каталоге файл index.html, и отдаёт клиенту его. А вот если такой функции не находит, т.е. в каталоге будет например только два указанных выше файла:

Обновлённое содержимое каталога templates
Обновлённое содержимое каталога templates

То при загрузке веб-страницы командой localhost:8080, мы получим список файлов из каталога для выбора - что всё-таки хотим загрузить:

Содержимое веб-страниц
Содержимое веб-страниц

Вернём файл index.html в каталог templates, удалим лишние файлы, и продолжим разработку веб-сервиса.

2.4. Обработчик Create: POST-запросы

Операция Create нужна для сохранения данных в хранилище по запросу клиента. Общаясь в соцсети при отправлении текстового сообщения или фотографии, заполняя ФИО в профиле и многое другое - это POST-запросы. Мы постоянно создаём новые данные, которые хранятся на серверах.

2.4.1. Алгоритм запроса клиента

Напишем первый обработчик, который будет обрабатывать запросы клиента при нажатии кнопки "Создать":

Фрагмент веб-страницы
Фрагмент веб-страницы

Как указано на иллюстрации, команда Create относится к POST-запросу согласно принципам REST и протоколу http. Происходить будет следующее:

  1. Пользователь вводит в браузер адрес сайта;
  2. Браузер обращается к серверу по url и сервер отдаёт браузеру файл index.html с разметкой веб-страницей, стилями и кодом JavaScript;
  3. Пользователь смотрит на появившуюся в браузере веб-страницу, заполняет на веб-странице соответствующие поля и нажимает кнопку "Создать";
  4. Драйвер браузера обрабатывает код, написанный на JavaScript внутри файла index.html и отправляет серверу json с полями { product и price } с типами данных string и float соответственно по url /home/create_item.

На этом моменте нам нужен обработчик соответствующих событий.

2.4.2. Первая версия обработчика POST-запроса

Код с обработчиком POST-запроса
Код с обработчиком POST-запроса

Реализацию обработчика напишем в другом пакете - в файле /handlers/handlers.go. Но сперва напишем не обработчик, а сделаем внутри этого пакета ещё два файла:

Содержимое каталога handlers через терминал
Содержимое каталога handlers через терминал

В файле msg буду прописывать в константах код ошибок и информационных сообщений, которые будут повторяться в разных обработчиках. А в файле types пропишу структуру с полями json, которые передаёт фронтенд. Сейчас содержимое файлов выглядит так:

Константы сообщений
Константы сообщений
Структура в отдельном пакете
Структура в отдельном пакете

Переходим к реализации обработчика:

Реализация обработчика
Реализация обработчика

В строке 11 log.Printf... я пишу лог, который будет публиковать в терминал информацию о поступившем запросе с указанием с какого url пришёл запрос, протокол передачи данных и метод запроса. Константа msgNewRequest определена в другом файле этого же пакета, см. иллюстрацию выше.

В строке 13 мы создаём экземпляр структуры, сама структура описана в другом файле этого же пакета.

В случае неудачи декодирования переданного клиентом json'а, мы выводим сообщение об ошибке логом в терминал и отправляем клиенту статус ошибки.

http.StatusBadRequest - это константа в виде числа. Хорошая практика прописывать статусы ответов не в виде чисел типа 404 или что-то ещё, а использовать константы, заботливо предоставленные разработчиками языка Go:

Часть констант статусов ответа
Часть констант статусов ответа

Ну и в конце я пока просто печатаю в терминал содержимое декодированного json-файла, чтобы посмотреть - то вообще пришло, или не то, что ожидалось.

2.4.3. Тестирование первой версии обработчика POST-запросов

Прототип обработчика POST-запросов написан, проверяем. Запускаем приложение в терминале:

Запуск сервера
Запуск сервера

Переходим в браузер, вводим информацию в поля раздела "Создать товар" и нажимаем кнопку "Создать". Появляется сообщение:

Запрос и ответ на стороне клиента
Запрос и ответ на стороне клиента

Переходим в терминал, смотрим что он пишет:

Логи в терминале
Логи в терминале

Отлично, информация от клиента получена и декодирована.

Доработаем обработчик:

  • Добавим функцию сохранения информации в файл (мы при определении архитектуры приложения решили, что не стоит перегружать учебный проект подключением БД, и ограничимся сохранением информации в файл).
  • Добавим проверку, что запрос с данного URL поступил с методом POST, а, например не GET, чтобы не делать лишнюю логику при ошибке.
  • Изменим тип контента ответа: сейчас он был application/json, но я не возвращаю json-ов, а возвращают простой текст и то, если будет ошибка.

2.4.4. Вторая версия обработчика POST-запросов

Обновил код обработчика:

Код обработчика
Код обработчика

Функция сохранения полученной от клиента информации в файл:

Код сохранения файла
Код сохранения файла

В коде сохранения мы делаем следующее:

  1. В строке 43 if ok = ... проверяем, все ли поля json-структуры заполнены: хотя это должно быть реализовано на стороне клиента в JavaScript, но мы фронтендерам доверять не станем и реализуем свои проверки. Детали реализации пока не важны, важно что либо все поля в json переданы и декодированы, либо нет.
  2. Создаём каталог, если его нет, в котором будем хранить информацию.
  3. Определяем имя файла по названию товара, переданного в json-поле procuct от клиента.
  4. Если файл с таким именем уже есть - возвращаем ошибку.
  5. Преобразуем цену товара в строку.
  6. Создаём файл с наименованием товара, а внутрь помещаем стоимость товара, переданную в json-поле price.
  7. Печатаем лог об успешном завершении сохранения данных и возвращаем отсутствие ошибки.

В пакете я объявил константы, необходимые для функции сохранения:

Константы для работы с файлами и директорией
Константы для работы с файлами и директорией

О константе perm и 0644 я писал в публикации #33. Права доступа к файлам и основы обработки ошибок.

Так выглядит функция проверки полей, декодированных в экземпляр структуры из json-объекта клиента:

Код проверки полей json-объекта
Код проверки полей json-объекта

Также я обновил содержимое файла msg, указав там новые сообщения в виде констант и доработал существующие константы:

Константы сообщений
Константы сообщений

2.4.5. Создаём несколько записей в хранилище и тестируем

Итак, обработчик доработан бизнес-логикой. Загрузим на сервер несколько товаров со стороны клиента:

Пример добавления товара
Пример добавления товара

Проверим через терминал содержимое каталога, куда сохраняем товары и цену на последний добавленный товар:

Команды в терминале
Команды в терминале

Всё в порядке: в каталоге появляются файлы, в файлах - цена товара.

Кстати, фронтенд не позволяет отправить запрос без заполнения любого или обоих полей:

Фрагмент веб-сайта
Фрагмент веб-сайта

Но если бы фронтенд не предусматривал такой защиты от ошибок, мы реализовали этот код на стороне сервера: проверим его через postman, в который мы можем прописать свой json, чего не можем сделать через браузер:

Работа с Postman
Работа с Postman

Как и ожидали - ответ с ошибкой 400 и сообщением в теле ответа об ошибке.

То же самое можем получить, если в одном терминале запустим сервер, а в другом создадим запрос через утилиту curl, ниже на иллюстрации представлены два терминала - хотя зрительно видимость одного:

Тестируем сервер в терминале
Тестируем сервер в терминале

Проверим ещё в Postman'e, что будет, если сделаем не POST-операцию, а какую-нибудь другую:

Работа в Postman
Работа в Postman

Как и ожидалось - в теле ответа информационное сообщение, и код статуса ответа 405 "Method Not Allowed".

Посмотрим, что пишет терминал:

Работа в терминале
Работа в терминале

Терминал соответственно тоже выдаёт лог ошибки.

2.4.6. Третья версия обработчика POST-запроса

Пока я писал этот пост, лучше разобрался с архитектурой REST. И вот что выяснил: для POST-запросов необходимо возвращать код статуса 201, а не 200 и информацию о созданном контенте. Пусть это будет имя созданного файла. Доработаем реализацию обработчика:

Код обработчика
Код обработчика

Доработаем функцию сохранения - теперь она возвращает имя файла по относительному пути:

Сохранение файла
Сохранение файла

При работе из браузера, визуально ничего не изменится. Протестируем обработчик через Postman:

Работа в Postman
Работа в Postman

В теле ответа появилась информация о созданном ресурсе. Думаю, можно считать этот обработчик, спроектированным по принципам REST.

Итак, первый эндпоинт обработали. Остаётся ещё 4.

2.5. Обработчик Update: PATCH-запросы

Изменяя отправленное сообщение в соцсети, редактируя свой профиль в Тенчат (отечественный аналог LinkedIn), изменяя язык или настройки уведомления на веб-странице, редактируя пост в блоге, изменяя пароль или адрес доставки в интернет-магазине - мы создаём PATCH-запросы: изменяем ранее созданные данные.

Patch можно перевести с английского, как "заплатка". Это своего-рода доработка ранее пошитой одежды.

2.5.1. Данные от клиента

Разберём, что отдаёт нам клиент при Patch-запросе.

  • Клиент отдаёт URL вида: /home/update_item/<ТекущееНазваниеТовара>
  • Json-объект вида: { "price": 190.60 }

Таким образом, нам нужно расшифровать конечную часть энпоинта в наименование текущего товара, найти файл с таким именем и перезаписать содержимое файла обновлённой ценой.

В случае успеха обновления будем возвращать статус ответа 204 - работу выполнили, но тела ответа не направляем. Статус 204 (как и все остальные коды статусов) определён константой в пакете http как StatusNoContent.

2.5.2. Первая версия PATCH-запроса

Добавляем обработчик обновлений товара в функцию main пакета main:

Обработчик PATCH-запроса
Обработчик PATCH-запроса

Пишем реализацию обработчика:

Реализация обработчика
Реализация обработчика

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

В функции должно быть всё понятно - делаем примерно то же самое, что при реализации обработчика сохранения файлов.

Затем реализуем функцию обновления данных:

Код обновления данных
Код обновления данных

В строке 96 получаем из url название товара.

В строке 98 присваиваю полю Product экземпляра структуры значение товара, т.к. в текущем экземпляре структуры это поле отсутствует - клиент передал json-объект с полем. А я хочу воспользоваться функцией по проверке корректности декодированных в экземпляр структуру полей и не создавать новую функцию: существующая функция, созданная ранее, проверяет оба поля и выдаст ошибку, если не найдёт поля Product.

В строке 107 перезаписываю файл новым содержимым.

Ещё я решил вывести в отдельную функцию процесс преобразования типа float64 в строку текста:

Преобразование типов
Преобразование типов

В остальном код похож на сохранение файла в POST-запросе. Переходим к проверке.

2.5.3. Проверяем работу обработчика PATCH-запросов

Заполняем поля на веб-странице:

Работа в браузере
Работа в браузере

Смотрим, что пишет терминал сервера:

Работа в терминале
Работа в терминале

Так, мы видим что получен запрос и напечатан товар, расшифрованный из эндпоинта. Мы забыли добавить лог успешного обновления. Это мы сделаем, а сейчас проверим что там в содержится в файле "Дыня.txt":

Работа в терминале
Работа в терминале

Так, у нас нет информации - что было в файле до обновления. Обновим в браузере данные вновь и посмотрим на содержимое файла:

Обновление товара Дыня в браузере
Обновление товара Дыня в браузере
Смотрим в терминале
Смотрим в терминале

Итак, данные вновь обновлены. Мы молодцы. Переходим к следующему запросу.

2.6. Обработчик Update: PUT-запросы

PUT-запросы похожи на PATCH, но обновляют все данные, а не часть. Честно говоря, я не разобрался, когда нужны PUT, а когда PATCH-запросы: PUT в моём понимании менее применим. А так его цель та же - обновить ранее созданные данные, но если PATCH обновлял часть данных, то PUT перезаписывает все данные.

2.6.1. Данные от клиента

На веб-странице мы обновляем все данные о товаре:

Фрагмент веб-страницы для PUT-запроса
Фрагмент веб-страницы для PUT-запроса

Разберём, что отдаёт нам клиент при PUT-запросе.

  • Клиент отдаёт URL вида: /home/update_item/<ТекущееНазваниеТовара>
  • Json-объект вида: {"product": "НовыйТовар", "price": 200.50 }

Как и для PATCH-запроса, нам нужно расшифровать конечную часть энпоинта в наименование текущего товара, найти файл с таким именем и перезаписать содержимое файла обновлённой ценой и наименование файла с новым товаром.

В случае успеха обновления будем возвращать статус ответа будет таким же 204 - работу выполнили без создания нового ресурса, и тела ответа не направляем.

Вот что ещё я обнаружил - что клиент отправляет нам по одному url запросы типа PATCH и PUT: /home/update_item/<ТекущееНазваниеТовара>

Если мы не хотим использовать сторонние библиотеки, где при объявлении обработчиков можно сразу указать метод, то нам потребуется сделать отдельную функцию.

2.6.2. Первая версия обработчика PUT-запросов

Функцию main пакета main не будем обновлять: у нас по одному эндпоинту будут обслуживаться два типа запросов: PUT и PATCH.

Создадим новую функцию, которая будет смотреть, какой метод пришёл с /home/update_item/<ТекущееНазваниеТовара>, доработаем реализацию обработчика PATCH и напишем реализацию обработчика PUT.

Код обработчика единого эндпоинта для нескольких методов
Код обработчика единого эндпоинта для нескольких методов
Обновлённый код реализации обработчика PATCH-запросов
Обновлённый код реализации обработчика PATCH-запросов
Реализация обработчика PUT-запросов
Реализация обработчика PUT-запросов
Логика реализации PUT-запроса
Логика реализации PUT-запроса

Код написан, добавлены несколько констант с сообщениями об ошибок, которые я приводить здесь не буду, т.к. их содержимое не принципиально и интуитивно понятно. Тестируем сервис.

2.6.3. Тестируем PUT-запросы

Заполняем соответствующие поля на веб-сайте:

Работа в браузере
Работа в браузере

Проверяем, что в терминале:

Работа в терминале
Работа в терминале

Сервис отработал корректно: заменил имевшийся файл Дыня со своим содержимым на товар Арбуз с новым содержимым. Проверим содержимое каталога и файла:

Работа в терминале
Работа в терминале

Всё на своих местах. Переходим к следующему типу запроса.

2.7. Обработчик DELETE-запросов

DELETE-запросы удаляют ресурсы: ненужные записи в мессенджере или блоге, фотографии, контакты в телефонной книжке (в т.ч. из облачного хранилища) и т.д.

2.7.1. Данные от клиента

На веб-странице мы обновляем все данные о товаре:

Фрагмент веб-сайта
Фрагмент веб-сайта

Разберём, что отдаёт нам клиент при DELETE-запросе.

  • URL вида: /home/delete_item/<НазваниеТовара>

В случае успеха удаления будем возвращать статус ответа таким же, как при PUT и PATCH: 204 - работу выполнили, и тело ответа не направляем.

Если товар не будет найден, вернём клиенту код статуса 404 - ресурс не найден.

В случае других ошибок будем возвращать статусы кодов:

  • Передан не DELETE-запрос: код 405 - статус не разрешён;
  • Не смогли выполнить запрос: код 500 - внутренняя ошибка сервера.

2.7.2. Первая версия обработчика DELETE-запроса

Добавим обработчик в функцию main пакета main:

Создаём обработчик DELETE-запроса
Создаём обработчик DELETE-запроса

Реализуем обработчик:

Реализация обработчика DELETE-запросов
Реализация обработчика DELETE-запросов

Особенность этой реализации в том, что из функции, отвечающей за бизнес-логику я возвращаю ошибку и статус: если сервер "пятисотит" - это один ответ клиенту, а если товар не найден - т.е. код статуса 404, то ответ клиенту другой. В остальном логика та же, как в предыдущих запросах.

Реализация функции удаления товара:

Код удаления товара
Код удаления товара

Код написан, тестируем.

2.7.3. Реализуем DELETE-запрос

Заполняем поле на веб-сайте:

Работа в браузере
Работа в браузере

Смотрим, что пишет терминал:

Работа в терминале
Работа в терминале

Я использовал раздельный терминал: верхняя часть - работа сервера, нижняя часть - вспомогательный терминал, посмотрел содержимое каталога где хранятся данные о товарах: видно, что товар удалён.

Попробуем вновь удалить этот же товар:

Повторное удаление товара
Повторное удаление товара

Ответ сервера браузеру: нечего удалять. Смотрим, что пишет терминал:

Работа в терминале
Работа в терминале

Всё как и ожидалось: код работает. Переходим к следующему запросу.

2.8. Обработчик GET-запросов

GET-запросы запрашивают информацию: загрузка и просмотр веб-страницы, поисковой запрос, просмотр изображения из миниатюры и т.д.

2.8.1. Данные от клиента

На веб-странице мы ищем данные о конкретном товаре:

Фрагмент веб-сайта
Фрагмент веб-сайта

Разберём, что отдаёт нам клиент при GET-запросе.

  • URL вида: /home/item/<НазваниеТовара>

В случае успеха получения информации, будем возвращать статус ответа 200 и json с информацией о товаре.

Если товар не будет найден, вернём клиенту код статуса 404 - ресурс не найден.

В случае других ошибок будем возвращать статусы кодов:

  • Передан не GET-запрос: код 405 - статус не разрешён;
  • Не смогли выполнить запрос: код 500 - внутренняя ошибка сервера.

2.8.2. Обработчик GET-запроса

Дополним функцию main пакета main:

Добавили обработчик GET-запросов
Добавили обработчик GET-запросов

Напишем реализацию обработчика GET-запросов:

Реализация обработчика GET-запросов
Реализация обработчика GET-запросов

В коде мы делаем следующее:

  1. Печатаем лог в терминал с информацией о полученном запросе;
  2. Проверяем корректность метода запроса;
  3. Вызываем функцию, возвращающую нужный для отправки клиенту json-объект, статус ответа ошибки если имеется и ошибку;
  4. Две проверки на коды статусов, если есть ошибки;
  5. Отправка кода статусу и json-объект клиенту.

Напишем функцию для подготовки json-объекта для отправки клиенту:

Код подготовки ответа
Код подготовки ответа

В коде выполняется следующее:

  1. Извлекаем из ULR наименование необходимого товара;
  2. Формируем имя файла для поиска товара;
  3. Выполняем проверку, есть ли такой файл;
  4. Вызываем функцию, возвращающую цену товара с типом данных float64 и ошибку;
  5. Выполняем проверку, удалось ли получить цену товара;
  6. Создаём экземпляр структуры и кладём в неё имя требуемого товара и его цену;
  7. Формируем json-объект из созданного экземпляра структуры;
  8. Выполняем проверку успешности создания json-объекта;
  9. Печатаем лог в терминал об успешном создании json-объекта;
  10. Возвращаем json-объект.

Напишем функцию чтения файла. Она читает файл и преобразует тип string в тип float64 - хотя считается для чистого кода, каждая функция должна делать что-то одно для гибкости кода, я считаю что эта функция лаконична и гибка:

Код получения цены
Код получения цены

К слову, название функции readFile не совсем отражает её действие - думаю, логичнее назвать её price или как-то в этом духе.

Итак, код написан, проверяем.

2.8.3. Тестируем обработчик GET-запроса

Заполняем требуемое поле в браузере:

Веб-страница
Веб-страница

Сразу получаем уведомление с json-объектом. Смотрим, что происходит в терминале:

Работа в терминале
Работа в терминале

В верхней части - терминал сервера, в нижней части - вспомогательный терминал, который показывает какие файлы в хранилище и содержимое файла запрошенного товара. Всё сходится.

На этом этапе можно заканчивать разработку сервиса и переходить к Swagger'у.

3. Инструментарий Swagger

Основная идея автоматической генерации спецификации API по стандарту OpenAPI, следующая:

  1. В коде пишем особые комментарии об API - своего рода, высокоуровневый код;
  2. Устанавливаем утилиту для генерации спецификации API;
  3. Запускаем утилиту и она генерирует спецификацию, преобразуя высокоуровневые комментарии в более низкоуровневые.

Далее эту спецификацию можем использовать для визуализации API - скриншоты в начале публикации.

3.1. Общая информация

Я нашёл несколько библиотек для автоматической генерации спецификации API по стандарту OpenAPI:

  • Swaggo;
  • go-swagge;
  • gofiber;
  • oapi-codegen;
  • ogen-go.

Разбираться будем с первой - Swaggo. Установим его, ниже скриншот из официальной документации на GitHub:

Установка библиотеки Swaggo
Установка библиотеки Swaggo

Далее нужно каким-то образом сообразить, как дополнить комментариями наш код, чтобы Swaggo считал информацию и преобразовал в спецификацию API по стандарту OpenAPI. Ниже фрагмент информации по комментированию кода из ReadME библиотеки Swaggo:

Таблица комментирования
Таблица комментирования

Также имеется выбор библиотек по обработке http-запросов, поддерживаемых библиотекой Swaggo - здесь есть и стандартная библиотека net/http:

Перечень поддерживаемых фреймворков
Перечень поддерживаемых фреймворков

Суть в чём - нужно добавить комментарии согласно требованиям библиотеки Swaggo. Напишем минимальное количество комментариев для одного обработчика, чтобы протестировать как всё работает.

3.2. Документация на обработчик POST-запроса

Swagger-аннотирование делится на два этапа: общее описание проекта, которое выполняем в пакете main перед функцией main, и аннотации для каждого обработчика.

3.2.1. Минимальная спецификация по OpenAPI

Сделаю самый простой проект, чтобы опробовать технологию автогенерации.

Заполню общее описание в функции main комментариями title, version, description, contact name-url-evail, host и basePath - которые интуитивно понятны:

Общее описание проекта перед функцией main
Общее описание проекта перед функцией main

Описание получилось какое-то неотформатированное. Воспользуюсь командой swag fmt в терминале, результат:

Отформатированное swagger-описание проекта
Отформатированное swagger-описание проекта

Создал описание обработчика перед функцией-реализацией обработчика POST-запроса:

Swagger-описание обработчика
Swagger-описание обработчика

Посмотрим разъяснения по полям Accept, Produce и Param в ReadME библиотеки Swaggo - остальные поля, думаю, интуитивно понятны.

Описание аннотаций Swagger
Описание аннотаций Swagger

В документация есть ссылка на Mime Types.

MIME (Multipurpose Internet Mail Extensions) types — это стандартизированные идентификаторы, которые используются для указания типа содержимого, передаваемого через интернет. Изначально разработанные для электронной почты, сейчас MIME-типы широко применяются в HTTP-протоколе для указания типа файлов, передаваемых между клиентом и сервером.

Посмотрим, что в ReadME библиотеки Swaggo говорится о Mime Types:

Фрагмент ReadMe библиотеки Swaggo о Mime Types
Фрагмент ReadMe библиотеки Swaggo о Mime Types

В общем, по-русски говоря, аннотация @Accept говорит о том, какие данные в теле запроса ожидаются от клиента. В моём случае API ожидает от клиента в теле запроса json-объект.

Аннотация @Produce говорит о том, какой объект в теле ответа сервер возвращает клиенту: в данном случае также json-объект.

Аннотация @Param используется для определения параметра, который передаются в API-метод. Её расшифровка посложнее двух предыдущих аннотаций. Взглянем ещё раз на аннотацию:

// @Param item body Item true "Создаем новый товар"

или её можно представить так:

// @Param <param name> <param type> <data type> <is mandatory?> "<comment>"

Разберем теги аннотации Param подробнее:

1. Тег name - это просто название, чтобы по-названию объяснить структуру данных, которая ожидается из запроса. В нашем случае у тега name наименование item.

2. Тег param type указывает, где в запросе находится параметр, в нашем случае - в body. В ReadMe библиотеки Swaggo указаны следующие типы:

Разновидности Param type библиотеки Swaggo
Разновидности Param type библиотеки Swaggo

3. Тег data type определяет тип данных для объекта тега <name>. В нашем случае тип данных Item - это наша пользовательская структура, описанная в коде, см. параграф 2.4.2 публикации. Посмотрим, что написано о типах данных в Readme библиотеки Swaggo:

Фрагмент ReadMe библиотеки Swaggo
Фрагмент ReadMe библиотеки Swaggo

В общем, аннотация @Param через тег <data type> подтягивает структуру Item в спецификацию.

4. Тег is mandatory? определяет, требуется ли этот параметр для выполнения запроса (в нашем случае указано true, - требуется). Пока не могу представить примеров, где этот параметр будет false.

5. Тег comment задаёт описание параметра, чтобы читающие спецификацию специалисты понимали, что должно быть передано в этом параметре.

Теперь строка // @Param item body Item true "Создаем новый товар" не должна вызывать вопросов.

Ещё хочу сказать про аннотацию @Success - документирует, какой должен быть ответ при успешном выполнении запроса. Взглянем на описание аннотации в ReadMe библиотеки Swaggo:

Фрагмент ReadMe библиотеки Swaggo
Фрагмент ReadMe библиотеки Swaggo

В целом, всё знакомые теги.

Напомню, как выглядит аннотация в нашем коде:

// @Success 201 {object} Item "Товар успешно создан"

Тег {object} указывает, что в ответе будет передан объект, описанный следующим тегом. А следующим тегом у нас Item, а объект Item - это наша пользовательская структура, содержащая json-объект.

Взглянем вновь на написанные для обработчика аннотации:

Аннотации обработчика
Аннотации обработчика

Теперь уже, думаю, с новым пониманием того, что здесь указано.

Разобрались с аннотациями. Запускаем автоматическую генерацию спецификации по стандарту OpenAPI командой swag init в терминале:

Работа в терминале
Работа в терминале

На иллюстрации мы запустили генерацию документации, получили логи документации, посмотрели содержимое каталога где должна создаться документация и начало содержимого одного из файлов.

Может показаться, что мы пишем какой-то непонятный код в виде комментариев Swagger: для чего? Для того, чтобы получить другой код (фрагмент которого представлен в терминале)? А почему бы не убрать из цепочки транслятор. Можно относится к этому так:

При написании комментариев swagger, мы создаём высокоуровневое описание API, а библиотека swaggo выполняет роль интерпретатора, который использует наши аннотации для написания более низкоуровневого кода, понятного компьютеру.
Написание swagger-аннотаций в коде намного проще и гораздо быстрее, чем писать спецификацию на API по стандарту OpenAPI вручную.

3.2.2. Работа в Swagger Editor

Перейдём в браузере в Swagger Editor по пути https://editor.swagger.io/ и вставим в неё сгенерированную спецификацию. Результат:

Работа со Swagger Edior
Работа со Swagger Edior

Йуху! Мы создали нашу первую спецификацию по стандарту OpenAPI методом автоматической генерации. Можно раскрыть ручку POST и посмотреть что там:

Работа со Swagger Edior
Работа со Swagger Edior

Пока здесь нет понятных примеров запросов и кодов статусов возможных ошибок, заголовков - но это демо-версия. Со всем этим разберёмся далее.

Что ж, мы протестировали команды swagger и создали работающую спецификацию, идём дальше.

3.2.3. Доработка Swagger-аннотаций обработчика POST-запросов

3.2.3.1. Добавим описание нашему обработчику:

Код
Код

После генерации спецификации, этот код будет выглядеть в Swagger Editor так (выделил текст):

Визуализация спецификации API
Визуализация спецификации API

3.2.3.2. Добавим тег в шапку

Чтобы в шапке был тег не default, а осмысленный, добавим тег item. Он нам понадобиться, чтобы упростить понимание что происходит при обращении по /home/create_item:

Код
Код

После генерации спецификации, этот код будет выглядеть в Swagger Editor так:

Визуализация спецификации API
Визуализация спецификации API

3.2.3.3. Добавим пример для запроса

Напомню, как сейчас выглядит визуализация спецификации в части информации о запросе:

Визуализация упрощённой документации API
Визуализация упрощённой документации API

Есть возможность указывать не стандартные типы для полей json-объекта, а конкретные цифры и текст: это упрощает понимание API. В ReadMe хитро и как-бы вскользь указано описание этого функционала: нужно доработать нашу структуру, а конкретно - добавить к json-описанию поле example - оно должно называться именно так, и описание к нему, например:

Код
Код

После генерации спецификации, её визуализация будет выглядеть так:

Визуализация спецификации API
Визуализация спецификации API

Согласитесь, информация нагляднее.

3.2.3.4. Доработаем ответ

Напомню, как выглядит сейчас визуализация спецификации API:

Визуализация старой версии API
Визуализация старой версии API

Так выглядит аннотация, отвечающая за блок Example Value визуализации:

// @Success 201 {object} Item

Вспомним код, формирующий ответ на POST-запрос, вот его финальная часть:

Код
Код

Суть в чём: на запрос POST по REST нужно возвращать идентификатор созданного ресурса при успешном выполнении запроса, а не информацию о созданном товаре - как указано на текущий момент в спецификации API.

В моём случае, идентификатор - это имя созданного файла, а по спецификации API мы возвращаем в теле ответа тело запроса. Некорректно.

Что здесь можно сделать: написать что-то такое:

// @Success 201 {object} string

Чем нас не устраивает такая аннотация? В ней нет примера возвращаемого ресурса. Я так и не нашёл способа создать пример, кроме как добавить тег example к json-объекту. Поэтому я пошёл путём создания новой структуры с соответствующим тегом и возвращением её:

Код
Код

И текст аннотаций:

Аннотации в коде
Аннотации в коде

После этих изменений, сгенерируем документацию API и посмотрим, как она выглядит:

Визуализация документации API
Визуализация документации API

Меня это устраивает. Идём дальше.

3.2.3.5. Коды ошибок

Добавим теги для ошибок. По аналогии с предыдущим, успешным ответом, можно предположить что-то такое:

// @Success 400 {object} string

ну и указать комментарий

// @Success 400 {object} string "Ошибка валидации данных"

Как это будет выглядеть:

Визуализация документации API
Визуализация документации API

Мне не нравится в поле Example Value тип "string". Как его убрать оттуда? Можем вместо типа данных указать nil, и всё получится:

// @Failure 400 {object} nil "Ошибка валидации данных"

Напишем все аннотации на ошибки, возвращаемые нашим обработчиком:

Аннотации в коде
Аннотации в коде

Генерируем спецификацию, проверяем в визуальном редакторе:

Визуализация документации API
Визуализация документации API

Меня такая документация устраивает.

Мы написали расширенную документацию на обработчик POST-запроса. Если попрактиковаться - это просто, а главное - наглядно для других специалистов.

Напишем в таком же духе документацию на остальные обработчики.

Крайне рекомендую своими руками также написать документацию на каждый обработчик, чтобы научиться инструментам Swagger.

3.3. Документация на обработчик DELETE-запроса

Аннотации выглядят так:

Swagger-аннотации к коду
Swagger-аннотации к коду

Что здесь интересного: добавлен параметризованную часть эндпоинта {id}, и убрал аннотацию @Accept, т.к. клиент не передаёт информации в теле запроса.

Выглядеть это будет так:

Визуализация спецификации API
Визуализация спецификации API

Кстати, в предыдущем пункте не указал - можно "поиграть" и сделать пробный запрос, нажав кнопку Try it out:

Работа в Swagger Editor
Работа в Swagger Editor

При заполнении требуемых API полей (в данном случае заполнил только поле id - указал фрукт kiwi), Swagger Editor сформировал запрос.

Пока писал аннотации, понял что обработчики написаны с ошибкой в части типа возвращаемого клиенту контента. Возьмём сперва предыдущий обработчик - для POST-запроса, вот его код:

Код обработчика POST-запросов
Код обработчика POST-запросов

В строке 34 мы указали тип возвращаемых значений application/json, а сами в ошибках не возвращаем то возвращаем строку, т.е. тип text/plain, то не возвращаем ничего (кроме кода статуса). Это некорректно, и если мы хотим возвращать текст ошибки, то нужно преобразовывать их в json-объект, а не надеяться на продвинутый клиент, который может обработать контент несоответствующего типа согласно переданному сервером заголовку.

Вернёмся к обработчику DELETE-запросов, вот его код:

Код обработчика DELETE-запросов
Код обработчика DELETE-запросов

Здесь мы указываем тип возвращаемого контента клиенту, как простой текст, т.е. text/plain. В действительности, мы возвращаем текст, только для ошибок, а при успешном выполнении кода - ничего не возвращаем (кроме кода статуса). Ошибка ли это? Нет, это не ошибка.

3.4. Документация на обработчик PATCH-запроса

Swagger-аннотации к обработчику выглядят так:

Аннотации к коду
Аннотации к коду

Чтобы добавить пример заполнения тела запроса (см. стр. 154 на иллюстрации), создал структуру:

Код
Код

Что здесь ещё интересного: когда писал аннотации, обратил внимание, что обработчик не возвращает ошибку 500, хотя она возможна. В спецификации я прописал это, а в коде пока такой реализации нет.

Вот так, полезно писать документацию, чтобы выявить ошибки. Хотя на это есть тесты, и их тоже нужно писать, и выявленное отсутствие обработки ошибки сервера - это одна из причин, почему тесты обязательны.

Сгенерируем спецификацию, посмотрим на неё в Swagger Editor:

Первая часть визуализации спецификации API
Первая часть визуализации спецификации API
Вторая часть визуализации спецификации API
Вторая часть визуализации спецификации API

Документация меня устраивает, идём к следующему обработчику.

3.5. Документация на обработчик PUT-запроса

Напишем swagger-аннотации:

Аннотации к коду
Аннотации к коду

Что здесь интересного понял, когда заполнял аннотации: логика обработки POST-запроса такова, что мы создаём id с привязкой к наименованию ресурса, т.е. наименованию товара. Это не лучшая практика, и вот почему:

При смене наименования товара, у нас фактически изменился id ресурса, и по идее нужно в ответ PUT-запросу возвращать обновлённый id. Но у нас не создавался новый ресурс, и это не соответствует принципам REST.

Короче, по-хорошему нужно изменить логику. Например, для идентификатора ресурса использовать не наименование товара, а хеш, который будет формироваться так: наименование товара + время получения запроса. Так мы получим практически уникальное наименование файла, в котором храним цену.

Ладно, возвращаемся к спецификации. Сгенерируем её и посмотрим:

Первая часть визуализации спецификации API
Первая часть визуализации спецификации API
Вторая часть визуализации спецификации API
Вторая часть визуализации спецификации API

Всё в порядке, переходим к следующему обработчику.

3.6. Документация на обработчик GET-запроса

Напишем swagger-аннотации:

Аннотации к коду
Аннотации к коду

Смотрим, как выглядит визуализация в Swagger Editor:

Первая часть визуализации
Первая часть визуализации
Вторая часть визуализации
Вторая часть визуализации

Что ж, документация на все обработчики готова. Теперь я хочу обновить swagger-аннотации к общему описанию проекта: затронуть больше доступных тегов.

3.6. Расширяем общее описание проекта

Сейчас, с документированием обработчиков, спецификация API в Swagger Editor выглядит так:

Визуализация спецификации
Визуализация спецификации

Аннотации к общему описанию сейчас выглядят так:

Аннотации к коду
Аннотации к коду

Здесь указаны 8 swagger-аннотаций. В ReadMe библиотеки Swaggo для общего описания проекта предусмотрено 22 аннотации. Используем этот функционал по-максимуму и напишем аннотации:

Аннотации к коду
Аннотации к коду

Визуализируем спецификацию:

Визуализация спецификации API
Визуализация спецификации API

Все эти поля определены в ReadMe библиотеки Swaggo, продублирую свою интерпретацию из ReadMe, а после перечня дополню своими соображениями по сложным аннотациям. Также в тексте перечня ниже в скобках фразой "был ранее" обозначено, что эти аннотации были в первой версии описания проекта:

  1. @title: название API или проекта (был ранее).
  2. @version: версия API или проекта (был ранее).
  3. @description: описание API, объясняющее назначение и функционал (был ранее).
  4. @termsOfService: URL для условий предоставления услуг. Это может быть юридическая информация о том, как и когда можно использовать API, ограничения ответственного использования и тому подобное.
  5. @contact.name: имя контактного лица или разработчика (был ранее).
  6. @contact.url: URL для обратной связи, например, GitHub, (был ранее).
  7. @contact.email: Email-адрес для связи (был ранее).
  8. @license.name: название лицензии, под которой выпущен API (например, MIT). Лицензия определяет, как другие могут использовать код, что важно для Open Scource проектов.
  9. @license.url: URL, по которому можно найти текст лицензии.
  10. @host: хост, на котором развернут API (был ранее).
  11. @BasePath: базовый путь для API, обычно используется для определения начального пути для эндпоинтов (был ранее).
  12. @accept: перечень форматов, которые API принимает (например, json, xml, plain) - обратите внимание, перечисление форматов в аннотации через пробел.
  13. @produce: перечень форматов, которые API возвращает.
  14. @query.collection.format: формат, используемый для определения типа массива данных в запросах, если предусмотрено API (например, multi для передачи нескольких значений) - см. скриншот из ReadMe библиотеки Swaggo и дополнительные разъяснения ниже.
  15. @schemes: протоколы, поддерживаемые вашим API (например, http, https) - обратите внимание, в аннотации перечисление протоколов (если оно есть) выполняется через пробел.
  16. @externalDocs.description: описание внешней документации, связанной с API - например, чтобы дополнительно прояснить некоторые моменты. Эта строка используется, когда есть, например, специальные руководства по работе с API.
  17. @externalDocs.url: URL для внешней документации.
  18. @x-name: пользовательское расширение для документации, может быть использовано для добавления специфической информации. Эта аннотация предназначена для случаев, когда нужно добавить специфическую информацию, которая не вписывается в стандартные аннотации. Она может быть полезна для разработчиков API, чтобы включить метаданные или специальные расширенные функции.
  19. @tag.name: название тега для группировки эндпоинтов (например, items).
  20. @tag.description: описание тега.
  21. @tag.docs.url: URL для документации, связанной с тегом.
  22. @tag.docs.description: описание документации по тегу.

Теперь разберём спорные, или просто, вызывающие сложности, аннотации.

@termsOfService и @license.name/@license.url - не разобрался, чем они отличаются. Вероятно, если у вас какая-то нестандартная лицензия - нужно указать url для её чтения в аннотации @termsOfService.

@query.collection.format - эта аннотация управляет тем, как API ожидает получение массивов параметров в запросах - если ожидает, как я понимаю. Вот что говорится об этом в ReadMe библиотеки Swaggo:

Фрагмент ReadMe библиотеки Swaggo
Фрагмент ReadMe библиотеки Swaggo

Разберём форматы на примерах:

csv (Comma-Separated Values): использует запятые для разделения значений.Пример: GET /items?ids=1,2,3
ssv (Space-Separated Values): использует пробелы для разделения значений.Пример: GET /items?ids=1 2 3
tsv (Tab-Separated Values): использует табуляции для разделения значений.Пример: GET /items?ids=1\t2\t3
pipes: использует символы "вертикальная черта" для разделения значений.Пример: GET /items?ids=1|2|3
multi: позволяет отправлять одно и то же имя параметра множество раз, в котором каждый экземпляр параметра представлен отдельно.Пример: GET /items?id=1&id=2&id=3

У меня в API нет поддержания массива в запросах. А теперь, когда посмотрели примеры форматов, если ранее с ними знакомы не были - теперь становится понятно, что за массив данных в запросах определяет аннотация @query.collection.format.

Аннотация @x-name - это документирование API не по стандарту OpenAPI, и визуализироваться в Swagger Editor она не будет. Она может быть полезна, если нужно передать дополнительные данные, которые затруднительно выразить стандартными аннотациями OpenAPI. Например, у себя я ввёл такую запись:

// @x-name {"environment": "production", "version": "1.0.0", "team": "backend"}

Суть её в чём - данные передаются как json-объект, а значение полей я предполагаю такое:

  • environment: указывает, в каком окружении работает API (например, production или development).
  • version: версия API, которая может быть полезна для отслеживания изменений в зависимости от окружения.
  • team: название команды, которая отвечает за разработку данного API.

Разобрались со всеми аннотациями для общего описания. Я, пожалуй, уберу из своей спецификации несколько аннотаций, которые не актуальны для моего API и могут сбивать с толку:

  • @query.collection.format, т.к. у меня API не поддерживает массив запросов;
  • @termsOfService - у меня нет документации, определяющей особые условия использования API, неоговоренные стандартной лицензией.

Выглядит теперь документация так:

Визуализация API
Визуализация API

На этом основной разбор проектирования документации на API по стандарту OpenAPI закончен. Далее посмотрим, что ещё интересного можно добавить в разработку документации API.

3.7. Интеграция Swagger UI

Мы можем подключить Swagger UI на нашем сервере, чтобы пользователи могли сразу видеть интерактивную документацию API из браузера, при обращении к серверу, а не копировать спецификацию API на сайт Swagger Edotor.

Воспользуемся сторонней библиотекой http-swagger - это обёртка стандартного пакета net/http для автоматической генерации API:

Пакет http-swagger: https://pkg.go.dev/github.com/swaggo/http-swagger@v1.3.4#section-readme
Пакет http-swagger: https://pkg.go.dev/github.com/swaggo/http-swagger@v1.3.4#section-readme

Допишем обработчик в функцию main:

Код
Код
Код
Код

Теперь, когда мы запустим сервер и в браузере перейдём по url

http://localhost:8080/swagger/

мы вызываем функцию http.Handle("/swagger/", ...), которая использует обработчик WrapHandler пакета http-swagger (httpSwagger).

Swagger UI будет автоматически загружать необходимые HTML/CSS/JS-файлы для отображения интерфейса в виде веб-страницы в браузере.

Посмотрим, что находится в функции WrapHandler, а конкретно я хочу показать код, формирующий фронтенд:

Начало описания функции WrapHanler сторонней библиотеки http-swagger
Начало описания функции WrapHanler сторонней библиотеки http-swagger
Окончание описания функции WrapHandler
Окончание описания функции WrapHandler

Как видим - в функции сохранён фронтенд, написанный на языке разметки html.

Хорошо, немного посмотрели, что под капотом этой интересной функции, идём дальше.

По-умолчанию Swagger UI требует спецификацию в формате json, поэтому мы также прописываем обработчик с эндпоинтом .../doc.json и используем специальную функцию для возвращения клиенту одного файла - ServeFile. Возвращаем мы .json-файл спецификации API по стандарту OpenAPI, сгенерированного автоматически на предыдущих шагах командой

swag init

Когда мы переходим на

http://localhost:8080/swagger/

Swagger UI автоматически делает запрос к обработчику эндпоинта

/swagger/doc.json

(это поведение Swagger UI по умолчанию). Это происходит потому, что в конфигурации Swagger UI задается URL спецификации, который по умолчанию указывает на doc.json. Почему я об этом подробно говорю? Т.к. по-умолчанию, команда swag init генерирует три файла:

Генерируемые файлы командой swag init
Генерируемые файлы командой swag init

Ладно, запустили сервер, перешли в браузер и ввели команду http://localhost:8080/swagger/ - смотрим, что получилось:

Визуализация спецификации API
Визуализация спецификации API

Успех! Мы смогли настроить наш сервер так, чтобы он обрабатывал входящие запросы и через интерфейс Swagger UI, визуализировал нашу спецификацию в виде веб-страницы, как если бы мы копировали её вручную из файла и вставляли в соответствующее поле на сайте Swagger Editor. Удобно.

На этом разбор автоматической генерации спецификации по стандарту OpenAPI считаю законченной с уточнением по безопасности.

В ReadMe библиотеки Swaggo есть блок, посвящённый безопасности:

Фрагмент ReadMe библиотеки Swaggo
Фрагмент ReadMe библиотеки Swaggo

Безопасность осуществляется через аутентификацию и/или авторизацию пользователя. Согласитесь - это полезная штука: без такой фичи любой пользователь может перейти по url и посмотреть документацию на наш сервер, это как если любой желающий может посмотреть паспорт или другие личные документы: опасно.

Но аутентификацию и авторизацию я пока не изучал, не представляю какие есть способы для этого. Оставим это на домашнее задание.

Код приложения на GitHub >>> здесь <<<

4. Дополнения

В ходе написания публикации, я выявил недочёты и опечатки в аннотациях, которые по-хорошему нужно доработать в сервисе; в целом они не влияют на порядок действий для автоматической генерации спецификации API, поэтому в этом проекте код переделывать не буду, а в следующих учту недочёты, и сделаю сервисы лучше.

Получил обратную связь от опытного разработчика. Информация следующая:

1. Не нужно использовать snake_case в ручках, только kebab-case. Т.е. вместо

/home/create_item

нужно писать

/home/create-item

2. Ручки в ответ должны отдавать структуру, лучше json вида: OKResponse {status: ok, body: data} или ErrorResponse{ code: 500, message: error}, например:

Пример возвращения ошибки
Пример возвращения ошибки

Т.е. не просто статус кода в случае ошибки отдавать в ответе, а дополнительно json-объект. Может возникнуть вопрос: зачем это дублирование статуса. Возможный ответ:

Статус, устанавливаемый в заголовке при ответе сервера и статус json ответа, который будут ждать фронты и другие потребители ручек, будут благодарны, если будешь отдавать стандартизированный json ответ, если речь идёт об OpenAPI и сгенерированные на его основе методы и структуры данных.

5. Выводы

Идея публикации - научиться применять автоматическую генерацию спецификации API по стандарту OpenAPI, - выполнена. Значит ли это, что мы полностью освоили библиотеку Swaggo, даже со скидкой на отсутствие безопасности? Нет. Значит ли это, что мы освоили базовый функционал и часть ситуативного функционала библиотеки Swaggo для генерации спецификации API? Да.

Также мы в процессе разработки сервера коснулись некоторых основ REST API. Постепенно наш уровень растёт, осваиваются новые инструменты, и мы развиваемся, как разработчики.

Благодарю, что дочитали эту публикацию до конца. Продолжаем развиваться технически, улучшать свой характер и у нас всё получится. Будем на связи. Успехов!

Бро, ты уже здесь? 👉 Подпишись на канал для начинающих IT-специалистов «Войти в IT» в Telegram, будем изучать IT вместе 👨‍💻👩‍💻👨‍💻