Разработка GraphQL API для веб-приложения
GraphQL — язык запросов для API и runtime для их выполнения. Клиент запрашивает ровно те поля, которые нужны, и получает именно их — не больше, не меньше. Это решает две проблемы REST: over-fetching (лишние данные) и under-fetching (несколько запросов для одного экрана).
Основные концепции
Schema-first: API определяется через типы:
type Article {
id: ID!
title: String!
body: String!
author: User!
tags: [Tag!]!
createdAt: DateTime!
}
type Query {
article(id: ID!): Article
articles(filter: ArticleFilter, page: Int, limit: Int): ArticleConnection!
}
type Mutation {
createArticle(input: CreateArticleInput!): Article!
updateArticle(id: ID!, input: UpdateArticleInput!): Article!
}
type Subscription {
articleUpdated(id: ID!): Article!
}
Запросы и фрагменты
# Клиент запрашивает только нужные поля
query ArticlePage($id: ID!) {
article(id: $id) {
title
body
author {
name
avatar
}
tags { name, slug }
}
}
# Переиспользуемые фрагменты
fragment ArticleCard on Article {
id, title, slug
author { name }
createdAt
}
query ArticleList {
articles(limit: 10) {
nodes { ...ArticleCard }
pageInfo { hasNextPage, endCursor }
}
}
N+1 проблема и DataLoader
Главная техническая проблема GraphQL — N+1 запросы. Для списка из 20 статей с полем author будет 1 + 20 = 21 SQL-запрос.
Решение — DataLoader (Facebook, порты для всех языков):
const userLoader = new DataLoader(async (userIds: readonly string[]) => {
const users = await db.user.findMany({
where: { id: { in: [...userIds] } }
});
return userIds.map(id => users.find(u => u.id === id));
});
// В resolver
const articleResolver = {
author: (article, _, { loaders }) => loaders.user.load(article.authorId),
};
// Теперь: 1 запрос за статьями + 1 батч-запрос за всеми авторами
Реализация (Node.js + Apollo Server + Prisma)
import { ApolloServer } from '@apollo/server';
import { makeExecutableSchema } from '@graphql-tools/schema';
const typeDefs = gql`...`;
const resolvers = {
Query: {
article: async (_, { id }, { db }) =>
db.article.findUnique({ where: { id } }),
articles: async (_, { filter, page = 1, limit = 20 }, { db }) =>
db.article.findMany({
where: filter ? { status: filter.status } : undefined,
skip: (page - 1) * limit,
take: limit,
}),
},
Mutation: {
createArticle: async (_, { input }, { db, user }) => {
if (!user) throw new GraphQLError('Unauthorized', {
extensions: { code: 'UNAUTHENTICATED' }
});
return db.article.create({ data: { ...input, authorId: user.id } });
},
},
};
const server = new ApolloServer({ schema: makeExecutableSchema({ typeDefs, resolvers }) });
Авторизация на уровне резолверов
import { shield, rule, and } from 'graphql-shield';
const isAuthenticated = rule()((_, __, ctx) => !!ctx.user);
const isOwner = rule()(async (_, { id }, ctx) => {
const article = await ctx.db.article.findUnique({ where: { id } });
return article.authorId === ctx.user?.id;
});
const permissions = shield({
Mutation: {
createArticle: isAuthenticated,
updateArticle: and(isAuthenticated, isOwner),
},
});
Подписки (Subscriptions)
subscription CommentAdded($articleId: ID!) {
commentAdded(articleId: $articleId) {
id, body, author { name }
}
}
Реализация через WebSocket (graphql-ws) + Redis Pub/Sub для масштабирования на несколько инстансов.
Persisted Queries
Для production-приложений: клиент отправляет hash запроса вместо полного текста. Уменьшает трафик и позволяет кэшировать на CDN.
Когда выбирать GraphQL
GraphQL выгоден при:
- Разные клиенты (web, mobile, TVapps) с разными потребностями в данных
- Сложная вложенная структура данных
- Частые изменения требований к полям (клиент сам управляет данными)
- Публичный API для сторонних разработчиков
REST лучше: простые CRUD-операции, файловые API, streaming.
Сроки
GraphQL API (10–20 типов, queries + mutations, DataLoader, авторизация): 2–4 недели. С subscriptions, persisted queries, federation (micro-services): 1–2 месяца.







