Веб-разработка

GraphQL

Когда Facebook в 2012 переписывал мобильное приложение, REST API убивал производительность: на построение одного экрана нужны были 5-10 запросов, под слабым 3G это занимало секунды. Lee Byron и Nick Schrock придумали обратить: пусть клиент описывает в одном запросе ровно нужные поля, сервер вернёт ровно их - один round-trip. К 2024-му GraphQL обслуживает Shopify (5+ млрд запросов в день), GitHub, Airbnb. Не серебряная пуля - другая модель данных, со своими trade-offs.

  • **Shopify Storefront API**: 5+ млрд GraphQL запросов в день; каждый shopify-магазин использует GraphQL для гибкой витрины, не покрываемой REST.
  • **GitHub GraphQL v4**: 8 лет параллельно с REST v3; используется CLI tools, integrations, GitHub Mobile - там, где нужна гибкость queries.
  • **Apollo Federation в Netflix**: 50+ микросервисов соединены через Federated gateway - один GraphQL endpoint вместо API gateway с REST.

GraphQL Schema

В 2012 году Facebook переписывал мобильное приложение. REST API возвращал ленту новостей: посты, авторы, комментарии, лайки. Чтобы построить экран, мобильное приложение делало 5-10 запросов и склеивало данные. Под слабым 3G это было больно. Под backbone-сетью - дорого по batter. Команда Lee Byron и Nick Schrock придумали другое: клиент описывает, какие именно поля нужны, сервер возвращает ровно их. Schema-first - типы знают и клиент, и сервер. GraphQL открыто релизнули в 2015. К 2024-му его используют Shopify (5+ млрд запросов в день), GitHub, Airbnb, Twitter. Не серебряная пуля - но другая модель данных API.

Schema Definition Language (SDL): type определяет объект, query/mutation/subscription - root типы. Скалары: Int, Float, String, Boolean, ID + custom (DateTime, JSON). Модификаторы: ! - non-null, [] - список, [Type!]! - non-null list of non-null. Интерфейсы и union types для полиморфизма. Директивы (@deprecated, @auth) для метаинформации. Schema = контракт между клиентом и сервером.

Cursor pagination через connections (Relay-стандарт) - правильный способ paginate в GraphQL. offset+limit ломается при изменении данных между запросами. Cursor-based устойчив: 'дай мне следующие N после этого cursor'.

Главное преимущество GraphQL над REST API для мобильного клиента - это:

Resolvers

Schema - это что. Resolvers - это как. Для каждого поля в schema существует функция, возвращающая его значение. GraphQL движок обходит дерево запроса и для каждого поля вызывает соответствующий resolver. Resolver получает: parent (родительский объект), args (аргументы запроса), context (auth, data sources), info (info о текущем поле в schema). Если post.author не имеет resolver - используется default: post.author (читаем поле из объекта). Если есть - вызывается явная функция. Это даёт точку расширения для lazy-loading: post сам по себе не содержит author, но resolver запрашивает его при необходимости.

Resolver chain - дерево вызовов соответствует структуре запроса. Query { user(id) { posts { author { name } } } } - 4 уровня resolver'ов: Query.user -> User.posts -> Post.author -> User.name. Каждый возвращает Promise или value. Apollo Server, Mercurius, Yoga - популярные runtime. Resolver context создаётся per-request, содержит DB connections, current user, dataloader instances. Auth и authorization обычно реализованы через directives или context check в resolver.

Query depth и complexity attack: злоумышленник может запросить { user { posts { author { posts { author { posts { ... }}}}}}} - бесконечная вложенность экспоненциально нагружает БД. Защита: depth-limit (типично 7-10), query complexity calculation (graphql-cost-analysis), persisted queries для prod.

Что произойдёт, если для типа Post не определён resolver для поля author, но в БД у post есть колонка authorId?

Subscriptions

Query - один-разовый запрос. Mutation - один-разовое изменение. Subscription - long-lived поток событий: клиент подписывается на server-pushed updates. WebSocket поверх graphql-ws протокола - стандарт. Каждый subscription - persistent connection с фильтрами на сервере. Apollo Server + Redis PubSub - типичная архитектура: mutation publish'ит событие в Redis channel, все subscriber'ы фильтруют по своим args. Реальные применения: чат, live notifications, collaborative editing (Figma), dashboard real-time. Не для всего - polling часто проще и достаточно. Subscriptions имеют смысл при < 1s latency и event-driven UX.

Subscription resolver возвращает AsyncIterator вместо value. PubSub - thin abstraction поверх Redis/Kafka/RabbitMQ. Subscription filter - функция, фильтрующая события для конкретного subscriber. Authorization в subscription выполняется при подключении (через connectionParams) и опционально при каждом event. Server-sent events (SSE) - альтернатива WebSocket: проще, но одностороннее (server -> client). graphql-http умеет multipart response для streaming результатов больших queries.

