О насБлогКонтакты
Веб-разработка28 марта 2026 г. 15 мин 164Обновлено: 22 июня 2026 г.

GraphQL vs REST API: что выбрать для проекта в 2026

AunimedaAunimeda
📋 Содержание

Каждый раз, когда начинается новый проект, возникает один и тот же вопрос: какой подход к API выбрать? REST, GraphQL, или что-то ещё? Я работаю с обоими подходами ежедневно - в нашей команде в Бишкеке мы строили REST API для публичных сервисов, GraphQL для сложных маркетплейсов, и tRPC для TypeScript full-stack приложений. Это статья не про академические определения - это разбор того, что реально работает в продакшне.

Коротко для тех, кто торопится: REST по умолчанию, GraphQL когда данные сложные и вложенные, tRPC когда весь стек на TypeScript. Остальное - детали, но именно в деталях кроется разница между проектом, который приятно поддерживать, и тем, от которого хочется уволиться.


REST API: фундамент, на котором держится интернет

REST (Representational State Transfer) - это не протокол и не стандарт. Это архитектурный стиль, описанный Роем Филдингом в его диссертации 2000 года. Основная идея: сервер предоставляет ресурсы, клиент взаимодействует с ними через стандартные HTTP методы.

Ресурсы и HTTP методы

В REST всё вращается вокруг концепции ресурса. Пользователь - это ресурс. Заказ - это ресурс. Статья в блоге - это ресурс. Каждый ресурс имеет свой URL, и взаимодействие с ним происходит через HTTP методы:

GET    /users          - получить список пользователей
GET    /users/42       - получить пользователя с ID 42
POST   /users          - создать нового пользователя
PUT    /users/42       - обновить пользователя полностью
PATCH  /users/42       - обновить пользователя частично
DELETE /users/42       - удалить пользователя

Это интуитивно понятно. Любой разработчик, видящий такой API впервые, сразу понимает что происходит. В этом огромная сила REST - предсказуемость и конвенции.

HTTP статус-коды

REST активно использует HTTP статус-коды для передачи смысла ответа:

200 OK              - успешный запрос
201 Created         - ресурс создан
204 No Content      - успешно, но нечего возвращать (часто при DELETE)
400 Bad Request     - клиент отправил некорректные данные
401 Unauthorized    - нужна аутентификация
403 Forbidden       - аутентифицирован, но нет прав
404 Not Found       - ресурс не найден
409 Conflict        - конфликт (например, email уже занят)
422 Unprocessable Entity - данные получены, но не прошли валидацию
500 Internal Server Error - что-то сломалось на сервере

Клиент может принимать решения на основе статус-кода, не парся тело ответа. Это важно - браузеры, прокси, CDN-серверы всё понимают HTTP статусы.

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

Вот где REST начинает скрипеть. API эволюционирует: вы добавляете поля, удаляете устаревшие, меняете структуру. Как это делать без поломки существующих клиентов?

Традиционный подход - URL-версионирование:

/api/v1/users
/api/v2/users

Звучит просто, но на практике это превращается в кошмар. Через два года у вас три версии API, которые нужно поддерживать одновременно. Бизнес-логика дублируется. Баги фиксятся в одной версии и забываются в другой. Команда тратит 30% времени на поддержку легаси.

Альтернативы - заголовки версий (Accept: application/vnd.api.v2+json) или эволюционное API с только аддитивными изменениями - у каждой свои компромиссы.

Типичный ответ REST API

GET /api/v1/orders/123

{
  "id": 123,
  "status": "pending",
  "total": 15000,
  "currency": "KGS",
  "created_at": "2026-03-15T10:30:00Z",
  "user_id": 42,
  "items": [
    {
      "product_id": 7,
      "quantity": 2,
      "price": 7500
    }
  ]
}

