Learning Platform
Глоссарий Troubleshooting
Урок 11.01 · 35 мин
Начальный
GraphQLApolloSchemaResolversDataLoaderBFF

GraphQL: query language для API

REST построен вокруг ресурсов и фиксированных endpoint-ов: GET /users/42, GET /users/42/orders, GET /orders/100/items. Чтобы собрать данные о пользователе, его заказах и товарах в этих заказах, мобильному клиенту приходится делать три-четыре запроса по сети, склеивать их и выкидывать половину полей.

Это называется underfetching и overfetching.

GraphQL придумали в Facebook в 2012 году и открыли в 2015. Идея простая: клиент сам описывает, какие поля и в какой форме ему нужны, а сервер возвращает ровно эту структуру. Один endpoint, один POST-запрос, любая глубина выборки.

Что такое GraphQL

GraphQL — это два разных вещи под одним названием:

  1. Query language — формальный синтаксис запросов с типами, переменными и фрагментами.
  2. Runtime — спецификация того, как сервер обрабатывает эти запросы, валидирует против schema и возвращает JSON.

GraphQL работает поверх любого транспорта (чаще всего HTTP POST), не привязан к базе данных или языку программирования. Apollo Server, Hasura, Strawberry, graphql-core — это конкретные реализации. Sanity сервер по сути просто получает текст запроса в теле POST-а и превращает его в выборки данных.

REST против GraphQL: один экран мобильного приложения

Мобильный клиент

Мобильный экран профиля: показать имя пользователя, последние 5 заказов и в каждом заказе первый товар
REST: 7 запросов
GET /users/42Возвращает все 30 полей пользователя -- нужны только имя и email
GET /users/42/orders?limit=5Список заказов с полными деталями каждого
5x GET /orders/N/items?limit=1По одному запросу на каждый заказ -- N+1 проблема на стороне клиента

Мобильный клиент

Тот же экран -- но клиент описывает структуру одним запросом
GraphQL: 1 POST
POST /graphqlВ body -- текст запроса с нужной формой данных. Ответ ровно такой же формы, без лишних полей

Один ответ нужной формы

Один RTT, минимальный payload, никаких лишних полей
Protobuf и JSON Schema в Schema Registry

Schema: типы как контракт

Сердце любого GraphQL-сервиса — schema. Это типизированное описание того, что можно запрашивать. Schema пишется на Schema Definition Language (SDL):

type Query {
  user(id: ID!): User
  searchProducts(q: String!, limit: Int = 20): [Product!]!
}

type Mutation {
  createOrder(input: CreateOrderInput!): Order!
}

type User {
  id: ID!
  email: String!
  name: String
  orders(status: OrderStatus): [Order!]!
}

type Order {
  id: ID!
  total: Float!
  status: OrderStatus!
  items: [OrderItem!]!
}

enum OrderStatus {
  PENDING
  PAID
  SHIPPED
  CANCELLED
}

input CreateOrderInput {
  userId: ID!
  productIds: [ID!]!
}

Здесь видны все ключевые конструкции SDL:

  • Скаляры — встроенные Int, Float, String, Boolean, ID. Можно объявлять кастомные: scalar DateTime, scalar JSON.
  • Восклицательный знак ! после типа означает «не может быть null». String! — обязательная строка. [Order!]! — непустой массив, в котором каждый элемент гарантированно не null.
  • Enums — фиксированный набор значений.
  • Object types (User, Order) — объекты с именованными полями.
  • Input types (CreateOrderInput) — отдельный вид типов для аргументов мутаций; нельзя путать с обычными object types.
  • Interfaces и unions — для полиморфизма: interface Node { id: ID! }, union SearchResult = Product | Article | User.

Три корневых типа: Query (чтение), Mutation (изменение), Subscription (подписка на события). Из этих типов начинается обход.

Resolvers: как сервер отвечает

Schema описывает, что можно запросить. Resolvers — это функции, которые отвечают, как собрать данные для каждого поля. На каждое поле любого типа можно подвесить свой resolver. Если resolver не определён — берётся

