Сюрпризи і пастки JSON API, GraphQL і gRPC: як зробити правильний вибір
Привіт, мене звати Андрій Мойсол. Я iOS Developer у стартапі AlphaNovel від венчур-білдера SKELAR. Ми створюємо маркетплейс романтичних новел і коміксів для читачів і письменників з усього світу.
Сьогодні хочу поговорити про найпоширеніші технології взаємодії між клієнтом і сервером, а саме: JSON API, GraphQL і gRPC. Розберу, в чому плюси і мінуси кожного метода, які підводні камені чекають під час реалізації та чи варто переходити з одного на інший в проєкті.
У цій статті більшість прикладів будуть на Swift, як мови для розробки під iOS, та на Golang, щоб продемонструвати сторону бекенда. Та не лякайтесь, якщо не знаєте жодну з мов, приклади будуть достатньо прості для розуміння і можуть бути застосовані на будь-якій мові та для будь-якої платформи.
Як працює HTTP
Для початку треба зрозуміти, як узагалі дані передаються від клієнта до сервера. Найпоширенішим протоколом прикладного рівня є HTTP (Hypertext Transfer Protocol), абсолютна більшість вебсайтів та мобільних застосунків використовують його для обміну даними з сервером. За допомогою нього можна передавати будь-яку текстову інформацію та бінарні дані.
Зазвичай клієнт і сервер — це просто два комп’ютери, які мають публічну IP-адресу. Через протокол транспортного рівня TCP (Transmission Control Protocol) клієнт відправляє серверу request, який реалізує протокол HTTP. Сервер обробляє відправлену інформацію і відправляє клієнту назад response, також під протоколом HTTP.
Базова структура HTTP-запиту складається з status line, headers та body.
Перший рядок описує метод запиту (GET, POST, PUT, DELETE тощо), URL та версію HTTP. На сьогоднішній день найпоширенішими версіями є HTTP 1.1 та HTTP 2.0. Майже всі браузери на пристрої підтримують новішу версію, та через деякі сервіси все ще використовується HTTP 1.1.
Далі йдуть headers — параметри запиту, найважливіші для нашої теми це Content-Type, Content-Length. Вони описують розмір і тип body — найважливішої частини запита.
Body — це місце де записується основні дані запита. Хоча для деяких HTTP-методів, наприклад, GET, він не використовується (замість цього використовуються Query Parameters), більшість мобільних застосунків послуговуються ним із HTTP-методом POST, щоб відправити дані на сервер.
Сервер обробляє запит, розуміючи з хедеру Content-Type, який тип body йому прийшов, і відправляє схожий за структурою HTTP response.
Відмінність від request у першому рядку, тут він відправляє версію HTTP та статус-код, який свідчить, чи відбулася помилка, чи все відбулося успішно. По хедерам Content-Type та Content-Length клієнт може розпарсити body, якщо воно є, і отримати дані від сервера.
Ось так ми розібрали, як спілкуються клієнт і сервер у форматі request-response. Зазначу, що можна зробити upgrade
HTTP-запита до Websocket API — ще один тип з’єднання, у якому сервер може без ініціативи клієнта відправляти йому повідомлення, але у цій статті розбирати realtime-спілкування ми не будемо.
Усі три методи спілкування, які ми розбираємо, так чи інакше будуть працювати на протоколі HTTP, тому базові знання HTTP необхідні для подальшого розуміння.
JSON API
Зараз розберемо, мабуть, найбільш популярний метод серед Mobile-розробників — JSON API. Під цим поняттям я маю на увазі звичайний HTTP-запит, body якого є текст в форматі JSON.
Front-end
Усе, що треба для сетапу цього методу спілкування: будемо в хедері Content-Type відправляти application / json, метод HTTP на POST і body закодоване в JSON. Сервер організовує так звані endpoints, кожен з яких виконує свою функцію. Якщо сервер потребує авторизації клієнта, дуже часто використовують JWT-токени, які передають в хедер Authorization
.
Цей спосіб комунікації є найпростішим з точки зору клієнта, бо не потребує ніякого сетапу, усе це можна зробити, використовуючи структури та методи стандартної бібліотеки.
Усе, що нам потрібно, це:
- зробити структури request та response;
- закодувати request в JSON;
- створити HTTP-запит на конкретний endpoint та передати закодований JSON та необхідні хедери;
- у хендлері обробити транспортну помилку;
- обробити HTTP-помилку (
status code
); - розкодувати response в структуру і обробити його;
- відправити запит.
Як ми бачимо, рішення займає 60 строчок, якщо використовувати стандартну бібліотеку. Тому більшість використовує зручні бібліотеки-обгортки, як-от Alamofire та SwiftyJSON. Також можна помітити, що response-модель має поле для помилки. Цей механізм дозволяє бекенду чітко казати, в чому помилка, замість того, щоб використовувати HTTP status code
. Хоча дуже часто можна натрапити на комбінацію з status core
та модельки.
Використані бібліотеки
Built-in
|
Third party 🚫 |
Back-end
На стороні сервера JSON API також легко реалізується за допомогою стандартних бібліотек. Ми пишемо handler для якогось endpoint, в якому приймаємо request та ResponseWriter. Декодуємо дані з request та записуємо дані в ResponseWriter. Приклад простого endpoint можна побачити нижче.
- створюємо моделі request та response;
- перевіряємо HTTP-метод;
- декодуємо request, відправлений клієнтом;
- виконуємо дію з даними;
- кодуємо response та відправляємо назад клієнту;
- створюємо endpoint і назначаємо йому handler;
- запускаємо сервер.
На практиці використовують фреймворки та бібліотеки, як-от Gin та FastHttp.
Використані бібліотеки
Built-in
|
Third party 🚫 |
Документація
Ось і приходить час на дев-рефайменті писати API-контракт між клієнтом і бекендом. Зазвичай використовують або текстову документацію, або Swagger. Клієнт може в будь-який момент подивитись цю документацію, зрозуміти, які моделі request та response йому потрібно відправляти і на які endpoints — звучить легко і просто. Але на плечі бекенд-розробника лягає тягар у вигляді написання та підтримки API-документації.
Підсумок
Як ми бачимо, JSON API — просте і швидке рішення для початку написання нового проєкту. Та у нього є недолік у вигляді документації, оскільки бекенд-розробнику треба руками або в напівавтоматичному режимі підтримувати документацію.
Також можна помітити, що, як і клієнт, так і бекенд будуть мати в своїй кодовій базі дуже багато request та response структур, що може захламити проєкт, та призвести до того, що модельки будуть не синхронізовані, оскільки часто бекенд і клієнт пишеться на різних мовах і в різних кодових базах.
Ще одним мінусом є те, що якщо клієнту потрібно отримати частину даних, то потрібно або робити окремий endpoint, або використовувати наявний endpoint, який повертає повну модель даних, що не є оптимальним.
Обидві з цих проблем вирішує GraphQL, до якого ми зараз і переходимо.
GraphQL
GraphQL — це мова запитів для API, розроблена Facebook. Вона дозволяє клієнтам визначати точну структуру запитаних даних, що робить можливим отримання всіх необхідних даних одним запитом.
Головна проблема, яка була у JSON API — окремі моделі request / response для клієнта і бекенда.
На відміну від традиційних REST API, де структура відповіді визначається сервером, GraphQL надає клієнтам повний контроль над запитуваною інформацією, зокрема вибір полів та вкладені запити. Бекенд і фронтенд мають одну базу опису API на одній мові. Це дозволяє дуже швидко синхронізувати моделі даних та не робити одну й ту саму роботу двічі.
Приклад моделей на GraphQL:
GraphQL поділяє запити на Query та Mutation. Відповідно запити в Query віддають дані без змін в стейті бекенда, а запити в Mutation навпаки змінюють стейт.
Усі запити можуть приймати параметри. Щоб зробити параметр обов’язковим, можна після типа вказати !
. Також є enums та union types, що дозволяє дуже легко робити інтерфейси з різними видами UI.
Під капотом GraphQL працює на HTTP, але завжди використовує POST-запити. Тому GraphQL працює всюди, де працює звичайне JSON API.
Оʼкей, тепер ми пишемо наше API на одній мові — GraphQL, але як же працювати із цим? Для цього є багато бібліотек як для бекенда, так і для фронтенда. Почнемо з першого.
Back-end
Використовувати будемо бібліотеку gqlgen
. Запишемо наше GraphQL API в окремий файл та за допомогою команди generate
згенеруємо весь код за нас 🙂. Створються чотири нові файли:
models_gen.go
— тут згенерувались усі модельки, потрібні нам для роботи.
generated.go
— тут генерується допоміжний код, на практиці цей файл завжди ігнорується.
resolver.go
— файл, у якому всього одна структура Resolver. Тут ми маємо можливість зробити DI, оскільки інстанс Resolver буде доступним в імплементації запитів.
Додамо для прикладу стейт — массив коментарів.
І останній файл schema.resolvers.go
— тут ми й будемо імплементувати наші запити. Тут створюються resolvers
для Query
та Mutation
. Імплементуємо CreateComment
та Comments
.
Усе, що залишилось, це запустити сервер з нашим Resolver
.
У цьому прикладі сервер буде доступний на порту 8080 під шляхом /query
. Та ще буде запущений GraphQL Playground під шляхом /
.
Playground — дуже крутий бонус до написання API, так як нам тепер не треба писати Swagger. GraphQL на основі схеми сам за нас зробить красивий UI і документацію.
Перейшовши на localhost:8080
, ми можемо побачити всю структуру API та навіть протестувати її прямо в браузері (тепер і Postman не потрібен 🙂)
Можна побачити, що під час виклику мутації createComment
ми у відповідь отримаємо модель коментаря, але ми перераховуємо всі поля цієї моделі. Навіщо?
Ось тут ми і переходимо до головної фішки GraphQL — можливість обирати поля, які нам потрібні саме в конкретному контексті. Тому ми плавно переходимо до клієнта.
Використані бібліотеки
Built-in
|
Third party
|
Front-end
GraphQL API під капотом також працює через HTTP та JSON в якості формату даних. Тому теоретично можна використовувати такі самі підходи для відправлення запитів на клієнті. Але є багато бібліотек, які спрощують відправлення запитів і, найголовніше, генерують моделі на основі моделей GraphQL. Поглянемо, як це виглядає на прикладі бібліотеки Apollo GraphQL. Вона пропонує генерацію кода, кешування запитів та ярусну структуру запитів, що дозволяє нам робити middlewares, наприклад, для авторизації.
Додаємо бібліотеку в проєкт, запускаємо ініціалізацію конфігураційного файла через CLI. Додаємо в проєкт файл зі схемою GraphQL.
Далі в окремих файлах .graphql
треба описати операції, які буде використовувати клієнт у конкретному контексті.
Як ми бачимо, запити можуть приймати динамічні параметри. Також можна створювати фрагменти — зріз полів оригінальної моделі, таким способом можна перевикористовувати одні й ті самі фрагменти в багатьох запитах.
Далі запускаємо команду generate
через CLI. Генерується локальна бібліотека з кодом запитів і моделями.
Приклад згенерованого фрагменту:
Далі все дуже просто, створюємо інстанс клієнта та викликаємо запити.
Використані бібліотеки
Built-in
|
Third party
|
Підсумок
GraphQL вирішує обидві проблеми з JSON API. У нас є можливість мати на клієнті й на бекенді єдиний source of truth для опису API, а також ми можемо, залежно від контексту, запитувати тільки ту інформацію, яка нам потрібна, без зміни бекенда. Також не забуваємо, що GraphQL схема — strongly typed, що унеможливлює неправильний request чи парсинг response.
Ще великим плюсом GraphQL є subscriptions — вони працюють на WebSockets та також використовують загальну схему.
Що в мінусах? З мого досвіду, GraphQL — одне з найкращих рішень для створення API, однак треба бути обережним. Наприклад, дуже часто можна переборщити з кількістю фрагментів на стороні клієнта, і в коді буде дуже багато згенерованих моделей для багатьох різних контекстів, що створює проблему для сприйняття. Тому дуже важливо використовувати мінімальну кількість фрагментів.
Ще одним мінусом GraphQL та JSON API можна вважати розмір request та response. Якщо ваш проєкт повинен працювати швидко і не може собі дозволити великий трафік, то варто подивитись в сторону gRPC.
gRPC
gRPC — реалізація Remote Procedure Call від Google. Це спосіб передачі даних у бінарному вигляді за допомогою Protocol Buffers. Працює на HTTP 2.0, у якому хедери займають менше місця.
Також підтримує Bi-directional streaming з коробки.
Protocol Buffers — зручний формат для опису API-контракта, також однаковий для клієнта та сервера, що дуже схоже на GraphQL-схему. Але немає вибору полів клієнтом, як це є в GraphQL.
Найчастіше gRPC використовується разом з HTTP/3, тому клієнти повинні його підтримувати. Деякі старі браузери та версії OS не мають цієї підтримки, та працюють з HTTP/2, і навіть з HTTP/1. Якщо треба підтримувати ці старі клієнти, то використовують gRPC-Web, який трансформує старі формати в HTTP/3.
Подивимось на приклад реалізації бекенда і фронтенда.
Back-end
Створюємо в проєкті .proto
-файл та описуємо сервіс коментарів. Структури називаються message
, а кожне поле повинно мати свій індекс. Сам сервіс описується в блоці service
, а запити ключовим сломов rpc
.
Після цього, використовуючи protoc
, генеруємо із цього файлу код моделей та сервера.
Згенеруються моделі, які легко конвертуються у бінарний вигляд. Усе, що залишилось — створити сервер та імплементувати rpc-функції. Також будемо для прикладу збирати коментарі в масиві.
Як бачимо, структура і дії дуже схожі на GraphQL.
Використані бібліотеки
Built-in
|
Third party |
Front-end
Як можна здогадатись, на клієнті сетап буде схожим на GraphQL. Треба взяти proto
-файл схеми і через CLI protoc згенерувати моделі та структуру gRPC-клієнта.
Зауважу, що HTTP 2.0 наразі підтримується не всіма клієнтами на Web, тому для роботи з gRPC саме на Web треба використовувати проксі у вигляді gRPC Web, який трансформує HTTP 1.1 в HTTP 2.0. На мобільних девайсах все працює з коробки.
Отримаємо два файли — один з моделями, інший зі структурою клієнта. Приклад моделі:
Після цього створюємо з’єднання з gRPC-сервером і викликаємо процедуру.
Використані бібліотеки
Built-in
|
Third party
|
Підсумок
gRPC крута технологія, яка дуже часто використовується для взаємодії між мікросервісами на стороні бекенда, тому логічно було б використовувати його і для клієнтської взаємодії. Запити дуже швидкі, малі за розміром. Як і в GraphQL, тут є генерація коду з proto
-файлу, що забезпечує синхронізацію.
Мінусом gRPC є його тестування, якщо в GraphQL є зручний playground, де можна і запускати запити і дивитись документацію, то у gRPC такого зручного середовища немає.
Що обрати
Ось ми поверхово розібрали що являють собою три найпопулярніші методи комунікації у світі бекенда і фронтенда, але що ж обрати?
Звичайно ж, для кожного проєкту буде свій найкращий вибір, але я спробую дати невеличкий roadmap.
JSON API |
GraphQL |
gRPC | |
Легкий сетап |
🟢 |
🟡 |
🟡 |
Швидкодія |
🟡 |
🟡 |
🟢 |
Realtime комунікація з коробки |
🔴 |
🟢 |
🟢 |
Автоматична документація / sandbox |
🔴 |
🟢 |
🟡 |
Автоматичне генерування коду |
🔴 |
🟢 |
🟢 |
Синхронізація back-end- та front-end-моделей |
🔴 |
🟢 |
🟢 |
JSON API підійде в таких кейсах:
- MVP;
- маленьке API;
- немає необхідності у швидкодії та малому розмірі даних;
gRPC буде кращим варіантом, якщо:
- бекенд уже використовує gRPC у якості взаємодії між мікросервісами;
- середнє-велике API;
- є необхідність у швидкодії;
- ваша інфраструктура побудована на сервісах Google;
- realtime-спілкування;
- хочеться уникнути помилок і генерувати код.
GraphQL:
- середнє-велике API;
- хочеться легко і швидко тестувати бекенд через GraphQL Playground;
- є потреба запитувати тільки ті дані, які необхідні в цьому контексті;
- realtime спілкування;
- хочеться уникнути помилок і генерувати код;
Куди далі
Сподіваюсь, стаття дозволила вам краще зрозуміти, які технології в яких випадках кращі і з якими легше працювати. До речі, не забудьте поділитися в коментарях своїм досвідом роботи з цими методами комунікації.
54 коментарі
Додати коментар Підписатись на коментаріВідписатись від коментарів