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 |
Частые ошибки
- N+1 проблема — запрос user.posts для 100 юзеров = 1 + 100 SQL-запросов (решение: DataLoader)
- Слишком глубокие запросы — клиент запрашивает 10 уровней вложенности → DDoS (решение: depth limit)
- Нет валидации input — GraphQL валидирует типы, но не бизнес-логику (email формат)
- Всё через GraphQL — файлы и стриминг лучше через REST
- Нет error handling — ошибки в массиве
errors, а HTTP-статус всегда 200
Практика
- Описать GraphQL-схему для блога (User, Post, Comment)
- Написать query для получения пользователя с его постами
- Написать mutation для создания поста
- Реализовать запрос с клиента через fetch
- Сравнить: сколько REST-запросов нужно для той же страницы
Связанные темы
- REST API — классический подход к API
- HTTP протокол — GraphQL работает поверх HTTP
- Клиент-серверное взаимодействие — паттерны взаимодействия