GraphQL: query language для API
REST построен вокруг ресурсов и фиксированных endpoint-ов: GET /users/42, GET /users/42/orders, GET /orders/100/items. Чтобы собрать данные о пользователе, его заказах и товарах в этих заказах, мобильному клиенту приходится делать три-четыре запроса по сети, склеивать их и выкидывать половину полей.
GraphQL придумали в Facebook в 2012 году и открыли в 2015. Идея простая: клиент сам описывает, какие поля и в какой форме ему нужны, а сервер возвращает ровно эту структуру. Один endpoint, один POST-запрос, любая глубина выборки.
Что такое GraphQL
GraphQL — это два разных вещи под одним названием:
- Query language — формальный синтаксис запросов с типами, переменными и фрагментами.
- Runtime — спецификация того, как сервер обрабатывает эти запросы, валидирует против schema и возвращает JSON.
GraphQL работает поверх любого транспорта (чаще всего HTTP POST), не привязан к базе данных или языку программирования. Apollo Server, Hasura, Strawberry, graphql-core — это конкретные реализации. Sanity сервер по сути просто получает текст запроса в теле POST-а и превращает его в выборки данных.
Мобильный клиент
Мобильный экран профиля: показать имя пользователя, последние 5 заказов и в каждом заказе первый товарМобильный клиент
Тот же экран -- но клиент описывает структуру одним запросомОдин ответ нужной формы
Один RTT, минимальный payload, никаких лишних полей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 не определён — берётся
# Псевдо-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) — берут
gqlN+1 проблема и DataLoader
Гибкость GraphQL имеет тёмную сторону. Запрос { users { name orders { total } } } для 100 пользователей наивно выльется в 1 (список users) + 100 (orders для каждого) = 101 SQL-запрос. Это N+1 проблема, классика ORM, обостряющаяся в GraphQL из-за вложенности.
Решение — паттерн DataLoader (придуман в Facebook вместе с GraphQL). Это маленький объект, который:
- Батчит все запросы за орудный в один пакетный вызов.tick event loop
- Кэширует результаты в рамках одного запроса, чтобы повторное обращение к тому же 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-а.
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. Имеет смысл, когда:
- Несколько клиентов с разными потребностями — мобильное приложение, веб-SPA, партнёрский dashboard. Каждый берёт свою shape данных.
- Backend for Frontend (BFF) — фронтенд-команда хочет агрегировать данные нескольких микросервисов и не зависеть от backend-команд.
- Глубокая графовая модель — социальные связи, дерево комментариев, иерархии каталога.
- Мобильные клиенты на нестабильной сети — экономия на размере payload и round trip-ах критична.
Не подходит, если:
- Public API с простой CRUD-моделью и потребностью в HTTP-кэшировании.
- Tight команда из 2 человек на 3 endpoint-а — overhead schema/codegen не окупается.
- Нужна простая интеграция с CDN/caching layer (REST с GET кэшируется проще).