Антон Ларичев

Введение
GraphQL — это язык запросов к API и среда выполнения для них, разработанная Facebook в 2012 году и открытая в 2015. В отличие от REST, где клиент получает фиксированный набор данных по URL, GraphQL позволяет клиенту запросить именно те поля, которые ему нужны. Это решает две классические проблемы REST: over-fetching (получение лишних данных) и under-fetching (необходимость нескольких запросов для одной сущности).
В этой статье разберём три кита GraphQL: схемы (Schema), резолверы (Resolvers) и интеграцию с React через Apollo Client. После прочтения вы сможете поднять свой первый сервер и подключить его к фронтенду.
Установка сервера
Начнём с минимального сервера на Apollo Server. Установим зависимости:
npm install @apollo/server graphql
Создадим файл server.js:
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
// Описание схемы на языке SDL
const typeDefs = `#graphql
type Book {
id: ID!
title: String!
author: String!
}
type Query {
books: [Book!]!
book(id: ID!): Book
}
`;
// Тестовые данные
const books = [
{ id: '1', title: 'Чистый код', author: 'Роберт Мартин' },
{ id: '2', title: 'Совершенный код', author: 'Стив Макконнелл' },
];
// Резолверы — функции, возвращающие данные для каждого поля
const resolvers = {
Query: {
books: () => books,
book: (_, { id }) => books.find((b) => b.id === id),
},
};
const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, { listen: { port: 4000 } });
console.log(`Сервер готов: ${url}`);
Схемы и типы
Схема — это контракт между клиентом и сервером. Она описывает, какие данные доступны и какие операции можно выполнять. GraphQL поддерживает скалярные типы (Int, Float, String, Boolean, ID) и пользовательские объекты.
Восклицательный знак (!) означает, что поле не может быть null. Квадратные скобки ([Book!]!) описывают массив, элементы которого тоже не могут быть null.
Помимо Query существует Mutation для изменения данных и Subscription для подписки на события:
type Mutation {
addBook(title: String!, author: String!): Book!
deleteBook(id: ID!): Boolean!
}
type Subscription {
bookAdded: Book!
}
Резолверы
Резолвер — это функция, которая возвращает значение для конкретного поля схемы. Сигнатура у всех резолверов одинаковая:
resolver(parent, args, context, info)
Где:
parent— результат родительского резолвера;args— аргументы, переданные в поле;context— общий объект для запроса (часто содержит юзера и подключение к БД);info— мета-информация о запросе.
Добавим мутацию:
const resolvers = {
Query: {
books: () => books,
},
Mutation: {
addBook: (_, { title, author }) => {
const newBook = { id: String(books.length + 1), title, author };
books.push(newBook);
return newBook;
},
},
};
Если у объекта есть поле, требующее вычисления, добавьте резолвер на уровне типа:
Book: {
// Вычисляемое поле — короткое описание
shortTitle: (parent) => parent.title.slice(0, 20),
},
Интеграция с React
На фронте используем Apollo Client. Устанавливаем пакеты:
npm install @apollo/client graphql
Инициализируем клиент и оборачиваем приложение в провайдер:
import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';
const client = new ApolloClient({
uri: 'http://localhost:4000/',
cache: new InMemoryCache(), // нормализованный кэш по __typename и id
});
export function Root() {
return (
<ApolloProvider client={client}>
<BookList />
</ApolloProvider>
);
}
Запрашиваем данные хуком useQuery:
import { gql, useQuery } from '@apollo/client';
const GET_BOOKS = gql`
query GetBooks {
books {
id
title
author
}
}
`;
export function BookList() {
const { data, loading, error } = useQuery(GET_BOOKS);
if (loading) return <p>Загрузка...</p>;
if (error) return <p>Ошибка: {error.message}</p>;
return (
<ul>
{data.books.map((book) => (
<li key={book.id}>{book.title} — {book.author}</li>
))}
</ul>
);
}
Для мутаций есть хук useMutation. Он возвращает функцию-триггер и состояние выполнения:
const ADD_BOOK = gql`
mutation AddBook($title: String!, $author: String!) {
addBook(title: $title, author: $author) {
id
title
}
}
`;
const [addBook, { loading }] = useMutation(ADD_BOOK, {
// Обновляем список после добавления
refetchQueries: [{ query: GET_BOOKS }],
});
Частые ошибки
1. Проблема N+1. Когда резолвер вложенного поля делает запрос к БД на каждом элементе списка. Решение — батчинг через DataLoader, который собирает запросы за один тик и выполняет их одним SQL.
2. Чрезмерно глубокие запросы. Клиент может запросить user → posts → comments → author → posts... и положить сервер. Ставьте ограничение через graphql-depth-limit или graphql-cost-analysis.
3. Возврат всей сущности из мутации. Apollo Client автоматически обновит кэш только если в ответе мутации присутствует id и поля, которые уже есть в кэше. Не забывайте возвращать id.
4. Отсутствие обработки ошибок на резолверах. Бросайте GraphQLError с понятным code в extensions, чтобы клиент мог корректно среагировать.
5. Использование any в типах. Генерируйте TypeScript-типы из схемы через graphql-codegen — это даст автокомплит и проверку на этапе сборки.
Заключение
GraphQL даёт клиенту контроль над формой данных и снимает с бэкенда задачу по поддержке десятка эндпоинтов под разные экраны. Базовый стек прост: схема описывает контракт, резолверы достают данные, Apollo Client на фронте кэширует ответы и предоставляет хуки.
Для продакшена изучите DataLoader, авторизацию через context, persisted queries и Federation, если у вас несколько сервисов. Начните с маленького сервера, подключите его к React-приложению и постепенно переносите фичи с REST — это самый безболезненный путь миграции.






Комментарии
0