Вступление
В этой статье хочу рассказать о проблемах, с которыми столкнулся в процессе интеграции с 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:
![](https://habrastorage.org/getpro/habr/upload_files/63f/a2a/b32/63fa2ab32c3e165d8e04d6b4a69b8a42.png)
Архитектура API моего проекта — не REST архитектура — выглядит следующим образом:
![](https://habrastorage.org/getpro/habr/upload_files/5ac/191/933/5ac1919330043f283a85c79e46e21b89.png)
Можно заметить, что все запросы имеют один тип, это 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.
![](https://habrastorage.org/getpro/habr/upload_files/5e6/621/755/5e6621755efc057b70ca2646a3804bd9.png)
Тут мне снова пришлось внести ещё больше кода, чтобы обойти эту проблему с типом ответа сервера:
Добавил новую версию запроса в Retrofit, где во второй версии я ожидаю тип “строка”
![](https://habrastorage.org/getpro/habr/upload_files/5d4/9d6/989/5d49d698956a0564ff133f9c6fc03712.png)
Далее начал вызывать новую версию запроса, которая возвращает строку, и добавил преобразование строки в объект. Между тем, это преобразование Retrofit мог бы произвести самостоятельно в своем сгенерированном коде. А сейчас в каждом месте, где мы выполняем запросы на сервер, получая ответы, мы должны добавлять это преобразование сами:
![](https://habrastorage.org/getpro/habr/upload_files/ecf/095/762/ecf0957623963768dca38e232f014fd2.png)
В итоге, добавилось ещё больше строк кода, что никак не радует.
JSON keys case types
Бывает несколько видов оформления названий полей в json:
![](https://habrastorage.org/getpro/habr/upload_files/7af/00f/45c/7af00f45cb405f051a16edd9fa25617f.png)
В идеале нужно выбрать один понравившийся вариант, и придерживаться его во всём проекте и во всех запросах. Но в моём проекте бэкенд разработчик решил использовать несколько видов:
![](https://habrastorage.org/getpro/habr/upload_files/7e5/fa5/7b2/7e5fa57b2c29a9aa9f16226069257909.png)
В Dart в идеале для именования полей класса и методов используется camelCase стиль. И если от сервера приходит json с полями в camelCase стиле, то Retrofit автоматически, без усилий с нашей стороны, правильно сопоставит поля класса и json. Но в данной ситуации мне приходится указывать дополнительные аннотации JsonKey, которые в одном случае как snake_case, а в другом — UPPER_CASE_SNAKE_CASE:
![](https://habrastorage.org/getpro/habr/upload_files/384/f6a/ef7/384f6aef7dfe65817f481facf41c3ad9.png)
В итоге — нет единообразия, если ошибёшься в case type, то можешь потерять значение, т.к. поля json в Dart класса не сопоставятся. Я разозлился еще больше на API, но интеграцию с API всё же можно было продолжать, затрачивая больше усилий и ещё больше кода.
Различный ответ на один и тот же запрос
Когда я попробовал выполнить вход в приложение под другим пользователем, моё приложение снова сломалось. Я получил сообщение об ошибке типа “Невозможно преобразовать строку в число”. Это было связано с тем, что мой класс, описывающий ответ от сервера, стал неверным. Типы, описанные в классе, не соответствуют типам полей, имеющимся в json ответе. Я снова открыл Insomnia, и выполнил один и тот же запрос входа для двух разных пользователей. И обнаружил, что в одном случае числа приходят в ответе в виде строк, а в другом — в виде чисел. В Insomnia можно заметить, что строки выделены желтым цветом, а числа — фиолетовым:
![](https://habrastorage.org/getpro/habr/upload_files/990/1e4/241/9901e4241ea86ef51bf78db8cc1405cb.png)
Как вообще может быть, что на один и тот же запрос приходит один и тот же json, но с разными типами значений? Неужели на бэкенде стоит условие на пользователя и нужно возвращать разный json? Зачем? Как?
![](https://habrastorage.org/getpro/habr/upload_files/837/54f/59a/83754f59a504a7eb27919f9cafda3764.png)
Я знаю, что сервер написан на PHP. Господа, кто программирует на PHP, могли бы вы написать в комментариях, как такое возможно?
Заключение
Сейчас я испытываю противоположные чувства. С одной стороны, я расстроен, что приходится интегрировать такой уродливый API, с другой — уже жду с нетерпением следующий кейс, который покажет, как же ещё можно изуродовать API.
Хотелось бы узнать о вашем опыте интеграции с API, и с какими проблемами вы сталкивались.
Спасибо. Happy coding!