Главная / Статьи

REST против GraphQL: реальные компромиссы каждой архитектуры

Объясняются конкретные проблемы, которые решают REST и GraphQL, их внутренняя работа, а также скрытые компромиссы, которые необходимо учитывать перед выбором одного из них для вашего API.

Издатель: REACTAPP.TOP10 мин чтения2224 слов

Источники: REST vs GraphQL: Which Problem Does Each One Actually Solve?

Когда вы начинаете разрабатывать способ взаимодействия клиента и сервера, почти всегда возникает один вопрос: REST или GraphQL?

Дискуссии на эту тему часто принимают форму соревнования, будто одна из сторон обязана победить. Некоторые разработчики утверждают, что REST уже устарел и его следует заменить. Другие считают, что GraphQL добавляет слои сложности, которые не нужны большинству проектов. Ни один из таких подходов не является особенно полезным.

Технологии создаются не для того, чтобы набирать очки в спорах. Они существуют для решения конкретных проблем, с которыми сталкиваются разработчики в определенных обстоятельствах.

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

Чтобы принять правильное решение, полезно отбросить шум и рассмотреть основные мотивы: что предназначалось исправить каждым подходом, как он работает внутри и какие компромиссы с ним связаны.

Какую проблему решил REST?

До того как REST стал стандартом, веб-сервисы в значительной степени опирались на протоколы вроде SOAP и XML-RPC. Они требовали строгих схем XML, объёмных оболочек сообщений и сложной настройки. Клиентам необходимо было знать точные названия удалённых процедур, чтобы получить данные.

В 2000 году Рой Филдинг представил REST (Representational State Transfer).

В своей сути REST предложил простую, но эффективную концепцию: использовать собственную архитектуру веба.

Вместо того чтобы рассматривать сервер как непрозрачный механизм выполнения функций, REST моделирует систему как совокупность ресурсов, каждый из которых доступен по четкому URL. Действия над этими ресурсами выполняются с помощью стандартных HTTP-глаголов (GET, POST, PUT, DELETE), а результат каждого запроса сообщается с помощью знакомых кодов состояния HTTP, таких как 200 OK для успешного выполнения, 404 Not Found при отсутствии ресурса или 500 Server Error при сбое на сервере.