Subscription persistent connection растёт линейно с числом активных подписчиков. 100k онлайн-пользователей в чате = 100k WebSocket соединений. Память на connection ~5-10 KB, плюс CPU на keepalive. Нужен horizontal scaling через sticky sessions или Redis PubSub fanout. Cloudflare Workers/Durable Objects - правильное окружение для масштабируемых subscriptions.

Чем GraphQL subscription отличается от REST long-polling для real-time updates?

DataLoader и N+1

Самая частая болезнь GraphQL - N+1 запросы. Query { posts { author { name } } } делает 1 запрос на posts (получает 100 записей), а потом 100 отдельных запросов на каждого author. Один пользовательский query превращается в 101 SQL-запрос. На REST это видно сразу: разработчик пишет JOIN. В GraphQL resolver вызывается per-field, иллюзия чистоты прячет проблему. DataLoader (Facebook 2014) решает: оборачивает функцию load(id), batching и dedup запросы за один tick event loop'а. Resolver вызывает loader.load(authorId) сто раз - DataLoader аккумулирует ID и делает ОДИН запрос: SELECT * FROM users WHERE id IN (1,2,...,100). Caching per-request защищает от повторных запросов одного и того же объекта.

DataLoader API: new DataLoader(batchLoadFn) - функция принимает list of keys, возвращает list of values (в том же порядке!). loader.load(key) - async, возвращает Promise. loader.loadMany([keys]) - batch версия. Cache scope per-request, не shared между requests. Сложные fetcher'ы: dataloader-mongoose, mongoose-dataloader-cache - готовые интеграции. Альтернатива - join monster: парсит GraphQL info и генерирует SQL JOIN, но менее гибко.

DataLoader cache scope - per-request важно. Если cache был бы global, разные пользователи получали бы данные друг друга, и stale-data между запросами. Per-request: cache живёт длительность одного GraphQL request, дальше garbage-collected.

GraphQL заменяет REST - надо переходить на GraphQL для всех новых API

GraphQL и REST решают разные задачи. GraphQL силён при: гибких клиентах с разными нуждами, агрегации из нескольких сервисов, mobile приложения с разными экранами. REST силён при: file uploads, простые CRUD, HTTP caching, public APIs (легче документировать и понимать). Многие компании используют оба: GraphQL для BFF (backend-for-frontend), REST для service-to-service

GraphQL добавляет complexity: schema, resolvers, N+1 проблема, security (query depth/complexity), client setup (Apollo/Relay). Для простого CRUD API эта overhead не окупается. GitHub использует GraphQL v4 параллельно с REST v3 уже 8 лет - оба нужны.

DataLoader решает N+1. Но если в одном запросе спрашивается { posts { author { posts { author } } } } - что происходит?

Ключевые идеи

  • **Schema-first контракт**: клиент и сервер знают типы, ID, обязательность полей. SDL - источник истины, генератор client/server SDK.
  • **Resolvers** - функции на каждое поле, дерево вызовов соответствует структуре query. Default resolver работает для скаляров, для связей нужны explicit resolvers.
  • **Subscriptions через WebSocket** дают server-pushed real-time updates. Не для всего - polling часто проще, subscription оправдан при < 1s latency.
  • **DataLoader** решает N+1: batching и dedup запросов в одном tick event loop'а. Без него GraphQL легко даёт 100+ SQL-запросов на одну query.

Связанные темы

GraphQL пересекается с REST, БД, и микросервисной архитектурой:

  • REST API Design — GraphQL и REST - не альтернативы, а инструменты разных задач. Многие используют оба: GraphQL для BFF, REST для public APIs и file uploads
  • Микросервисы — GraphQL Federation объединяет схемы из разных сервисов в единый supergraph - альтернатива API gateway для микросервисного backend'а

Вопросы для размышления

  • GraphQL даёт клиенту гибкость, но снимает HTTP caching (всё POST на /graphql). Persisted queries и automatic persisted queries (APQ) - решение, но они добавляют сложность. В каких сценариях REST остаётся правильным выбором?
  • Schema-first vs code-first: схема описана в SDL отдельно, или генерируется из кода. Какой подход лучше для команды из 20 человек, разрабатывающей public API?
  • Federation позволяет соединять GraphQL-схемы из разных сервисов. Но это вводит coupling и сложности отладки. Когда federation оправдан, а когда лучше иметь один монолитный GraphQL service?

Связанные уроки

  • web-12 — REST API - предшественник GraphQL
  • web-14 — После GraphQL - аутентификация и авторизация
  • db-05-sql-basics — GraphQL queries и SQL SELECT: схожий декларативный стиль
  • aie-16-tool-calling — GraphQL resolver и tool calling - декларативная спецификация доступа
  • ds-09-trees-intro — GraphQL Schema - это дерево типов, traversal которого даёт данные
  • net-21-http-basics
GraphQL

0

1

Войти