Привет! Сегодня у нашей статьи два автора — бэкенд-разработчик Артём и фронтенд-разработчик Илья.
Примерно год назад мы решили попробовать ввести graphQL в свой проект и сейчас хотим поделиться, как это происходило. Расскажем, что такое GraphQL, как его внедряли, почему мы вообще решили с ним подружиться и как начать взаимодействовать с API бэкенда словно вы граф, а не холоп.
Если лень читать или больше нравится видеоформат — вам сюда.
Разбираемся на пальцах
Для начала давайте рассмотрим предметную область нашего приложения – это поможет понять, с какими проблемами мы столкнулись.
Мы разрабатываем отдельный от основного hh.ru продукт — внешнюю CRM-систему по подбору персонала Talantix. Если hh.ru, как и ряд схожих продуктов, это скорее job-борда, где кандидат создает резюме, а работодатель создает вакансию, на которую можно откликнуться, то после этого обычно в бой вступает CRM-система, где создаются интервью-встречи, кандидаты переводятся по кастомным этапам, строится аналитика работы рекрутеров и происходит прочий очень занимательный для hr флоу работы.
И чтобы подобраться к задачам, которые решает GraphQL, попробуем спроектировать API для нашей системы и заодно посмотрим, какие могут быть проблемы взаимодействия этого API с бэкендом.
Итак, мы зашли на сайт и собираемся создать встречу на интервью.
Помимо всего прочего нам понадобятся ФИО и контакты кандидатов. Сделаем стандартный по REST GET-метод /candidates
, который будет возвращать необходимые данные.
GET /candidates
[
{
"id": 1,
"firstName": "Леонид",
"lastName": "Якубович",
"email": "leonid@yakubovich.ru",
"phone": "+79672222222",
"resume_id": 500
},
{
"id": 2,
"firstName": "Лёня",
"lastName": "Агутин",
"email": "hophey@lalaley.com",
"phone": "+78005553535",
"resume_id": 501
}
]
Underfetching
Однако в нашей системе данные о кандидатах нужны не только на странице создания встречи. Ещё есть отдельная страница кандидатов, например, где помимо ФИО нам также понадобятся последний опыт работы и зарплата из резюме.
Окей, можем сделать ещё один GET-метод по REST, назовём его /resumes
и по id резюме получим необходимые данные.
GET /resumes?resumeId=500&resumeId=501
[
{
"resumeId": 500,
"lastExperience": {
"company": "Поле чудес",
"position": "Ведущий"
},
"salary": {
"currency": "RUB",
"position": "45000"
}
},
{
"resumeId": 501,
"lastExperience": {
"company": "Голос",
"position": "Ведущий"
},
"salary": {
"currency": "RUB",
"position": "50000"
}
}
]
Круто, но тогда появляется особенность, которая называется underfetching
— мы не можем получить все данные за один запрос и ходим на бэкенд ещё раз. И это действительно иногда может стать проблемой — мы делаем несколько сетевых походов для получения данных на одной странице, не у всех пользователей может быть стабильный интернет, мы теряем в производительности.
Overfetching
Ну что же, мы можем забить на REST, создать единый метод /candidates
, который будет возвращать все данные из сущностей кандидатов и резюме. Мы же знаем как и где они будут использоваться, так чего бы в один метод не напихать туда всё, что нам нужно?
GET /candidates
[
{
"id": 1,
"firstName": "Леонид",
"lastName": "Якубович",
"email": "leonid@yakubovich.ru",
"phone": "+79672222222",
"resume": {
"resumeId": 500,
"lastExperience": {
"company": "Поле чудес",
"position": "Ведущий"
},
"salary": {
"currency": "RUB",
"position": "45000"
}
}
},
{
"id": 2,
"firstName": "Лёня",
"lastName": "Агутин",
"email": "hophey@lalaley.com",
"phone": "+78005553535",
"resume": {
"resumeId": 501,
"lastExperience": {
"company": "Голос",
"position": "Ведущий"
},
"salary": {
"currency": "RUB",
"position": "50000"
}
}
}
]
Не всё так просто. При создании встречи нам не нужны данные о резюме. Эта проблема тоже имеет значение, и она называется overfetching
. Мы отдаём избыточные данные, которые никак не будут использоваться – снова теряем в производительности.
Завязка на страницы приложения
Можно, например, создать методы /candidates_min
и /candidates_max
. С точки зрения производительности всё ок, но не очень масштабируемо - а если добавится третий метод, как назовём?
Но у нас есть знание какие данные нужны конкретным страницам - можно им и воспользоваться.
Можем сделать отдельный слой на бэкенде, который будет ходить в разные микросервисы, аккумулировать данные и возвращать в соответствии с определенной страницей. Вариант хороший, но есть свои минусы. Например, на страницах с почти идентичными данными придётся дублировать логику с походами в другие сервисы и сбора информации. Да и если появится ещё одна страница со схожими данными придётся всё равно создавать новую страницу на бэкенде - занимает время разработки.
Клиент сам решает какие данные нужны
Есть ещё один вариант — оставить метод /candidates
и в query-параметрах передавать только те поля, которые нам нужны.
/candidates?fetchEmail=true&fetchPhone=true&fetchResumeSalary=true…
Однако и это решение нам не полностью подходит — таких полей могут быть сотни и тысячи - сложно разобраться, сложно контролировать, сложно поддерживать. И вот тут мы плавно подходим к самой сути GraphQL.
Да кто такой этот ваш GraphQL
GraphQL позволяет декларативно на клиенте описать, какие данные ему нужны, и бэкенд вернет только их. Это значит, что клиент сам говорит, какие данные он хочет получить. Например, как в sql-запросах, когда работаем с таблицами: перечисляем нужные поля и откуда их взять. А со стороны бэкенда — это составление некой схемы, которой будут следовать запросы с клиента (опять же, аналог составления таблицы в sql), и разработка неких распознавателей, которые будут понимать, куда ходить за данными и что с ними делать. То есть на каждое поле объекта можно написать свой метод распознавателя, в котором будет содержаться его бизнес-логика.
GraphQL часто путают с базой данных или SQL, хотя мы только что убедились, что это совсем другой уровень абстракции. Более того, у нас теперь будет только один endpoint — POST-метод, который займется обслуживанием вообще всех запросов GraphQL, в отличие от того же REST. А значит, GraphQL — это стандарт языка описания запросов от клиента к бэкенду.
Реализация
Опишем краткую схему для нашего случая и заодно поймем, где граф в GraphQL.
Схема описывается на специальном языке GraphQL-схемы, и он чем-то похож на JSON. Мы описали кандидата, резюме и соединили их вместе. А вот и Graph в GraphQL — мы объявили ноды графа и соединили их.
Однако войти в этот граф пока нельзя. Поэтому опишем входные точки. В данном случае будет одна точка — candidates, например, с фильтром по id.
candidates(id: 5) {
firstName,
lastName,
phone,
email,
resumes {
lastExperience {
company,
position
},
salary {
currency,
position
}
}
}
И вот почему GraphQL — это QL (query language): запрос очень похож на JSON, но на самом деле это не он. И после этого сервер возвращает нам нужные данные.
Сравним с REST
В REST:
N ресурсов;
Бэкенд решает, какие данные выдать;
Нет строго контракта, вернее, контракт на уровне согласования;
Обработка ошибок через коды ответа 4**-5**.
В GraphQL:
1 POST-ресурс;
Клиент решает, какие данные запросить;
Контракт диктуется схемой и имеет строгую типизацию;
Обработка ошибок через массив errors в теле ответа — это обусловлено тем, что graphQL не завязан на определённый протокол.
Таблица
REST | GraphQL |
N ресурсов | 1 POST-ресурс |
Бэкенд решает, какие данные выдать | Клиент решает, какие данные запросить |
Нет строгого контракта | Контракт диктуется схемой |
Обработка ошибок через коды ответа | Обработка ошибок через массив errors в теле ответа |
Из грязи в графы или как мы совершили переход
Итак, мы начали переходить на GraphQL. На самом деле стратегия была довольно простой — мы брали определённые страницы и переводили на новый формат. Поскольку мы сомневались, зайдёт ли нам GraphQL, политика была следующая: мы переводили высоконагруженные страницы со сложной логикой, чтобы сразу окунуться на всю катушку и понять, какие у нас могут быть проблемы и действительно ли помогает GraphQL.
В промежуточных стадиях у нас могут быть 2 версии однотипного кода (legacy и graphQL), пока полностью не переведём целую сущность — это нормально, если в коде сразу объявить deprecated legacy-методы и не забивать с переводом остальной функциональности. В hh.ru рекомендуется делать 30% техналога на команду и мы воспользовались этой возможностью. Также мы ставили себе технические цели в OKR, чтобы лучше мониторить прогресс.
Важное допущение — мы переводим пока что только GET-методы, хоть и graphQL позволяет изменять сущности. Это сделано только потому, что пока что перевод GET-методов для нас более актуален.
Плюсы
Мы сидим на GraphQL с осени прошлого года и продолжаем развивать этот протокол. Вот какие преимущества нам удалось обнаружить за это время:
Декларативное общение клиент-сервера и решение underfetching/overfetching проблем.
Единый API, который планируем расшарить не только для web-клиента, но и на пользователей.
Есть вероятность, что это ускорит разработку: при редизайне страницы в идеальном мире со стороны бэкенда ничего не нужно делать.
Бэкенд работает по строго типизированной схеме — и, как следствие, получаем документированное API из коробки.
Поменяли подход к проектировании задачи — теперь мы проектируем API, а не страницы.
Гарольд рад
В чем подвох
Также мы встретились и с некоторыми трудностями.
Клиент теперь сам готовит данные. Мы говорили, что это хорошо, но есть нюанс. Клиент может запросить вообще все данные в любом количестве и бэкенд тогда слегка приляжет. Поэтому нужно уметь контролировать внешние запросы. И у GraphQL есть решение — мы можем навешивать веса на определенные поля, и если сумма весов превысила заданную константу, GraphQL зареджектит этот запрос. Этот подход нормально работает, но не всегда получается сделать гибко и удобно.
Экосистема Java. Всё не так плохо, но java-мир скорее на догоняющей позиции. Обсуждение java-фреймворков мы решили вынести в отдельную статью, а видео уже можно посмотреть тут.
Построение инфраструктуры. Мониторинг, обработка ошибок, встраивание в текущую инфру — всё это реально, но во всё это нужно вкладываться. По внедрению инфраструктуры graphQL на фронтенде можно посмотреть тут.
Редиректы теперь намного сложнее
Кеширование запросов непонятно как реализовать
Ретраи — не очень понятно, что теперь идемпотентный запрос, а что нет, и какие запросы можно ретраить
Работа с неструктурированными данными — graphQL не совсем предназначен для заливки и выдачи файлов, например
Гарольд не очень рад
В общем, минусов тоже хватает, поэтому мы бы не советовали рассматривать GraphQL как следующую ступень эволюции работы с API после REST. Хотя, если вы начнёте гуглить про graphQL, то увидите пёстрые статьи о том, как он "уничтожает REST целиком и полностью". Этому не стоит верить. GraphQL — “всего лишь” альтернатива REST со своими болячками. Просто посчитайте, даёт ли он вам больше плюсов, чем минусов, оцените риски и только после этого принимайте решение — внедрять его или нет. Мы внедрили и нам понравилось.
Заключение
Вот мы и поговорили в общих чертах про GraphQL, как мы его внедряли и на какие проблемы напоролись. Эта статья — только начало весёлого цикла про GraphQL. Не переключайтесь!
Ну и пишите в комментах, пробовали ли вы подружиться с GraphQL и чем это кончилось, нам интересно.