Resource-Oriented Architecture (REST)
Client ─── GET /api/users/12 ──────────> Server (Find user #12)
Client <── 200 OK [ { user data } ] ──── Server
Client ─── DELETE /api/posts/99 ───────> Server (Delete post #99)
Client <── 204 No Content ───────────── Server

Почему важен REST

  • Простота и предсказуемость: Как только вы поймете, что /api/products относится к коллекции продуктов, а /api/products/42 — к конкретному элементу, интерфейс становится самоочевидным.
  • Встроенное кэширование HTTP: Поскольку запросы GET являются идемпотентными и соответствуют уникальным URL, инфраструктура вроде браузеров, CDN (например, Cloudflare) и обратных прокси (таких как Nginx) может автоматически кэшировать ответы без необходимости дополнительной логики приложения.
  • Разделение циклов жизни: Команды, работающие с бэкендом, могут свободно изменять внутренние схемы баз данных, при условии сохранения целостности публичного HTTP-интерфейса.
  • Эта модель долгое время хорошо служила разработчикам. Однако по мере усложнения клиентских приложений начали проявляться определенные проблемы.

    Новые проблемы: чрезмерное и недостаточное получение данных

    Представьте мобильное приложение, отображающее экран профиля пользователя. Для этого экрана требуются ровно три элемента:

    • Имя пользователя
    • URL, ведущий к аватару пользователя
    • Названия трех самых недавних публикаций пользователя в блоге

    При использовании традиционного REST API для удовлетворения этой незначительной требования часто возникают две отдельные проблемы.

    1. Чрезмерный сбор данных

    Клиент запрашивает GET /api/users/12.

    Сервер возвращает полную информацию о пользователе:

    {
      "id": 12,
      "name": "Sarah Connor",
      "avatarUrl": "https://cdn.example.com/sarah.jpg",
      "email": "sarah@example.com",
      "address": { "street": "...", "zip": "...", "country": "..." },
      "billingHistory": [ ... ],
      "preferences": { ... },
      "createdAt": "2021-04-12T08:14:00Z"
    }
    

    На экране требовались только name и avatarUrl, однако пришлось загружать не связанные с этим данные: адрес, информацию о оплате и настройки предпочтений. При медленном сотовом соединении передача ненужных данных тратит заряд батареи и замедляет отображение контента. Такая ситуация называется чрезмерным сбором данных.

    2. Недостаточный сбор данных (проблема N+1 в сети)

    Клиенту всё ещё нужны публикации Сары, но ответ на запрос /api/users/12 их не содержал.

    Поэтому отправляется второй запрос: GET /api/users/12/posts.

    Предположим, что этот конечный пункт возвращает лишь простой список идентификаторов постов. В таком случае клиенту, возможно, придётся совершить три отдельных дополнительных запроса: один для получения данных с /api/posts/101, второй — для получения данных с /api/posts/102, и третий — для получения данных с /api/posts/103.

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

    Какую проблему на самом деле решил GraphQL?

    Примерно в 2012 году Facebook столкнулся именно с этим проблемным моментом при переходе свего флагманского приложения от веб-версий для мобильных устройств к нативным приложениям. Соединения мобильных устройств были ненадёжными и медленными, а интерфейс Facebook состоял из тесно связанных между собой данных — пользователей, друзей, комментариев, лайков и реакций.

    Разработка отдельного REST-эндпоинта для каждого экрана, вроде /api/mobile-profile-screen, быстро превратилась в неуправляемую практику.

    Чтобы решить эту проблему, Facebook создал GraphQL.

    GraphQL — это не технология баз данных. Это язык запросов для API, сочетающийся с движком на стороне сервера, который выполняет запросы с учетом системы типов, определенной вами.

    Вместо множества запросов к разным эндпоинтам клиент отправляет один запрос POST с структурированным запросом, в котором четко указано, какой формат данных ему нужен. Сервер затем возвращает JSON-пакет, полностью соответствующий этому формату.

    Подход GraphQL

    Вот как тот же экран профиля может запросить свои данные с использованием GraphQL:

    GraphQL

    query GetUserProfile {
      user(id: "12") {
        name
        avatarUrl
        posts(limit: 3) {
          title
        }
      }
    }
    

    А сервер возвращает именно то, о чем был запрос, ничего больше:

    JSON

    {
      "data": {
        "user": {
          "name": "Sarah Connor",
          "avatarUrl": "https://cdn.example.com/sarah.jpg",
          "posts": [
            { "title": "Building Reliable Systems" },
            { "title": "Understanding Memory Leaks" },
            { "title": "A Guide to Clean Architecture" }
          ]
        }
      }
    }
    

    Не происходит траты пропускной способности на ненужные поля, и нет необходимости во втором или третьем запросе для получения связанных данных. Один сетевой вызов передает именно те данные, которые необходимы для отображения.

    Взгляд внутрь

    Чтобы полностью понять существующие компромиссы, полезно рассмотреть, как каждый подход обрабатывает выполнение на стороне сервера.

    Модель REST на основе конечных точек

    В фреймворке вроде Express.js маршруты напрямую соотносят входящие URL с функциями-обработчиками:

    JavaScript

    // Express.js REST route
    app.get('/api/users/:id', async (req, res) => {
      const user = await db.users.findById(req.params.id);
      if (!user) return res.status(404).json({ error: 'User not found' });
      res.json(user);
    });
    

    Разработчик бэкенда имеет полный контроль над логикой запросов, вызовами к базе данных и структурой ответа. Оптимизация производительности здесь проста: достаточно обратиться к соответствующему обработчику конечной точки и отредактировать SQL-запрос или внедрить кэширование там.

    Модель GraphQL с схемой и резолвером

    GraphQL основан на схеме, а также на отдельных функциях, называемых резолверами:

    JavaScript

    // Type definitions
    const typeDefs = `
      type Post {
        id: ID!
        title: String!
      }
    
      type User {
        id: ID!
        name: String!
        avatarUrl: String!
        posts: [Post]
      }
    
      type Query {
        user(id: ID!): User
      }
    `;
    
    // Resolvers map fields to actual data sources
    const resolvers = {
      Query: {
        user: async (_, { id }) => {
          return await db.users.findById(id);
        },
      },
      User: {
        posts: async (parentUser) => {
          // Runs when the client query includes the 'posts' field
          return await db.posts.findByAuthorId(parentUser.id);
        },
      },
    };
    

    Когда поступает запрос, движок GraphQL сравнивает его со схемой, затем проходит по резолверам, выполняя их в виде деревоподобной структуры.

    Это даёт командам фронтенда большую свободу в формировании запросов. Однако есть важный нюанс: клиент определяет форму запроса, но именно сервер должен нести расходы на его обработку.

    Как GraphQL влияет на вас за кулисами

    Такая гибкость не бесплатна. Переход с REST на GraphQL фактически переносит сложность с клиента на сервер.

    Проблема запросов N+1 на уровне базы данных

    Рассмотрим снова описанную выше настройку резолверов. Что произойдет, если клиент попросит список из 20 пользователей вместе с их постами?

    GraphQL

    query {
      users {
        name
        posts {
          title
        }
      }
    }
    
    • Резолвер users выполняется один раз, загружая все 20 записей сразу (SELECT * FROM users).
    • Затем резолвер posts запускается отдельно для каждого отдельного пользователя, возвращенного ранее.
    • Итоговый результат: база данных выполняет 1 запрос для получения списка пользователей плюс еще 20 запросов для постов — в общей сложности 21 запрос.

    В REST-архитектуре это обычно объединяется в один SQL-запрос с использованием оператора JOIN в коде эндпоинта (SELECT * FROM users JOIN posts...). В GraphQL для объединения таких разрозненных запросов требуются инструменты батчинга, такие как DataLoader, которые собирают их перед отправкой в базу данных.

    Проблемы с кэшированием в HTTP

    Рассмотрим запрос в формате REST примерно такой:

    GET /api/articles/42
    

    CDN может с удовольствием хранить этот ответ в кэше, скажем, на 10 минут. Если за это время 5 000 пользователей обратятся по адресу /api/articles/42, ваш сервер-источник должен обработать только первого пользователя — остальные обрабатываетсь CDN.

    GraphQL нарушает эту схему, поскольку практически каждая операция отправляется в формате POST на один единственный конечный пункт — /graphql.

    Поскольку сам запрос находится внутри тела запроса, а не в URL, обычные HTTP-кэши, прокси и CDN не могут определить, что два запроса запрашивают одинаковые данные. Чтобы решить эту проблему, необходим либо сложный нормализованный кэш на стороне клиента (например, Apollo Client или Relay), либо специально созданный шлюз кэширования.

    Глубина запроса как вектор атаки

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

    Однако в GraphQL злоумышленник может отправить что-то вроде этого:

    GraphQL

    query EvilQuery {
      user {
        posts {
          author {
            posts {
              author {
                posts {
                  title
                }
              }
            }
          }
        }
      }
    }
    

    При отсутствии таких мер защиты, как оценка сложности запроса или ограничение глубины, подобный запрос может существенно замедлить работу базы данных или исчерпать доступную память сервера.

    REST и GraphQL рядом друг с другом

    ОсобенностиRESTGraphQLЗагрузка данныхВозвращает данные фиксированной структуры для каждого URLВозвращает именно те поля, которые запрашивает клиентСетевые вызовыЧасто требуют нескольких итерацийОбычно решаются за один запросКэширование HTTPВстроено, просто и эффективноТрудно реализовать; требуется дополнительное оборудование на стороне клиента или шлюзаУровень сложностиНизкий, основан на знакомых конвенциях HTTPВысокий, требует понимания схем и резолверовОтчеты об ошибкахСтандартные коды состояния HTTP (404, 500, 401)Почти всегда 200 OK, причины проблем указаны в поле errorsЗагрузка файловПросто через данные формы типа multipartТребуется пользовательская логика мутации или заранее подписанные URL для загрузкиИдеальные сценарииСлужбы в стиле CRUD, микросервисы, открытые веб-APIБогатые интерфейсы и приложения для нескольких платформ

    Совершение вызова: как принять решение

    Когда REST более подходит

    • Ваша модель ресурсов проста. Для типичных продуктов, основанных на операциях CRUD, таких как панели администратора, дашборды или простые интернет-магазины, REST позволяет ускорить разработку и выпуск продукта.
    • Для вас очень важна кэширование. Для публичного контента, предназначенного в основном для чтения, такого как блоги, новостные страницы или каталоги, встроенная кэширование по протоколу HTTP через CDN не имеет соперников с точки зрения скорости и стоимости.
    • Вы публикуете API для внешних пользователей. Внешние разработчики уже могут обращаться к конечным точкам REST с помощью curl или fetch, не изучая специальные библиотеки клиентов.

    Когда GraphQL более целесообразен

    • Вы поддерживаете множество различных типов клиентов. Представьте продукт с клиентами для iOS, Android, умных часов и веба — каждый из них нуждается в отдельном подмножестве одних и тех же базовых данных.
  • Ваш домен представляет собой тесно связанную структуру. Это касается ситуаций, когда такие элементы, как пользователи, группы, комментарии, разрешения и медиафайлы постоянно взаимодействуют друг с другом.
  • Фронтенд и бэкенд работают по разным графикам. В крупных командах GraphQL выступает в роли буферного слоя: как только определены резолверы и типы, инженеры фронтенда могут внедрять новые интерфейсы, не дожидаясь от бэкенд-команд добавления новых точек входа при каждом цикле.
  • Заключение

    Ни одна из этих технологий не сделала другую устаревшей.

    • REST решил проблему необходимости единообразного, масштабируемого и подходящего для кэширования способа предоставления ресурсов в Интернете.
    • GraphQL позволяет клиентам формировать собственные запросы к данным, что сокращает количество ненужных передачи данных по медленным или ненадежным мобильным соединениям.

    Выбор между ними не связан с следованием трендам, а заключается в понимании реальных проблем, с которыми сталкивается ваша система. Если вашии главные проблемы — производительность на мобильных устройствах и постоянно растущий список конечных точек, специфичных для фронтенда, то GraphQL может стать эффективным решением. Если для вас наиболее важны простота, надежное кэширование и легко обслуживаемый бэкенд, то REST по-прежнему остается надежным и проверенным вариантом.