Хотите узнать имя пользователя? Отдельный запрос: GET /api/v1/users/42. Хотите название продукта? Ещё один: GET /api/v1/products/7. Это называется overfetching и underfetching - два классических недостатка REST, которые и привели к созданию GraphQL.


GraphQL: другая философия работы с данными

GraphQL создан в Facebook в 2012 году, опубликован в 2015-м. Задача была конкретная: мобильное приложение Facebook на медленных соединениях делало слишком много запросов и получало слишком много лишних данных.

Один эндпоинт, любые данные

В GraphQL нет множества эндпоинтов. Есть один: POST /graphql. Клиент описывает что именно ему нужно, и получает ровно это - не больше, не меньше.

query {
  order(id: 123) {
    id
    status
    total
    user {
      name
      email
    }
    items {
      quantity
      product {
        name
        imageUrl
      }
    }
  }
}

Один запрос - все нужные данные, никаких лишних полей. Для мобильного приложения на медленном 3G в каком-нибудь отдалённом районе это не просто удобство, это реальная экономия трафика и батареи.

Схема как единый источник истины

GraphQL строго типизирован. Схема описывает все типы данных и операции:

type User {
  id: ID!
  name: String!
  email: String!
  orders: [Order!]!
  createdAt: DateTime!
}

type Order {
  id: ID!
  status: OrderStatus!
  total: Float!
  items: [OrderItem!]!
  user: User!
}

enum OrderStatus {
  PENDING
  CONFIRMED
  SHIPPED
  DELIVERED
  CANCELLED
}

type Query {
  order(id: ID!): Order
  orders(userId: ID!, status: OrderStatus): [Order!]!
  user(id: ID!): User
}

type Mutation {
  createOrder(input: CreateOrderInput!): Order!
  updateOrderStatus(id: ID!, status: OrderStatus!): Order!
}

type Subscription {
  orderStatusChanged(orderId: ID!): Order!
}

Схема - это контракт между фронтендом и бэкендом. GraphQL Playground или Altair позволяют разработчику исследовать API прямо в браузере. Это называется introspection и это мощный инструмент... до момента когда попадает в продакшн. Но об этом позже.

Queries, Mutations, Subscriptions

GraphQL имеет три типа операций:

Query - получение данных (аналог GET в REST):

query GetUserOrders($userId: ID!) {
  user(id: $userId) {
    name
    orders(status: PENDING) {
      id
      total
      items {
        quantity
        product {
          name
        }
      }
    }
  }
}

Mutation - изменение данных (аналог POST/PUT/DELETE):

mutation CreateOrder($input: CreateOrderInput!) {
  createOrder(input: $input) {
    id
    status
    total
  }
}

Subscription - подписка на события в реальном времени через WebSocket:

subscription OnOrderStatusChange($orderId: ID!) {
  orderStatusChanged(orderId: $orderId) {
    id
    status
    updatedAt
  }
}

Сравнение в коде: одна задача двумя способами

Возьмём реальный сценарий: экран профиля пользователя в мобильном приложении, где нужно показать имя, аватар, последние 5 заказов с товарами и общий бонусный баланс.

REST подход

// Нужно сделать минимум 3 запроса

// 1. Получить профиль пользователя
const user = await fetch('/api/v1/users/me');
// Возвращает: id, name, email, avatar, phone, address,
//             createdAt, updatedAt, role, preferences...
// Нам нужно: id, name, avatar

// 2. Получить заказы
const orders = await fetch('/api/v1/users/me/orders?limit=5&sort=created_at:desc');
// Возвращает: массив заказов с product_id, но без названий продуктов

// 3. Получить бонусный баланс
const balance = await fetch('/api/v1/users/me/loyalty-points');

// 4. Для каждого заказа - отдельный запрос за продуктами (N+1!)
const ordersWithProducts = await Promise.all(
  orders.map(order =>
    Promise.all(
      order.items.map(item => fetch(`/api/v1/products/${item.product_id}`))
    )
  )
);