default resolver
.

# Псевдо-Python с библиотекой strawberry
import strawberry
from typing import Optional

@strawberry.type
class User:
    id: str
    email: str
    name: Optional[str]

    @strawberry.field
    async def orders(self, status: Optional[OrderStatus] = None) -> list["Order"]:
        # Этот resolver выполняется только если клиент запросил поле orders
        return await load_orders_for_user(self.id, status)

@strawberry.type
class Query:
    @strawberry.field
    async def user(self, id: str) -> Optional[User]:
        return await load_user(id)

Ключевая особенность: resolver orders выполнится только если клиент явно запросил поле orders. Это и есть «клиент задаёт shape». Если запрос — просто { user(id: "42") { name email } }, никакого обращения к таблице orders не произойдёт.

Operations: query, mutation, subscription

Три типа операций отличаются семантикой, но синтаксис похож:

# Query -- чтение, без побочных эффектов
query GetUserProfile($id: ID!) {
  user(id: $id) {
    name
    email
    orders(status: PAID) {
      id
      total
    }
  }
}

# Mutation -- изменение состояния
mutation PlaceOrder($input: CreateOrderInput!) {
  createOrder(input: $input) {
    id
    status
    total
  }
}

# Subscription -- подписка на события (требует WebSocket или SSE)
subscription OnOrderUpdated($orderId: ID!) {
  orderUpdated(id: $orderId) {
    status
    updatedAt
  }
}

Variables ($id, $input) передаются отдельным JSON-объектом, не вставляются в текст запроса. Это аналог prepared statements в SQL — никакой конкатенации строк, никаких injection-уязвимостей.

Operation name (GetUserProfile, PlaceOrder) — необязательное имя, но крайне полезное в логах, метриках и кэше клиента.

Fragments: переиспользование кусков

Если тот же набор полей User нужен в нескольких запросах, его выносят во fragment:

fragment UserSummary on User {
  id
  name
  email
}

query Dashboard {
  me { ...UserSummary }
  recentVisitors { ...UserSummary }
}

Fragment-ы избавляют от копипасты и помогают клиентским кодогенераторам (Relay, Apollo Codegen) автоматически выводить TypeScript-типы.

Introspection: schema как данные

GraphQL-сервер сам публикует свою schema через специальные query: __schema, __type. Это и называется introspection. На этом построены инструменты:

  • GraphiQL — встроенный IDE с автодополнением и документацией.
  • Apollo Studio, GraphQL Playground — то же самое, но красивее.
  • Codegen — автоматическая генерация TypeScript-типов или Python-DTO из schema.

В production introspection часто отключают для приватных API, чтобы не светить внутреннюю модель данных наружу.

Работа с GraphQL из Python

Для DE-задач (выгрузка данных из GraphQL-источника — Shopify, GitHub, Hasura) хватает обычного httpx с POST-запросом:

import httpx

QUERY = """
query Repos($login: String!, $first: Int!) {
  user(login: $login) {
    repositories(first: $first, orderBy: {field: STARGAZERS, direction: DESC}) {
      nodes { name stargazerCount primaryLanguage { name } }
      pageInfo { hasNextPage endCursor }
    }
  }
}
"""

async def fetch_top_repos(login: str, first: int = 50) -> list[dict]:
    async with httpx.AsyncClient(timeout=30.0) as client:
        resp = await client.post(
            "https://api.github.com/graphql",
            json={"query": QUERY, "variables": {"login": login, "first": first}},
            headers={"Authorization": f"bearer {TOKEN}"},
        )
        resp.raise_for_status()
        payload = resp.json()
        if "errors" in payload:
            raise RuntimeError(payload["errors"])
        return payload["data"]["user"]["repositories"]["nodes"]

Если нужны фичи посерьёзнее (subscriptions через WebSocket, retry, batching) — берут

библиотеку gql
.

N+1 проблема и DataLoader

