Навигация

GraphQL vs REST: когда что выбрать

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

Содержание

  1. Что такое REST
  2. Что такое GraphQL
  3. Примеры кода
  4. Сравнение GraphQL и REST
  5. Плюсы и минусы
  6. Сложность для разработчиков
  7. Когда что использовать
  8. Компромиссные решения
  9. Ключевые тезисы для интервью
  10. Заключение
  11. Полезные ссылки

Что такое REST

REST (Representational State Transfer) — архитектурный стиль для распределённых систем. Основные принципы:

  • Ресурсы — объекты с URI (/users/123)
  • HTTP-методы — GET, POST, PUT, DELETE
  • Stateless — каждый запрос независим
  • Стандартные коды ответов — 200, 404, 500

Пример REST API

GET    /api/users          # Получить всех пользователей
GET    /api/users/123      # Получить пользователя по ID
POST   /api/users          # Создать пользователя
PUT    /api/users/123      # Обновить пользователя
DELETE /api/users/123      # Удалить пользователя

Проблемы REST

Over-fetching — получаешь больше данных, чем нужно:

// Запрос: GET /api/users/123
// Ответ содержит ВСЕ поля, даже те, что не нужны:
{
  "id": 123,
  "name": "John",
  "email": "john@example.com",
  "password": "***",        // Не нужен на фронте
  "createdAt": "...",       // Не нужен на фронте
  "internalData": "..."     // Не нужен на фронте
}

Under-fetching — нужно делать несколько запросов:

// Получить пользователя и его посты
const user = await fetch('/api/users/123');
const posts = await fetch('/api/users/123/posts');
const comments = await fetch('/api/posts/456/comments');
// 3 запроса вместо одного

Что такое GraphQL

GraphQL — язык запросов для API, созданный Facebook в 2012 году (открыт в 2015). Позволяет клиенту запрашивать именно те данные, которые нужны.

Основные концепции

1. Схема (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!]!
}

type Comment {
  id: ID!
  text: String!
  author: User!
}

# Доступные запросы
type Query {
  users: [User!]!
  user(id: ID!): User
  posts: [Post!]!
}

# Доступные мутации (изменения)
type Mutation {
  createUser(name: String!, email: String!): User!
  createPost(title: String!, content: String!, authorId: ID!): Post!
}

# Подписки (real-time)
type Subscription {
  postCreated: Post!
}

2. Запросы (Queries)

Клиент сам определяет структуру ответа:

# Запрос
query {
  user(id: "123") {
    name
    email
    posts {
      title
    }
  }
}

# Ответ — ТОЛЬКО запрошенные поля
{
  "data": {
    "user": {
      "name": "John",
      "email": "john@example.com",
      "posts": [
        { "title": "First Post" },
        { "title": "Second Post" }
      ]
    }
  }
}

3. Мутации (Mutations)

Операции для изменения данных:

# Мутация с переменными
mutation CreateUser($name: String!, $email: String!) {
  createUser(name: $name, email: $email) {
    id
    name
    email
  }
}

# Переменные
{
  "name": "John",
  "email": "john@example.com"
}

4. Подписки (Subscriptions)

Real-time обновления через WebSocket:

subscription {
  postCreated {
    id
    title
    author {
      name
    }
  }
}

Примеры кода

Backend (Node.js + Apollo Server)

import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';

// Схема
const typeDefs = `#graphql
  type User {
    id: ID!
    name: String!
    email: String!
    posts: [Post!]!
  }

  type Post {
    id: ID!
    title: String!
    content: String!
    author: User!
  }

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

  type Mutation {
    createUser(name: String!, email: String!): User!
  }

  type Subscription {
    userCreated: User!
  }
`;

// Резолверы — как получать данные
const resolvers = {
  Query: {
    users: async () => {
      return await db.users.findAll();
    },
    user: async (_, { id }) => {
      return await db.users.findById(id);
    },
  },

  Mutation: {
    createUser: async (_, { name, email }) => {
      const user = await db.users.create({ name, email });
      // Публикуем событие для подписок
      pubsub.publish('USER_CREATED', { userCreated: user });
      return user;
    },
  },

  // Резолверы для вложенных полей
  User: {
    posts: async (user) => {
      return await db.posts.findByAuthor(user.id);
    },
  },

  Subscription: {
    userCreated: {
      subscribe: () => pubsub.asyncIterator(['USER_CREATED']),
    },
  },
};

const server = new ApolloServer({ typeDefs, resolvers });

const { url } = await startStandaloneServer(server, { listen: { port: 4000 } });
console.log(`Server ready at ${url}`);

Frontend (React + Apollo Client)

Настройка клиента

import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';

