GraphQL Basics

Зачем нужно

GraphQL — язык запросов к API, разработанный Facebook. В отличие от REST, где сервер определяет структуру ответа, в GraphQL клиент сам указывает, какие именно данные ему нужны. Это решает проблемы over-fetching (получаем лишнее) и under-fetching (данных не хватает, нужен ещё запрос).

Где используется

  • Приложения со сложными связями данных (соцсети, маркетплейсы)
  • Mobile-first API (экономия трафика — запрашивать только нужное)
  • Объединение нескольких бэкендов (GraphQL как API gateway)
  • GitHub API v4, Shopify, Hasura, Apollo

GraphQL vs REST

REST — Over-fetching:
GET /api/users/42
→ { id, name, email, phone, address, avatar, bio, createdAt, settings... }
  Нужны были только name и avatar

REST — Under-fetching:
GET /api/users/42        → { id, name }
GET /api/users/42/posts  → [{ id, title }]
GET /api/posts/1/comments → [{ text }]
  3 запроса для одной страницы

GraphQL — один запрос, только нужные данные:
POST /graphql
{
  user(id: 42) {
    name
    avatar
    posts {
      title
      comments {
        text
      }
    }
  }
}
→ Ровно те данные, которые запросили

Schema (схема)

Схема описывает типы данных и доступные операции:

# Типы данных
type User {
  id: ID!
  name: String!
  email: String!
  age: Int
  posts: [Post!]!
}

type Post {
  id: ID!
  title: String!
  content: String!
  author: User!
  comments: [Comment!]!
  createdAt: String!
}

type Comment {
  id: ID!
  text: String!
  author: User!
}

# Queries — чтение данных
type Query {
  user(id: ID!): User
  users(limit: Int, offset: Int): [User!]!
  post(id: ID!): Post
  posts(authorId: ID): [Post!]!
}

# Mutations — изменение данных
type Mutation {
  createUser(input: CreateUserInput!): User!
  updateUser(id: ID!, input: UpdateUserInput!): User!
  deleteUser(id: ID!): Boolean!
  createPost(input: CreatePostInput!): Post!
}

# Input-типы для мутаций
input CreateUserInput {
  name: String!
  email: String!
  age: Int
}

input UpdateUserInput {
  name: String
  email: String
  age: Int
}

input CreatePostInput {
  title: String!
  content: String!
  authorId: ID!
}

Скалярные типы

String    — строка
Int       — целое число
Float     — дробное число
Boolean   — true/false
ID        — уникальный идентификатор

!         — обязательное поле (non-null)
[Type]    — массив
[Type!]!  — обязательный массив обязательных элементов

Queries (запросы)

# Простой запрос
query {
  user(id: "42") {
    name
    email
  }
}

# Ответ:
# { "data": { "user": { "name": "Антон", "email": "a@mail.ru" } } }

# Вложенные данные
query {
  user(id: "42") {
    name
    posts {
      title
      comments {
        text
        author {
          name
        }
      }
    }
  }
}

# Переменные
query GetUser($id: ID!) {
  user(id: $id) {
    name
    email
  }
}
# Variables: { "id": "42" }

# Алиасы — два запроса одного типа
query {
  admin: user(id: "1") { name }
  viewer: user(id: "42") { name }
}
# { "data": { "admin": { "name": "Admin" }, "viewer": { "name": "Антон" } } }

# Фрагменты — переиспользование полей
fragment UserInfo on User {
  name
  email
  age
}

query {
  admin: user(id: "1") { ...UserInfo }
  viewer: user(id: "42") { ...UserInfo }
}

Mutations (мутации)

# Создание
mutation {
  createUser(input: { name: "Антон", email: "a@mail.ru" }) {
    id
    name
  }
}

# С переменными
mutation CreateUser($input: CreateUserInput!) {
  createUser(input: $input) {
    id
    name
    email
  }
}
# Variables: { "input": { "name": "Антон", "email": "a@mail.ru" } }

# Обновление
mutation {
  updateUser(id: "42", input: { name: "Антон Новый" }) {
    id
    name
  }
}

Resolvers (резолверы)

Функции, которые возвращают данные для каждого поля:

// Node.js с apollo-server
const resolvers = {
  Query: {
    user: async (parent, { id }, context) => {
      return context.db.users.findById(id);
    },
    users: async (parent, { limit, offset }, context) => {
      return context.db.users.findAll({ limit, offset });
    },
  },

  Mutation: {
    createUser: async (parent, { input }, context) => {
      return context.db.users.create(input);
    },
    updateUser: async (parent, { id, input }, context) => {
      return context.db.users.update(id, input);
    },
  },

  // Резолвер для вложенного поля
  User: {
    posts: async (user, args, context) => {
      // user — родительский объект (результат Query.user)
      return context.db.posts.findByAuthor(user.id);
    },
  },

  Post: {
    author: async (post, args, context) => {
      return context.db.users.findById(post.authorId);
    },
    comments: async (post, args, context) => {
      return context.db.comments.findByPost(post.id);
    },
  },
};

Запросы с клиента (fetch)

// GraphQL — это обычный POST-запрос
async function graphqlRequest(query, variables = {}) {
  const response = await fetch('/graphql', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ query, variables }),
  });

  const result = await response.json();

  if (result.errors) {
    throw new Error(result.errors[0].message);
  }

  return result.data;
}

// Использование
const data = await graphqlRequest(`
  query GetUser($id: ID!) {
    user(id: $id) {
      name
      email
      posts { title }
    }
  }
`, { id: '42' });

console.log(data.user.name);

GraphQL vs REST: сравнение

Аспект REST GraphQL
Endpoint Много (/users, /posts) Один (/graphql)
Данные Фиксированная структура Клиент выбирает
Over-fetching Часто Нет
Under-fetching Часто Нет
Кэширование HTTP cache (просто) Сложнее (POST)
Версионирование /v1/, /v2/ Не нужно (deprecate поля)
Обучение Проще Сложнее
Tooling Curl, Postman GraphiQL, Apollo DevTools

Частые ошибки

  1. N+1 проблема — запрос user.posts для 100 юзеров = 1 + 100 SQL-запросов (решение: DataLoader)
  2. Слишком глубокие запросы — клиент запрашивает 10 уровней вложенности → DDoS (решение: depth limit)
  3. Нет валидации input — GraphQL валидирует типы, но не бизнес-логику (email формат)
  4. Всё через GraphQL — файлы и стриминг лучше через REST
  5. Нет error handling — ошибки в массиве errors, а HTTP-статус всегда 200

Практика

  1. Описать GraphQL-схему для блога (User, Post, Comment)
  2. Написать query для получения пользователя с его постами
  3. Написать mutation для создания поста
  4. Реализовать запрос с клиента через fetch
  5. Сравнить: сколько REST-запросов нужно для той же страницы

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

Ресурсы