Перейти к содержимому

Решение проблем REST

Решение проблем REST: GraphQL решает over-fetching (клиент выбирает нужные поля), under-fetching (один запрос для связанных данных), N+1 проблему (один запрос вместо каскада) и версионирование (добавление полей вместо v2/v3).

Введение: REST хорош, но не идеален

REST — это доминирующий стиль API в современном вебе. Он прост, понятен, основан на HTTP и используется миллионами сервисов. Но у REST есть проблемы, которые становятся особенно заметными в сложных приложениях с большим количеством взаимосвязанных данных.

Представьте, что вы разрабатываете мобильное приложение для социальной сети. На главном экране нужно показать: информацию о пользователе, его последние посты, подписчиков, друзей, лайки под постами. В REST это может означать 5-10 отдельных запросов к разным эндпоинтам. Приложение медленное, трафик дорогой, батарея разряжается.

GraphQL был создан в Facebook именно для решения этих проблем. Он не отрицает ценность REST, но предлагает другой подход для сценариев, где REST показывает свои слабые места.

Проблемы REST, которые решает GraphQL:

  1. Over-fetching — получение лишних данных
  2. Under-fetching — неполучение нужных данных
  3. Множество запросов — N+1 проблема и круговые зависимости
  4. Версионирование — как менять API, не ломая клиентов
  5. Слабая типизация — нет формального контракта
  6. Сложность с вложенными ресурсами — глубокие связи требуют много запросов

Проблема 1: Over-fetching (Избыточность данных)

Что это такое

Over-fetching — это ситуация, когда клиент получает больше данных, чем ему нужно. Сервер возвращает весь объект целиком, а клиенту нужны только некоторые поля.

Пример в REST

GET /users/123
{
    "id": 123,
    "name": "Иван",
    "email": "ivan@example.com",
    "phone": "+7-999-123-45-67",
    "address": "Москва, Тверская, 1",
    "birthday": "1990-01-01",
    "created_at": "2024-01-15T10:30:00Z",
    "updated_at": "2024-03-20T15:45:00Z",
    "is_active": true,
    "is_verified": true,
    "last_login": "2024-04-10T08:00:00Z",
    "preferences": {...},
    "statistics": {...},
    "settings": {...}
}

Мобильному приложению для отображения имени пользователя нужны только id и name. Остальные 90% данных — лишние.

Почему это плохо

ПоследствиеОбъяснение
ТрафикПередаётся много лишних байт (особенно критично для мобильных сетей)
Время загрузкиБольше данных → дольше загрузка
ПамятьПриложение парсит и хранит ненужные данные
БатареяБольше данных → больше процессорного времени

Как GraphQL решает

Клиент сам выбирает, какие поля ему нужны.

query {
    user(id: "123") {
        id
        name
    }
}

Ответ (только то, что попросили):

{
    "data": {
        "user": {
            "id": "123",
            "name": "Иван"
        }
    }
}

Масштаб проблемы

СценарийREST (данные)GraphQL (данные)Экономия
Имя пользователя500 байт50 байт90%
Список пользователей (100 шт)50 КБ5 КБ90%
Список товаров с картинками5 МБ500 КБ90%

Проблема 2: Under-fetching (Недостаточность данных)

Что это такое

Under-fetching — это ситуация, когда одного запроса недостаточно, чтобы получить все нужные данные. Клиенту приходится делать несколько запросов, чтобы собрать полную информацию.

Пример в REST

Нужно показать пользователя и его последние 5 постов.

# Первый запрос: получить пользователя
GET /users/123
{
    "id": 123,
    "name": "Иван"
}
# Второй запрос: получить посты пользователя
GET /users/123/posts?limit=5
[
    {"id": 1, "title": "Пост 1", "content": "..."},
    {"id": 2, "title": "Пост 2", "content": "..."}
]

Два запроса вместо одного.

Почему это плохо

ПоследствиеОбъяснение
ЗадержкаКаждый запрос добавляет время (RTT — round trip time)
СложностьКлиент должен управлять несколькими запросами, ждать все ответы
ТрафикЗаголовки HTTP добавляются к каждому запросу