const client = new ApolloClient({
  uri: 'http://localhost:4000/graphql',
  cache: new InMemoryCache(),
});

function App() {
  return (
    <ApolloProvider client={client}>
      <Users />
    </ApolloProvider>
  );
}

Запросы (useQuery)

import { gql, useQuery } from '@apollo/client';

const GET_USERS = gql`
  query GetUsers {
    users {
      id
      name
      email
      posts {
        id
        title
      }
    }
  }
`;

function Users() {
  const { loading, error, data } = useQuery(GET_USERS);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;

  return (
    <ul>
      {data.users.map(user => (
        <li key={user.id}>
          <h3>{user.name}</h3>
          <p>{user.email}</p>
          <h4>Posts:</h4>
          <ul>
            {user.posts.map(post => (
              <li key={post.id}>{post.title}</li>
            ))}
          </ul>
        </li>
      ))}
    </ul>
  );
}

Запросы с переменными

const GET_USER = gql`
  query GetUser($userId: ID!) {
    user(id: $userId) {
      id
      name
      email
    }
  }
`;

function UserProfile({ userId }) {
  const { loading, error, data } = useQuery(GET_USER, {
    variables: { userId },
  });

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;

  return (
    <div>
      <h2>{data.user.name}</h2>
      <p>{data.user.email}</p>
    </div>
  );
}

Мутации (useMutation)

import { gql, useMutation } from '@apollo/client';

const CREATE_USER = gql`
  mutation CreateUser($name: String!, $email: String!) {
    createUser(name: $name, email: $email) {
      id
      name
      email
    }
  }
`;

