Pull to refresh

Уродливый API

Reading time 4 min
Views 12K

Вступление

В этой статье хочу рассказать о проблемах, с которыми столкнулся в процессе интеграции с 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.

Тут мне снова пришлось внести ещё больше кода, чтобы обойти эту проблему с типом ответа сервера:

  1. Добавил новую версию запроса в Retrofit, где во второй версии я ожидаю тип “строка”

  1. Далее начал вызывать новую версию запроса, которая возвращает строку, и добавил преобразование строки в объект. Между тем, это преобразование 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!

Tags:
Hubs:
+5
Comments 22
Comments Comments 22

Articles