Итого: как минимум 3 запроса, а если заказов 5 и в каждом 3 товара - это уже 3 + 5×3 = 18 запросов. На медленном соединении это несколько секунд ожидания.

Справедливости ради: хороший REST API спроектировал бы эндпоинт /api/v1/users/me/profile который возвращал бы всё нужное сразу. Но тогда вы начинаете создавать специализированные эндпоинты под каждый экран - и теряете преимущества "ресурсной" модели REST.

GraphQL подход

const { data } = await graphqlClient.query({
  query: gql`
    query UserProfile {
      me {
        id
        name
        avatar
        loyaltyPoints {
          balance
        }
        orders(limit: 5, sortBy: CREATED_AT_DESC) {
          id
          status
          total
          createdAt
          items {
            quantity
            product {
              id
              name
              imageUrl
            }
          }
        }
      }
    }
  `
});

// Один запрос. Ровно те данные которые нужны. Всё.

Один HTTP запрос. Только нужные поля. Клиент описал граф данных - сервер его вернул.


Когда GraphQL реально выигрывает

1. Мобильные приложения с ограниченным трафиком

Это исходная задача, для которой GraphQL создавался. Мобильные клиенты часто работают на нестабильном соединении. Возможность запросить ровно нужные поля и получить вложенные данные за один запрос - это не просто удобство, это UX.

Реальные числа: при переходе с REST на GraphQL в одном из наших проектов количество API запросов при загрузке главного экрана сократилось с 7 до 1. Размер передаваемых данных - с 34KB до 8KB.

2. Сложные вложенные данные

Маркетплейс, социальная сеть, ERP-система - там где данные глубоко связаны, GraphQL даёт огромное преимущество. Когда у вас есть пользователи, у которых есть заказы, у которых есть товары, у которых есть категории и отзывы - REST превращается в серию запросов, GraphQL решает это элегантно.

3. Быстрая итерация на фронтенде

Дизайнер попросил добавить поле "последнее место доставки" в карточку пользователя. В REST нужно идти к бэкенд-разработчику, он модифицирует эндпоинт, деплоит, фронтенд обновляет код. В GraphQL: если поле уже есть в схеме, фронтенд просто добавляет его в запрос - никаких изменений на бэкенде.

Это критично для стартапов и продуктов в активной фазе разработки, где требования меняются еженедельно.

4. Реал-тайм через Subscriptions

// Клиент подписывается на статус заказа
const subscription = client.subscribe({
  query: gql`
    subscription {
      orderStatusChanged(orderId: "123") {
        status
        estimatedDelivery
        trackingNumber
      }
    }
  `
});

subscription.subscribe(({ data }) => {
  updateOrderUI(data.orderStatusChanged);
});

WebSocket соединение, типизированные события, интеграция в ту же схему - всё из коробки.


Когда REST выигрывает (и это чаще, чем кажется)

1. Простые CRUD API

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

GET  /products        - список
POST /products        - создать
PUT  /products/:id    - обновить
DELETE /products/:id  - удалить

Это понятно, документируется автоматически через Swagger/OpenAPI, тестируется в Postman, потребляется любым HTTP клиентом на любом языке.

2. Публичные API

Если вы делаете API для сторонних разработчиков - партнёров, интеграторов, внешних сервисов - REST значительно проще для них. GraphQL требует понимания схемы, query language, специфических клиентов. REST-эндпоинт можно вызвать через curl в одну строку.

Stripe, Twilio, GitHub (старое API) - все успешные публичные API строились на REST. GitHub добавил GraphQL API v4, но v3 REST используется гораздо шире.

3. Загрузка файлов

GraphQL и файлы - это неловкое знакомство. GraphQL работает с JSON, файлы в JSON не кладут. Есть спецификация multipart uploads для GraphQL, но это хак поверх протокола. С REST всё просто:

// REST - элегантно
const formData = new FormData();
formData.append('file', file);
formData.append('userId', userId);