Как GraphQL решает

Один запрос может получить связанные данные.

query {
    user(id: "123") {
        name
        posts(limit: 5) {
            id
            title
            content
        }
    }
}

Ответ (один запрос):

{
    "data": {
        "user": {
            "name": "Иван",
            "posts": [
                {"id": 1, "title": "Пост 1", "content": "..."},
                {"id": 2, "title": "Пост 2", "content": "..."}
            ]
        }
    }
}

Проблема 3: Множество запросов (N+1 и круговые зависимости)

Что это такое

В сложных интерфейсах один экран может требовать данные из многих связанных ресурсов. REST часто приводит к каскаду последовательных запросов.

Пример: Лента новостей

Для отображения ленты новостей нужно:

  1. Получить список друзей
  2. Для каждого друга получить его последние посты
  3. Для каждого поста получить лайки
  4. Для каждого лайка получить информацию о пользователе
    graph TD
    A[GET /friends] --> B[GET /users/1/posts]
    A --> C[GET /users/2/posts]
    A --> D[GET /users/3/posts]
    B --> E[GET /posts/101/likes]
    B --> F[GET /posts/102/likes]
    C --> G[GET /posts/201/likes]
    E --> H[GET /users/501]
    E --> I[GET /users/502]
  

Количество запросов: 1 (друзья) + N (посты друга) + M (лайки на пост) + K (пользователи лайков)

При 10 друзьях, у каждого 5 постов, на каждом посте 10 лайков → 1 + 10 + 50 + 500 = 561 запрос!

Почему это плохо

ПоследствиеОбъяснение
Время загрузкиДесятки или сотни запросов → секунды ожидания
Нагрузка на серверКаждый запрос — это обработка, БД, сеть
Сложность кодаПромисы, async/await, обработка ошибок для каждого запроса

Как GraphQL решает

Один запрос с глубокой вложенностью.

query {
    me {
        friends(limit: 10) {
            posts(limit: 5) {
                likes(limit: 10) {
                    user {
                        name
                        avatar
                    }
                }
            }
        }
    }
}

Один запрос, одна поездка на сервер. Сервер сам разбирается, как эффективно собрать все данные (с помощью DataLoader для батчинга).

Проблема 4: Версионирование

Что это такое

Когда API меняется, нужно как-то управлять изменениями, чтобы не сломать старых клиентов. В REST это обычно делается через версионирование.

Пример версионирования в REST

GET /v1/users/123
GET /v2/users/123
GET /v3/users/123

Проблемы версионирования в REST:

ПроблемаОбъяснение
Разрастаниеv1, v2, v3, v4, v5 — нужно поддерживать все
Сложность кодаУсловные операторы, адаптеры, разные сериализаторы
Миграция клиентовКлиенты не обновляются мгновенно

Как GraphQL решает

GraphQL не требует версионирования. Вместо этого:

ПрактикаОбъяснение
Добавляйте поля, не удаляйтеСтарые клиенты не видят новые поля, новые — используют
Пометка @deprecatedПоле помечается устаревшим, но продолжает работать
Новые типыВместо изменения старого типа, создайте новый
type User {
    id: ID!
    name: String!
    oldField: String @deprecated(reason: "Use newField instead")
    newField: String
}

Результат: Клиенты на v1 продолжают работать бесконечно. Клиенты на v2 постепенно переходят на новые поля. Нет необходимости в поддержке нескольких версий API.

Проблема 5: Слабая типизация

Что это такое

REST API обычно не имеет формального контракта. Документация (OpenAPI) — опциональна. Клиент узнаёт структуру ответа только во время выполнения.

Пример

GET /users/123
{
    "id": 123,
    "name": "Иван"
}

Что такое id? Строка? Число? Что будет, если его нет? Клиент узнает только в рантайме.

Почему это плохо

ПоследствиеОбъяснение
Ошибки в рантаймеОшибки типов обнаруживаются только при выполнении
Сложная документацияНужно поддерживать документацию отдельно
Сложная генерация клиентовСложно генерировать типизированные клиенты

