Вступление
В этой статье хочу рассказать о проблемах, с которыми столкнулся в процессе интеграции с API по HTTP протоколу, и поделиться опытом их решения.
При разработке фронтенд приложений (Mobile/Web), часто сталкиваешься с тем, что API на бэкенде еще не реализован. Приходится ждать разработчика на бэкенде, когда он предоставит нужные запросы, постоянно напоминая ему о себе. Не редкость и другая ситуация, когда нужные http запросы уже есть, но они реализованы очень плохо и криво.
Возможно, я бы и не писал эту статью. Но мне показался поразительным тот факт, что все приведенные ниже примеры плохой реализации API попались мне в одном-единственном проекте, одновременно!
В этом проекте я разрабатываю мобильное приложение на Flutter, используя пакет Retrofit, который помогает мне сократить время и код, который приходится писать самому, генерируя значительный код автоматически. Так же использую Insomnia, для первоначальной проверки запросов до реализации их в коде.
Прежде чем я приступлю к проблемам, я хотел бы отметить, что до того, как я стал мобильным разработчиком, я работал разработчиком Full-Stack более 5 лет. И я понимаю, насколько важно реализовывать красивый API для frontend, с которым легко и приятно интегрироваться и безопасно, в случае будущих изменений в API.
Видео версия статьи
Итак, начнем.
Не-RESTful архитектура
Первым огорчением стал тот факт, что API архитектура реализована не в стиле RESTful. Честно сказать, я и не помню, когда приходилось сталкиваться с интеграцией таких не-RESTful APIs.
REST означает REpresentational State Transfer. RESTful — вид реализации архитектуры API, которая наилучшим образом позволяет использовать протокол HTTP. С REST нам нужно думать о приложении с точки зрения ресурсов. Определить, какие ресурсы мы хотим открыть для внешнего мира (например, tasks, customers, etc.). Используем глаголы, определенные протоколом HTTP, для выполнения CRUD операций с этими ресурсами, т.к. GET, POST, PUT, DELETE.
Пример RESTful API:
Архитектура API моего проекта — не REST архитектура — выглядит следующим образом:
Можно заметить, что все запросы имеют один тип, это POST запросы. И каждый запрос должен содержать параметр type, который определяет операцию. И, видимо, на сервере, в этом одном-единственном endpoint’е имеется какой-то if-else или switch операторы на проверку этого type параметра.
Я, конечно, предпочитаю RESTful, но раз уж другая реализация нормально работает, и на клиенте можно более-менее настроить запросы с Retrofit, я принял этот факт и продолжил интегрироваться с API
Header Accept: application/json
Обычно сначала я оформляю запросы на сервер в Insomnia, проверяю их, и после реализую их в коде.
В Insomnia, когда я выполнил запрос, во вкладке Preview увидел красивый json и подумал, что ко мне приходит json объект. Я оформил все это в коде, Retrofit мне конвертирует ответ запроса в Dart объект автоматически, и все замечательно сработало.
Но на следующий день моё приложение перестало работать из-за ошибки, связанной с запросом к серверу. Текст ошибки был: “не получается преобразовать строку в объект”.
Я снова вернулся к Insomnia и проверил запрос. В Preview я увидел тот же json, что и раньше. И только после проверки Header запроса я обнаружил, что Content-Type изменился, и значение его уже text/html, charset-utf-8, хотя Preview показывает мне json.
Таким образом, я определил, что тип ответа от сервера изменился с application/json на text/html, из-за чего Retrofit уже не может преобразовать ответ от сервера типа строки автоматически в Dart объект.
Я решил попробовать использовать Accept Header в запросе, который скажет серверу, что “я - клиент ожидаю от тебя ответ в формате json”. Но это не сработало, т.к. сервер не берет в расчет этот Header Accept.
Тут мне снова пришлось внести ещё больше кода, чтобы обойти эту проблему с типом ответа сервера:
Добавил новую версию запроса в Retrofit, где во второй версии я ожидаю тип “строка”
Далее начал вызывать новую версию запроса, которая возвращает строку, и добавил преобразование строки в объект. Между тем, это преобразование Retrofit мог бы произвести самостоятельно в своем сгенерированном коде. А сейчас в каждом месте, где мы выполняем запросы на сервер, получая ответы, мы должны добавлять это преобразование сами:
В итоге, добавилось ещё больше строк кода, что никак не радует.
JSON keys case types
Бывает несколько видов оформления названий полей в json:
В идеале нужно выбрать один понравившийся вариант, и придерживаться его во всём проекте и во всех запросах. Но в моём проекте бэкенд разработчик решил использовать несколько видов:
В Dart в идеале для именования полей класса и методов используется camelCase стиль. И если от сервера приходит json с полями в camelCase стиле, то Retrofit автоматически, без усилий с нашей стороны, правильно сопоставит поля класса и json. Но в данной ситуации мне приходится указывать дополнительные аннотации JsonKey, которые в одном случае как snake_case, а в другом — UPPER_CASE_SNAKE_CASE:
В итоге — нет единообразия, если ошибёшься в case type, то можешь потерять значение, т.к. поля json в Dart класса не сопоставятся. Я разозлился еще больше на API, но интеграцию с API всё же можно было продолжать, затрачивая больше усилий и ещё больше кода.
Различный ответ на один и тот же запрос
Когда я попробовал выполнить вход в приложение под другим пользователем, моё приложение снова сломалось. Я получил сообщение об ошибке типа “Невозможно преобразовать строку в число”. Это было связано с тем, что мой класс, описывающий ответ от сервера, стал неверным. Типы, описанные в классе, не соответствуют типам полей, имеющимся в json ответе. Я снова открыл Insomnia, и выполнил один и тот же запрос входа для двух разных пользователей. И обнаружил, что в одном случае числа приходят в ответе в виде строк, а в другом — в виде чисел. В Insomnia можно заметить, что строки выделены желтым цветом, а числа — фиолетовым:
Как вообще может быть, что на один и тот же запрос приходит один и тот же json, но с разными типами значений? Неужели на бэкенде стоит условие на пользователя и нужно возвращать разный json? Зачем? Как?
Я знаю, что сервер написан на PHP. Господа, кто программирует на PHP, могли бы вы написать в комментариях, как такое возможно?
Заключение
Сейчас я испытываю противоположные чувства. С одной стороны, я расстроен, что приходится интегрировать такой уродливый API, с другой — уже жду с нетерпением следующий кейс, который покажет, как же ещё можно изуродовать API.
Хотелось бы узнать о вашем опыте интеграции с API, и с какими проблемами вы сталкивались.
Спасибо. Happy coding!