GraphQL или REST: как выбрать архитектуру API для веб- и мобильного продукта
Сравнение GraphQL и REST по делу: лишние запросы и данные, схема и OpenAPI, кэширование и проблема N+1, версионирование, ошибки и безопасность, гибридная архитектура и чек-лист выбора API для проекта.
Выбор между GraphQL и REST делают в начале проекта, а живут с ним годами. От него зависит, сколько запросов сделает мобильное приложение при открытии экрана, как быстро фронтенд получит новые данные без доработки бэкенда, насколько просто кэшировать ответы и защитить API. Универсально правильного ответа нет, но есть понятные критерии, по которым решение принимается за один разговор с командой. В статье — чем подходы различаются по сути, где каждый сильнее, какие проблемы придётся решать при выборе GraphQL, как устроен гибридный вариант и какой набор инструментов актуален сейчас. Коротко о подходах REST — архитектурный стиль, который описал Рой Филдинг в диссертации 2000 года. API строится вокруг ресурсов: у каждого свой адрес, а действия выражаются HTTP-методами. Структуру ответа определяет сервер. GET /api/v1/users/42 { "id": 42, "name": "Иван Петров", "email": "ivan@example.com", "role": "admin", "createdAt": "2025-04-15T10:30:00Z", "preferences": { "language": "ru", "timezone": "Europe/Moscow" } } GraphQL — язык запросов и среда выполнения для API. Его создали в Facebook в 2012 году, открыли в 2015-м, а с 2019 года спецификацию развивает GraphQL Foundation при Linux Foundation. У API обычно одна точка входа, а клиент сам описывает, какие поля и связанные объекты ему нужны. query GetUser($id: ID!) { user(id: $id) { name email role } } { "data": { "user": { "name": "Иван Петров", "email": "ivan@example.com", "role": "ADMIN" } } } Из этого различия — кто решает, какие данные вернуть, — вытекают почти все остальные преимущества и недостатки. Лишние и недостающие данные У REST две типичные проблемы при работе с богатыми интерфейсами. Избыточная выборка. Экрану списка нужны имя и аватар, а эндпоинт возвращает полный объект пользователя с настройками и историей. Для мобильного клиента на медленной сети это лишние килобайты на каждом запросе. Недостаточная выборка. Один эндпоинт не отдаёт всего, что нужно экрану, и клиент делает цепочку запросов: список заказов, затем покупателя и статус доставки для каждого заказа. GET /api/orders?limit=20 → 20 заказов GET /api/customers/101 → покупатель первого заказа GET /api/deliveries/5501 → доставка первого заказа ... → и так для каждого заказа В GraphQL тот же экран получает всё одним запросом: query OrdersScreen { orders(first: 20) { id total status customer { name phone } delivery { status estimatedDate } } } Справедливости ради, в REST эти проблемы тоже решаются: параметрами выбора полей, вложением связанных ресурсов, отдельными эндпоинтами под экраны по паттерну Backend for Frontend. Разница в том, что в REST это нужно проектировать для каждого случая, а в GraphQL гибкость заложена в сам язык. Схема как контракт В GraphQL схема обязательна. Она описывает типы, поля, их обязательность и доступные операции, и сервер не выполнит запрос, который ей не соответствует. type User { id: ID! name: String! email: String! role: UserRole! orders(first: Int = 20, after: String): OrderConnection! } enum UserRole { ADMIN MANAGER CLIENT } type Order { id: ID! total: Money! status: OrderStatus! customer: User! } type Query { user(id: ID!): User orders(status: OrderStatus, first: Int = 20, after: String): OrderConnection! } type Mutation { createOrder(input: CreateOrderInput!): CreateOrderPayload! } Из схемы генерируются типы для TypeScript, клиентские хуки, моки для тестов и документация. Фронтенд и бэкенд могут работать параллельно, согласовав схему заранее, а редактор кода подсказывает поля и подсвечивает ошибки прямо в запросе. REST-API тоже можно описать строго — спецификацией OpenAPI. Актуальная версия 3.2 вышла в сентябре 2025 года и, среди прочего, добавила поддержку потоковых форматов ответа. Из OpenAPI так же генерируются клиенты, типы и документация. Разница в дисциплине: схема GraphQL — неотъемлемая часть сервера, а спецификация OpenAPI может разойтись с реальным поведением API, если её не генерировать из кода и не проверять в CI. Производительность и кэширование Где сильнее REST HTTP-кэширование. GET-запросы к ресурсам кэшируются браузером, CDN и прокси стандартными заголовками. Для публичных данных — каталогов, статей, справочников — это самый дешёвый способ выдержать нагрузку. Предсказуемые запросы. Бэкенд заранее знает, что вернёт каждый эндпоинт, и может оптимизировать запросы к базе данных и индексы именно под него. Простой мониторинг. Задержки и ошибки видны по каждому эндпоинту без дополнительной разметки. Где сильнее GraphQL Один запрос вместо цепочки. Особенно заметно на мобильных сетях с высокой задержкой. Только нужные поля. Меньше трафика, меньше работы на сервере, если резолверы не вычисляют лишнего. Агрегация источников. Один запрос может собрать данные из нескольких сервисов, а клиент об этом не узнает. Проблема N+1 и DataLoader Гибкость GraphQL имеет обратную сторону. Если резолвер поля customer ходит в базу для каждого заказа отдельно, список из 20 заказов порождает 21 запрос. Стандартное решение — DataLoader: он собирает все обращения за один проход выполнения и отправляет их одним пакетом. import DataLoader from 'dataloader'; // Создаётся на каждый входящий запрос, чтобы кэш не смешивал данные разных пользователей export function createLoaders(db) { return { userById: new DataLoader(async (ids) = { const rows = await db.query('SELECT * FROM users WHERE id = ANY($1)', [ids]); const byId = new Map(rows.map((row) = [row.id, row])); return ids.map((id) = byId.get(id) ?? null); // порядок ответа = порядок ключей }), }; } export const resolvers = { Order: { customer: (order, _args, ctx) = ctx.loaders.userById.load(order.customerId), }, }; Кэширование в GraphQL Запросы GraphQL обычно отправляются методом POST на один адрес, поэтому стандартное HTTP-кэширование к ним не применяется. Решения существуют, но требуют настройки: Клиентский нормализованный кэш в Apollo Client, Relay или urql хранит объекты по идентификаторам и обновляет все экраны, где они показаны. Сохранённые запросы (persisted queries, trusted documents): клиент отправляет идентификатор заранее зарегистрированного запроса. Такой запрос можно передать методом GET и закэшировать на CDN, а сервер заодно отклоняет произвольные запросы. Кэш на уровне резолверов и источников данных — в Redis или в памяти. Как устроить кэширование в Redis, мы разбирали в отдельной статье . Версионирование и эволюция API REST-API чаще всего версионируют в адресе ( /api/v1/ , /api/v2/ ) или заголовке. Это понятно клиентам, но заставляет поддерживать несколько версий одновременно, пока все потребители не перейдут, — особенно долго, если среди них мобильные приложения, которые пользователи не обновляют. GraphQL рассчитан на эволюцию одной схемы. Новые поля добавляются без последствий — старые клиенты их просто не запрашивают. Устаревшие поля помечаются директивой и удаляются, когда по метрикам их перестают запрашивать: type User { id: ID! displayName: String! username: String @deprecated(reason: "Используйте displayName") } Условие работы такой схемы — учёт использования полей и автоматическая проверка ломающих изменений в CI, например с помощью GraphQL Inspector или реестра схем. Без этого GraphQL-API ломается так же, как REST, только незаметнее. Ошибки и коды ответа В REST статус ответа — часть контракта: 404 — ресурс не найден, 403 — нет доступа, 422 — ошибка валидации. Мониторинг и прокси понимают это без дополнительной настройки. В GraphQL один запрос может частично выполниться: часть полей вернётся, а для других придут ошибки в массиве errors . Исторически серверы отвечали на такие запросы кодом 200. Спецификация GraphQL over HTTP, рабочий черновик которой опубликован в 2025 году, вводит тип ответа application/graphql-response+json и уточняет, когда сервер должен возвращать коды 4xx и 5xx. В любом случае мониторинг GraphQL должен анализировать тело ответа, а не только HTTP-статус. Практичный подход — разделять технические ошибки (массив errors ) и ожидаемые бизнес-результаты, которые описываются в схеме как часть ответа мутации: «недостаточно средств», «товар закончился». Безопасность: что меняется с GraphQL Базовые угрозы для обоих подходов общие: авторизация на уровне объектов, аутентификация, ограничение потребления ресурсов. GraphQL добавляет свою специфику: Сложные и глубокие запросы. Клиент может запросить вложенные связи на много уровней и нагрузить сервер. Нужны ограничения глубины и расчётной сложности, лимиты на размер списков и таймауты. Интроспекция. Схема раскрывается любому клиенту. Во внешнем API интроспекцию обычно отключают или ограничивают. Авторизация на уровне полей. Проверки только на уровне операции недостаточно: одно чувствительное поле может быть доступно через разные пути в графе. Пакетные запросы. Множество операций в одном HTTP-запросе обходят ограничения частоты, рассчитанные на количество запросов. Полный разбор угроз по OWASP API Security Top 10, включая раздел про GraphQL, — в статье о безопасности API . Сложность разработки REST проще начать: достаточно понимания HTTP, а фреймворки — Express, Fastify, NestJS, FastAPI, Django REST Framework, Laravel — дают готовые решения для маршрутизации, валидации и документации. Инфраструктура логирования, кэширования и мониторинга подходит без изменений. GraphQL требует больше решений на старте: спроектировать схему с учётом всех клиентов, а не одного экрана; выбрать подход: схема первична (SDL-first) или генерируется из кода (code-first); настроить DataLoader и следить за количеством запросов к базе; реализовать авторизацию на уровне полей; настроить ограничения сложности и сохранённые запросы; организовать мониторинг по операциям и полям. Эти вложения окупаются, когда клиентов несколько и интерфейсы часто меняются. Для одного веб-клиента с простыми экранами они часто не окупаются вовсе. Когда выбирать REST Публичный API для партнёров и сторонних разработчиков: REST и OpenAPI знакомы всем, а инструменты интеграции работают из коробки. Простая предметная модель без глубоких связей между сущностями. Много публичных данных, которые выгодно кэшировать на CDN. Вебхуки, загрузка файлов, интеграции с учётными и корпоративными системами. Команда без опыта GraphQL и сжатые сроки первой версии. Когда выбирать GraphQL Несколько клиентов с разными потребностями: веб, iOS, Android, партнёрские кабинеты. Богатая доменная модель со множеством связей: маркетплейс, CRM, личные кабинеты. Фронтенд часто меняется, и команда хочет получать новые комбинации данных без доработки бэкенда. Данные собираются из нескольких сервисов, и клиенту нужен единый граф. Продукт развивается долго, и важна эволюция схемы без выпуска версий. Гибридная архитектура Во многих системах подходы сосуществуют. Внутренние сервисы общаются по REST или gRPC, а для клиентских приложений работает слой GraphQL, который агрегирует их данные. Клиенты не знают о внутреннем устройстве, и сервисы можно менять, не трогая приложения. export const resolvers = { Query: { customerDashboard: async (_parent, { customerId }, ctx) = { const [customer, orders, bonuses] = await Promise.all([ ctx.services.customers.getById(customerId), ctx.services.orders.listRecent(customerId, { limit: 10 }), ctx.services.loyalty.getBalance(customerId), ]); return { customer, orders, bonuses }; }, }, }; При этом партнёрам удобно отдать отдельный REST-API с OpenAPI-документацией, а вебхуки оставить в REST. Когда внутренних сервисов много, для их взаимодействия часто выбирают gRPC — чем он отличается и какие у него ограничения, описано в статье о gRPC и Protocol Buffers . Вопрос о том, делить ли систему на сервисы вообще, разобран в материале «Микросервисы или монолит» . Для крупных систем, где разные команды владеют частями графа, применяют федерацию: каждая команда публикует свой подграф, а шлюз собирает их в единую схему. Решение мощное, но добавляет инфраструктуру и требует реестра схем — рассматривать его стоит, когда графом действительно владеет несколько независимых команд. Инструменты GraphQL Apollo Server и Apollo Client — распространённый стек с подробной документацией и федерацией. GraphQ