function CreateUserForm() {
  const [createUser, { loading, error }] = useMutation(CREATE_USER);

  const handleSubmit = async (e) => {
    e.preventDefault();
    const formData = new FormData(e.target);

    try {
      const { data } = await createUser({
        variables: {
          name: formData.get('name'),
          email: formData.get('email'),
        },
      });
      console.log('User created:', data.createUser);
    } catch (err) {
      console.error('Error:', err);
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input name="name" placeholder="Name" required />
      <input name="email" type="email" placeholder="Email" required />
      <button type="submit" disabled={loading}>
        {loading ? 'Creating...' : 'Create User'}
      </button>
      {error && <p>Error: {error.message}</p>}
    </form>
  );
}

Подписки (useSubscription)

import { gql, useSubscription } from '@apollo/client';

const USER_CREATED = gql`
  subscription UserCreated {
    userCreated {
      id
      name
      email
    }
  }
`;

function NewUsersNotification() {
  const { data, loading } = useSubscription(USER_CREATED);

  if (loading) return <p>Listening for new users...</p>;

  return (
    <div>
      <h3>New user registered!</h3>
      <p>{data.userCreated.name} ({data.userCreated.email})</p>
    </div>
  );
}

Ленивые запросы (useLazyQuery)

import { gql, useLazyQuery } from '@apollo/client';

const SEARCH_USERS = gql`
  query SearchUsers($query: String!) {
    searchUsers(query: $query) {
      id
      name
    }
  }
`;

function SearchUsers() {
  const [searchUsers, { data, loading }] = useLazyQuery(SEARCH_USERS);

  const handleSearch = (query) => {
    searchUsers({ variables: { query } });
  };

  return (
    <div>
      <input onChange={(e) => handleSearch(e.target.value)} />
      {loading && <p>Searching...</p>}
      {data?.searchUsers.map(user => (
        <div key={user.id}>{user.name}</div>
      ))}
    </div>
  );
}

Сравнение GraphQL и REST

КритерийGraphQLREST
EndpointsОдин (/graphql)Много (/users, /posts)
Получение данныхКлиент выбирает структуруСервер определяет структуру
Over-fetchingНетДа
Under-fetchingНетДа (N запросов)
КэшированиеСложнее (нужен Apollo Client)Из коробки (HTTP-кэш)
ВерсионированиеНе нужно/v1/users, /v2/users
ДокументацияАвтосгенерированная из схемыSwagger/OpenAPI вручную
Real-timeSubscriptions из коробкиWebSocket отдельно
ФайлыНе поддерживает (нужен multipart)Поддерживает
Порог входаВышеНиже
ТипизацияВстроена в схемуВручную (TypeScript)
ОтладкаGraphiQL/PlaygroundDevTools, URL

Плюсы и минусы

GraphQL

Плюсы:

  • Запрашиваешь ровно то, что нужно
  • Один запрос вместо нескольких
  • Автодокументация и автокомплит (GraphiQL)
  • Изменения без версионирования
  • Строгая типизация из коробки
  • Real-time подписки
  • Меньше round-trips к серверу

Минусы:

  • Сложность кэширования
  • Проблема N+1 (нужен DataLoader)
  • Высокий порог входа
  • Не поддерживает загрузку файлов нативно
  • Сложные запросы могут нагружать сервер
  • Меньше готовых инструментов для мониторинга

REST

Плюсы:

  • Простота и зрелость
  • HTTP-кэширование из коробки
  • Поддержка файлов
  • Низкий порог входа
  • Широкая экосистема инструментов
  • Легко масштабировать

Минусы:

  • Over-fetching и under-fetching
  • Множество endpoints
  • Нужна версионность
  • Документация вручную
  • Больше round-trips для сложных данных

Сложность для разработчиков

Frontend-разработчик

ЗадачаRESTGraphQL
Получение данныхНесколько fetch/axios вызововОдин запрос с нужными полями
Изменение данныхЖдать новый endpointСам добавляешь поля в запрос
ТипизацияВручную (TypeScript)Автогенерация из схемы
КэшированиеПростое (HTTP)Нужен Apollo Client / URQL
ОтладкаDevTools, URLGraphiQL/Playground
Порог входаНизкийСредний

Пример: Frontend-разработчику нужны имя пользователя и количество его постов.

REST:

// Нужно запросить весь объект пользователя
const user = await fetch('/api/users/123');
// Получить все посты
const posts = await fetch('/api/users/123/posts');
// Посчитать количество на клиенте
const postCount = posts.length;

GraphQL:

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

Backend-разработчик

ЗадачаRESTGraphQL
Создание endpointПростой CRUDНужна схема + резолверы
Защита данныхНа уровне endpointНа уровне полей (directive)
ОптимизацияПростаяПроблема N+1 (нужен DataLoader)
ВалидацияРучная или middlewareВстроена в схему
Версионирование/v1/, /v2/Не нужно
ДокументацияSwagger вручнуюАвто из схемы
Порог входаНизкийВысокий

Пример: Backend-разработчику нужно добавить новое поле avatar для пользователя.

REST:

// Нужно изменить endpoint, возможно создать новую версию
// GET /api/v2/users/123

GraphQL:

# Просто добавить поле в схему
type User {
  id: ID!
  name: String!
  avatar: String  # Новое поле
}

# Резолвер
User: {
  avatar: (user) => getAvatarUrl(user.id)
}

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

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

  • Простой CRUD API
  • Публичное API для третьих сторон
  • Нужна загрузка файлов
  • Команда не знакома с GraphQL
  • Простые сценарии использования
  • Важно HTTP-кэширование

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

  • Сложный UI с вложенными данными
  • Мобильные приложения (экономия трафика)
  • Нужна быстрая итерация без ожидания бэкенда
  • Real-time функциональность
  • Несколько клиентов с разными потребностями (web, mobile, tablet)
  • Команда готова к learning curve

Компромиссные решения

BFF (Backend for Frontend)

Отдельный слой между frontend и backend, который агрегирует данные:

[Mobile App] --> [Mobile BFF] --> [Backend Services]
[Web App]    --> [Web BFF]    --> [Backend Services]

REST + GraphQL

Использовать REST для простых операций, GraphQL для сложных:

// Простые операции через REST
POST /api/auth/login
GET  /api/files/download

// Сложные данные через GraphQL
query {
  dashboard {
    user { name }
    recentPosts { title }
    notifications { message }
  }
}

Ключевые тезисы для интервью

  • REST — архитектурный стиль с ресурсами, HTTP-методами и stateless-запросами.
  • GraphQL — язык запросов с единым endpoint, клиент сам определяет структуру ответа.
  • Over-fetching — получение лишних данных, under-fetching — необходимость нескольких запросов. GraphQL решает обе проблемы.
  • Схема GraphQL определяет типы данных, query, mutation и subscription.
  • Резолверы — функции, которые загружают данные для каждого поля схемы.
  • Apollo Client предоставляет хуки useQuery, useMutation, useSubscription и useLazyQuery.
  • GraphQL требует решения проблемы N+1 через DataLoader.
  • Кэширование в GraphQL сложнее, чем в REST, где работает HTTP-кэш.
  • REST подходит для простых CRUD API, GraphQL — для сложных UI с вложенными данными.
  • BFF — паттерн отдельного слоя агрегации данных для каждого клиента.
  • Компромисс: REST для простых операций, GraphQL для сложных запросов.

Заключение

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

REST — проверенное временем решение для простых API с широкой экосистемой инструментов.

Выбор зависит от:

  • Сложности приложения
  • Размера команды
  • Требований к производительности
  • Готовности к learning curve

Для большинства современных web-приложений с сложным UI GraphQL становится предпочтительным выбором, особенно если команда готова инвестировать в обучение.

Полезные ссылки