await fetch('/api/v1/avatars', {
  method: 'POST',
  body: formData
});

Если ваше приложение активно работает с файлами - аватары, документы, медиа - оставьте эти эндпоинты на REST.

4. Кэширование

HTTP кэширование - мощный механизм, который отлично работает с REST. Cache-Control, ETag, Last-Modified - браузеры, CDN-серверы, прокси всё это понимают нативно.

GET /products/popular
Cache-Control: public, max-age=3600
ETag: "a8f5f167f44f4964"

Следующий запрос к тому же URL вернёт 304 Not Modified - браузер отдаст из кэша. Нулевой трафик, мгновенный ответ.

GraphQL использует POST для всех операций (включая queries). HTTP кэширование для POST запросов по умолчанию не работает. Есть решения - persisted queries, GET для queries, клиентские кэши вроде Apollo Client - но всё это дополнительная сложность.

Если ваш API активно потребляется через CDN или публичные страницы - REST даст вам кэширование бесплатно.

5. Маленькая команда без GraphQL экспертизы

GraphQL требует инвестиций. Нужно понять схему, resolver'ы, DataLoader, проблему N+1, настройку безопасности. Если у вас команда из двух разработчиков, которые хорошо знают REST - не вводите GraphQL ради моды. Сложность инструмента должна быть оправдана сложностью задачи.


Проблема N+1 в GraphQL и как её решать

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

Как возникает N+1

// Resolver для поля user в Order
const resolvers = {
  Order: {
    user: async (order) => {
      // Этот запрос выполняется для КАЖДОГО заказа отдельно!
      return await User.findById(order.userId);
    }
  }
};

Запрос 100 заказов с полем user вызовет 1 запрос за заказами + 100 запросов за пользователями = 101 запрос к базе данных. Это N+1 проблема.

DataLoader: батчинг запросов

const DataLoader = require('dataloader');

// Создаём loader который батчит запросы
const userLoader = new DataLoader(async (userIds) => {
  // Один запрос за всеми пользователями
  const users = await User.findByIds(userIds);
  // Возвращаем в том же порядке что и входные ID
  return userIds.map(id => users.find(u => u.id === id));
});

const resolvers = {
  Order: {
    user: (order) => {
      // Теперь запросы батчатся: 100 заказов = 1 запрос за всеми пользователями
      return userLoader.load(order.userId);
    }
  }
};

DataLoader собирает все вызовы load() в течение одного tick event loop и выполняет один батч-запрос. 101 запрос превращается в 2.

Важно: DataLoader нужно создавать per-request, не globally. Иначе данные одного пользователя окажутся в кэше для другого.


Пагинация: REST offset vs GraphQL cursor

REST offset пагинация

GET /orders?page=3&limit=20
GET /orders?offset=40&limit=20

Просто, понятно, поддерживается большинством UI библиотек. Проблема: при активных вставках данные "скачут". Пока пользователь листает страницы, в базу добавляются новые записи - и страница 3 теперь показывает частично те же записи что и страница 2.

GraphQL cursor-based пагинация

query {
  orders(first: 20, after: "cursor_xyz") {
    edges {
      node {
        id
        status
        total
      }
      cursor
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}

Cursor - это непрозрачный идентификатор конкретной позиции в наборе данных (обычно base64 от ID или timestamp). Следующая страница запрашивается с after: endCursor - даже если новые записи добавились, вы получите ровно то, что после вашей текущей позиции.

Это правильный подход для бесконечного скролла и real-time данных. Реализация сложнее offset пагинации, но результат предсказуемый.


GraphQL безопасность: то, о чём не пишут в туториалах

GraphQL гибкость - это и уязвимость. Клиент контролирует что запрашивать, а значит может запросить что-то очень тяжёлое.

Атака через вложенность запросов

# Это легально с точки зрения GraphQL, но убьёт сервер
query Evil {
  user(id: "1") {
    friends {
      friends {
        friends {
          friends {
            friends {
              name
            }
          }
        }
      }
    }
  }
}

Ограничение глубины запроса

const depthLimit = require('graphql-depth-limit');

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [depthLimit(5)] // максимальная вложенность - 5 уровней
});