Как GraphQL решает

GraphQL имеет строгую типизацию “из коробки”. Схема — это контракт.

type User {
    id: ID!
    name: String!
    email: String!
    age: Int
}

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

Что это даёт:

ПреимуществоОбъяснение
Валидация на этапе запросаНеправильный запрос (поле не существует, тип не совпадает) вернёт ошибку ДО выполнения
АвтодокументацияИнтроспекция даёт документацию бесплатно
Автогенерация клиентовTypeScript, Swift, Kotlin клиенты генерируются из схемы
Автодополнение в IDEGraphQL-плагины знают схему и подсказывают поля

Проблема 6: Сложность с вложенными ресурсами

Что это такое

В REST отношения между ресурсами часто выражаются через вложенные эндпоинты.

GET /users/123/posts
GET /users/123/posts/456/comments
GET /users/123/posts/456/comments/789/replies

Почему это плохо

ПоследствиеОбъяснение
Глубокие URL/users/123/posts/456/comments/789/replies — трудно читать
Много эндпоинтовКаждая комбинация ресурсов требует отдельного эндпоинта
НегибкостьЕсли нужен другой путь (например, посты с комментариями, но без автора), нужен новый эндпоинт

Как GraphQL решает

В GraphQL любой тип может иметь поля любого другого типа. Нет вложенных URL — есть вложенные типы.

query {
    user(id: "123") {
        posts {
            comments {
                replies {
                    author {
                        name
                    }
                }
            }
        }
    }
}

Один запрос, сколько угодно уровней вложенности. Клиент сам решает, насколько глубоко идти.

Сравнение: REST vs GraphQL на примере

Задача: Показать профиль пользователя с его последними 5 постами и 3 последними комментариями к каждому посту

REST подход:

# 1. Получить пользователя
GET /users/123

# 2. Получить посты
GET /users/123/posts?limit=5

# 3. Для каждого поста (5 раз) получить комментарии
GET /posts/1/comments?limit=3
GET /posts/2/comments?limit=3
GET /posts/3/comments?limit=3
GET /posts/4/comments?limit=3
GET /posts/5/comments?limit=3

Итого: 1 + 1 + 5 = 7 запросов

GraphQL подход:

query {
    user(id: "123") {
        name
        email
        avatar
        posts(limit: 5) {
            title
            content
            createdAt
            comments(limit: 3) {
                text
                author {
                    name
                }
                createdAt
            }
        }
    }
}

Итого: 1 запрос

Когда REST всё ещё хорош

GraphQL решает реальные проблемы, но REST не стал плохим. REST всё ещё лучше в некоторых сценариях:

СценарийПочему REST лучше
Простые APICRUD над несколькими ресурсами
КешированиеHTTP кеш (CDN, браузер) отлично работает с REST
Загрузка файловGraphQL не имеет встроенной поддержки
Публичные APIREST проще для внешних разработчиков
Аналитика и логиREST эндпоинты легче анализировать

Резюме для системного аналитика

  1. Over-fetching — клиент получает лишние данные. GraphQL решает: клиент сам выбирает поля.

  2. Under-fetching — одного запроса недостаточно. GraphQL решает: один запрос может получить связанные данные любой глубины.

  3. Множество запросов (N+1) — каскад последовательных запросов. GraphQL решает: один запрос вместо десятков или сотен.

  4. Версионирование — поддержка нескольких версий API сложна. GraphQL решает: добавляйте поля, не удаляйте; deprecation вместо удаления.

  5. Слабая типизация — нет формального контракта. GraphQL решает: строгая схема, валидация на этапе запроса, автодокументация.

  6. Сложность с вложенными ресурсами — много эндпоинтов, негибкость. GraphQL решает: вложенные типы, один эндпоинт.

Проверка знаний

Вопрос 1 из 4
Какую типичную проблему REST помогает решить GraphQL?
Что означает over-fetching?
Почему GraphQL может уменьшить число клиентских запросов?
Что часто является обратной стороной гибкости GraphQL?

Вопросы, где были ошибки