Перейти к содержанию

GraphQL

GraphQL - спецификация языка запросов и среды их выполнения. Работает поверх разных транспортов: обычно HTTP, для подписок - WebSocket.

  • один URL - /graphql, вместо набора ресурсов
  • клиент сам решает, что получить - приходят только запрошенные поля. Это решает over-fetching (лишние данные) и under-fetching (данных не хватило, нужен ещё запрос)
  • stateless, как REST - запросы (queries) и мутации (mutations) идут обычными HTTP-запросами
  • stateful-часть - подписки (subscriptions) держат постоянное WebSocket-соединение и его состояние

Как выполняется запрос

Клиент отправляет запрос через HTTP POST:

POST /graphql HTTP/1.1
Content-Type: application/json

{
  "query": "query { user(id: 1) { name email } }"
}

Сервер:

  1. Парсит запрос в AST (Abstract Syntax Tree).
  2. Валидирует его по схеме: такие типы и поля существуют, аргументы правильные.
  3. Исполняет: для каждого поля запускает свой резолвер. Резолвер берёт данные откуда угодно:
    • из БД (SQL, MongoDB)
    • из других API (REST, gRPC)
    • из кэша (Redis)
  4. Собирает ответ в JSON ровно той формы, которую запросил клиент.
{ "data": { "user": { "name": "Alice", "email": "example@example.com" } } }

Схема и резолверы

Схема - контракт API: какие типы, поля и операции есть. Резолвер - функция, которая знает, откуда взять значение одного поля.

type Query {
  user(id: ID!): User
}

type User {
  id: ID!
  name: String!
  email: String!
  posts: [Post!]!
}
const resolvers = {
  Query: {
    user: (parent, args, context) => {
      return db.users.find(user => user.id === args.id);         // данные из БД
    }
  },
  User: {
    posts: (user) => {
      return db.posts.filter(post => post.authorId === user.id); // связанные данные
    }
  }
};

Операции

Query - чтение. Клиент точно указывает нужные поля и за один запрос получает связанные данные, например пользователя, его посты и комментарии к ним.

query {
  user(id: "1") {
    name
    email
    posts {
      title
      comments {
        text
      }
    }
  }
}

Mutation - изменение данных, аналог POST, PUT, PATCH, DELETE в REST. В ответе сразу приходят обновлённые поля.

mutation {
  createPost(title: "New Post", content: "Hello!") {
    id
    title
  }
}

Subscription - данные в реальном времени: уведомления, чаты. Единственный способ, которым сервер сам инициирует отправку данных клиенту:

  1. Клиент подключается к серверу через WebSocket и отправляет подписку.
  2. Сервер держит соединение и ждёт события, например новое сообщение в чате.
  3. Событие произошло - сервер отправляет данные всем подписанным клиентам.
subscription {
  newMessage(roomId: "123") {
    id
    text
    sender {
      name
    }
  }
}
{
  "type": "subscribe",
  "query": "subscription { newMessage { text } }"
}

Протокол поверх WebSocket - graphql-ws. Старый subscriptions-transport-ws больше не развивается.


Интроспекция - клиент сам получает схему API: типы, поля, документацию. На ней строятся автодокументация и IDE вроде GraphiQL.

query {
  __schema {
    types {
      name
      fields {
        name
        type {
          name
        }
      }
    }
  }
}

Ограничения

  • Нет встроенного HTTP-кэширования. В REST кэшируется ответ по URL, а в GraphQL всё идёт POST-запросами на один /graphql. Кэш настраивают на клиенте (Apollo Client) или через persisted queries с GET
  • Сложные запросы. Клиент может прислать запрос с глубокой вложенностью и положить сервер. Защита - лимиты глубины и сложности запроса
  • Проблема N+1. Резолвер поля posts вызывается для каждого пользователя отдельно, и на 100 пользователей выходит 101 запрос к БД. Решают батчингом через DataLoader. Та же проблема на уровне ORM - в N+1 и планы загрузки

Инструменты

Сторона Инструмент Что это
Сервер Apollo Server Сервер на Node.js
Сервер GraphQL Yoga Упрощённый сервер
Сервер Hasura GraphQL поверх PostgreSQL
Клиент Apollo Client Клиент для React и Vue
Клиент Relay Клиент от Meta, оптимизирован под их стек
Клиент URQL Лёгкая альтернатива