Анализ сложности запросов

const { createComplexityLimitRule } = require('graphql-validation-complexity');

const server = new ApolloServer({
  validationRules: [
    createComplexityLimitRule(1000, {
      scalarCost: 1,
      objectCost: 10,
      listFactor: 10,
    })
  ]
});

Каждое поле имеет стоимость. Запрос с суммарной сложностью выше 1000 отклоняется ещё до выполнения.

Introspection в продакшне

Introspection позволяет клиенту узнать всю схему сервера - все типы, поля, мутации. Это отличный инструмент разработки и потенциальная утечка информации в продакшне.

const server = new ApolloServer({
  introspection: process.env.NODE_ENV !== 'production',
});

Выключите introspection на продакшне. Если ваши фронтенд разработчики работают с production API - дайте им схему отдельным файлом, не через introspection.

Rate limiting

В REST rate limiting тривиален - ограничиваете запросы по IP или токену. В GraphQL один "запрос" может быть как простым query { me { name } }, так и монструозным запросом, вычисляющим граф половины базы данных. Используйте complexity-based rate limiting, а не request-based.


tRPC: современная альтернатива для TypeScript стека

Если весь ваш стек - TypeScript (Next.js фронтенд + Node.js бэкенд), есть третий вариант, который мы используем всё чаще.

tRPC - это не REST и не GraphQL. Это RPC (Remote Procedure Call) с end-to-end типобезопасностью через TypeScript inference. Никакой кодогенерации, никаких схем - типы инферируются автоматически.

// server/router.ts
import { z } from 'zod';
import { router, publicProcedure } from './trpc';

export const orderRouter = router({
  getById: publicProcedure
    .input(z.object({ id: z.string() }))
    .query(async ({ input }) => {
      return await db.order.findUnique({ where: { id: input.id } });
    }),

  create: publicProcedure
    .input(z.object({
      items: z.array(z.object({
        productId: z.string(),
        quantity: z.number().min(1)
      }))
    }))
    .mutation(async ({ input, ctx }) => {
      return await createOrder(ctx.userId, input.items);
    }),
});
// client/components/OrderPage.tsx
const { data: order } = trpc.order.getById.useQuery({ id: orderId });
// order здесь полностью типизирован - IDE подсказывает все поля
// Если поменяете тип на сервере - TypeScript ошибка на клиенте сразу

Никакого GraphQL schema. Никакого REST документирования. Типы живут один раз - на сервере, клиент получает их автоматически через TypeScript.

Когда использовать tRPC

  • Монорепо с фронтендом и бэкендом на TypeScript
  • Команда не имеет нужды делать публичное API
  • Хотите type safety без overhead GraphQL или кодогенерации
  • Next.js + tRPC + Prisma - это мощный стек для продуктивной разработки

Когда tRPC не подходит

  • Публичное API для сторонних клиентов (не-TypeScript потребители)
  • Мобильное приложение на Flutter/Swift/Kotlin
  • Нужна гибкость GraphQL для сложных вложенных данных
  • Команда уже инвестировала в GraphQL экосистему

Что используем мы в Aunimeda

Мы в Бишкеке строим разные типы проектов, и у нас нет религиозной приверженности одному инструменту. Вот наш практический стек:

Next.js API routes + tRPC - для большинства новых проектов, где весь стек на TypeScript. Корпоративные сайты, CRM-системы, платформы управления. Скорость разработки высокая, типобезопасность из коробки, никакой кодогенерации.

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

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

Узнать больше о наших подходах к разработке ПО можно здесь.


Практическое руководство по выбору