Гибкость GraphQL имеет тёмную сторону. Запрос { users { name orders { total } } } для 100 пользователей наивно выльется в 1 (список users) + 100 (orders для каждого) = 101 SQL-запрос. Это N+1 проблема, классика ORM, обостряющаяся в GraphQL из-за вложенности.

Решение — паттерн DataLoader (придуман в Facebook вместе с GraphQL). Это маленький объект, который:

  1. Батчит все запросы за орудный
    tick event loop
    в один пакетный вызов.
  2. Кэширует результаты в рамках одного запроса, чтобы повторное обращение к тому же id не лезло в БД дважды.
# Псевдокод для strawberry-graphql
from strawberry.dataloader import DataLoader

async def load_orders(user_ids: list[str]) -> list[list[Order]]:
    # Один SQL вместо N: SELECT * FROM orders WHERE user_id IN (...)
    rows = await db.fetch("SELECT * FROM orders WHERE user_id = ANY($1)", user_ids)
    by_user = group_by(rows, key=lambda r: r["user_id"])
    return [by_user.get(uid, []) for uid in user_ids]

orders_loader = DataLoader(load_fn=load_orders)

DataLoader должен создаваться на каждый HTTP-запрос отдельно, чтобы кэш не утекал между разными пользователями. Обычно его кладут в context resolver-а.

WARNING

N+1 — главная причина, почему «GraphQL-сервер тормозит». Без DataLoader (или эквивалента в виде join-аннотаций SQLAlchemy) GraphQL легко выдаёт сотни запросов в БД на один user-facing запрос. Профилируйте sql_log в dev-окружении.

Экосистема в 2026 году

  • Apollo Server / Apollo Client — де-факто стандарт в JS-мире. На бэке поддерживает Federation для микросервисной композиции schema.
  • Hasura — мгновенный GraphQL поверх Postgres/MS SQL/BigQuery. Не пишете resolvers — генерация по схеме БД с разрешениями на уровне строк.
  • Strawberry (Python) — code-first GraphQL c типами через dataclass-стиль.
  • GraphiQL / Apollo Studio Sandbox — UI для тестирования запросов.
  • pothos / nexus — TypeScript-библиотеки с типобезопасной schema.

Когда выбрать GraphQL

GraphQL не «лучше REST» — это другой trade-off. Имеет смысл, когда:

  1. Несколько клиентов с разными потребностями — мобильное приложение, веб-SPA, партнёрский dashboard. Каждый берёт свою shape данных.
  2. Backend for Frontend (BFF) — фронтенд-команда хочет агрегировать данные нескольких микросервисов и не зависеть от backend-команд.
  3. Глубокая графовая модель — социальные связи, дерево комментариев, иерархии каталога.
  4. Мобильные клиенты на нестабильной сети — экономия на размере payload и round trip-ах критична.

Не подходит, если:

  • Public API с простой CRUD-моделью и потребностью в HTTP-кэшировании.
  • Tight команда из 2 человек на 3 endpoint-а — overhead schema/codegen не окупается.
  • Нужна простая интеграция с CDN/caching layer (REST с GET кэшируется проще).
Проверка знанийKnowledge check
Мобильное приложение делает один POST на /graphql и получает ровно 5 полей user-а из 30 возможных. Объясните, почему это технически возможно и что произошло на сервере.
ОтветAnswer
Возможно благодаря двум вещам: query language и schema-driven resolvers. Клиент в теле POST отправил текст GraphQL-запроса с явным перечислением 5 полей -- именно эту форму JSON и сериализует сервер. На сервере произошла валидация: парсер построил AST запроса, проверил против schema (есть ли такие поля у типа User, правильные ли типы аргументов), затем GraphQL runtime обошёл AST и вызвал resolvers только для запрошенных полей. Resolver-ы для остальных 25 полей не вызывались -- это и есть экономия. В итоге payload содержит ровно ту структуру, что описана в запросе, без overfetching. Если у каких-то из этих 5 полей собственные resolver-ы (например, computed field), они тоже выполнились -- но никаких лишних обращений к БД для невостребованных полей не было.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Чем GraphQL принципиально отличается от REST на уровне модели взаимодействия?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 3