Выбирайте REST если:

  • Простое CRUD API без сложных связей
  • Публичное API для сторонних разработчиков
  • Активная работа с файлами
  • Важно HTTP кэширование (публичные страницы, CDN)
  • Маленькая команда без GraphQL опыта
  • Интеграция с legacy системами

Выбирайте GraphQL если:

  • Мобильное приложение с важным контролем трафика
  • Сложная доменная модель (маркетплейс, социальная сеть, ERP)
  • Несколько клиентов с разными потребностями в данных (веб + мобайл + SmartTV)
  • Активная итерация фронтенда без частых бэкенд изменений
  • Real-time функциональность через Subscriptions
  • Команда знает GraphQL и готова управлять его сложностью

Выбирайте tRPC если:

  • Весь стек TypeScript (Next.js или аналог + Node.js)
  • Не нужен публичный API
  • Важна type safety без кодогенерации
  • Монорепо или tight coupling между фронтом и беком

Частые ошибки при выборе

"Мы делаем GraphQL потому что Facebook делает GraphQL." Facebook имеет миллиарды пользователей, сотни клиентских приложений, и тысячи разработчиков. Их проблемы - не ваши проблемы. Выбирайте инструмент под свою задачу, не под технологический хайп.

"REST устарел." REST не устарел. REST превосходно подходит для большинства задач. Сотни миллионов успешных API работают на REST. Технология зрелая, понятная, с огромной экосистемой.

"GraphQL решит нашу проблему производительности." GraphQL не делает API быстрее автоматически. Без DataLoader, без правильного кэширования, без depth/complexity limits - GraphQL API может быть значительно медленнее плохо спроектированного REST API. Производительность - это результат правильного проектирования, не выбора протокола.

"Начнём с GraphQL, потом переделаем если не понравится." Миграция с GraphQL на REST или обратно - это большой рефакторинг. Принимайте решение осознанно в начале.


Итог

GraphQL и REST - это инструменты, каждый со своими сильными сторонами. В 2026 году ответ на вопрос "что выбрать" всё так же зависит от конкретной задачи, а не от того, что модно.

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

GraphQL окупается в конкретных сценариях: сложные данные, мобильные клиенты с чувствительностью к трафику, несколько клиентов с разными потребностями, активная итерация фронтенда. Если это ваш случай - инвестируйте в GraphQL правильно: DataLoader, depth limits, complexity analysis, выключенный introspection в продакшне.

tRPC - отличный выбор для TypeScript full-stack проектов, где не нужен публичный API. Скорость разработки и type safety без overhead GraphQL или REST документирования.

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

Читайте также

Как создать корпоративный сайт: полное руководство 2026aunimeda
Веб-разработка

Как создать корпоративный сайт: полное руководство 2026

Корпоративный сайт - это не визитка. Это инструмент продаж, который работает 24/7. Разбираем, что должно быть на сайте, сколько это стоит и как не выбросить деньги.

Разработка сайтов в Бишкеке - что реально входит в стоимостьaunimeda
Веб-разработка

Разработка сайтов в Бишкеке - что реально входит в стоимость

Почему один сайт стоит 20 000 сом, а другой - 500 000? Разбираем из чего складывается цена на разработку сайтов в Бишкеке и за что реально стоит платить.

Редизайн сайта в Бишкеке: когда пора менять и как не потерять позиции в Googleaunimeda
Веб-разработка

Редизайн сайта в Бишкеке: когда пора менять и как не потерять позиции в Google

Когда редизайн сайта действительно нужен, а когда это пустая трата денег. Как провести редизайн без потери SEO позиций, трафика и клиентов - реальный опыт из практики Бишкек.

Нужна IT-разработка для вашего бизнеса?

Разрабатываем сайты, мобильные приложения и AI-решения для бизнеса в Кыргызстане. Бесплатная консультация.

Разработка сайтов

Получить консультацию Все статьи