Fri Nov 21 2025
GraphQL: полная альтернатива REST для современных API
GraphQL — это язык запросов для API, разработанный Facebook в 2012 году и выпущенный с открытым исходным кодом в 2015. В отличие от REST, GraphQL позволяет клиенту запрашивать только те данные, которые ему нужны, и получать их за один запрос.
В этой статье мы разберём схемы, типы, резолверы, фрагменты, подписки, оптимизацию и лучшие практики использования GraphQL в продакшене.
Основные проблемы REST
Over-fetching (избыточные данные)
# REST: получаем все поля пользователя
GET /api/users/123
# Ответ (клиенту нужно только имя)
{
"id": 123,
"name": "Alice",
"email": "alice@example.com",
"phone": "+1234567890",
"address": {...},
"created_at": "...",
"updated_at": "..."
}
Under-fetching (недостаточно данных)
# REST: нужно несколько запросов для связанных данных
# 1. Получить пользователя
GET /api/users/123
# 2. Получить его заказы
GET /api/users/123/orders
# 3. Получить детали каждого заказа
GET /api/orders/1
GET /api/orders/2
GET /api/orders/3
# N+1 запросов!
GraphQL решает эти проблемы
# GraphQL: клиент запрашивает только нужные поля
query {
user(id: 123) {
name
orders {
id
total
}
}
}
# Ответ содержит только запрошенные данные
{
"data": {
"user": {
"name": "Alice",
"orders": [
{"id": 1, "total": 99.99},
{"id": 2, "total": 149.99}
]
}
}
}
Установка и настройка
Python с Graphene
pip install graphene graphene-django
# или для FastAPI
pip install strawberry-graphql
Node.js с Apollo Server
npm install apollo-server graphql
# или Express + Apollo
npm install express apollo-server-express graphql
Схема GraphQL
Базовые типы
# Скалярные типы
String # Строки
Int # 32-битные целые
Float # Числа с плавающей точкой
Boolean # true/false
ID # Уникальный идентификатор (String или Int)
# Пользовательские скаляры
scalar DateTime
scalar JSON
scalar Email
Типы объектов
type User {
id: ID!
name: String!
email: String!
age: Int
active: Boolean
posts: [Post!]!
profile: Profile
createdAt: DateTime!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
comments: [Comment!]!
published: Boolean!
createdAt: DateTime!
}
type Comment {
id: ID!
text: String!
author: User!
post: Post!
}
type Profile {
bio: String
avatar: String
website: String
}
Модификаторы:
String— nullable (может быть null)String!— non-nullable (обязательное)[String]— массив nullable строк[String!]— массив non-nullable строк[String]!— non-nullable массив nullable строк[String!]!— non-nullable массив non-nullable строк
Перечисления (Enums)
enum UserRole {
ADMIN
USER
GUEST
MODERATOR
}
enum OrderStatus {
PENDING
PAID
SHIPPED
DELIVERED
CANCELLED
}
type User {
id: ID!
role: UserRole!
}
type Order {
id: ID!
status: OrderStatus!
}
Union и Interface
# Union — одно из нескольких
union SearchResult = User | Post | Comment
type Query {
search(query: String!): [SearchResult!]!
}
# Interface — общие поля
interface Node {
id: ID!
createdAt: DateTime!
}
type User implements Node {
id: ID!
createdAt: DateTime!
name: String!
}
type Post implements Node {
id: ID!
createdAt: DateTime!
title: String!
}
Input типы
input CreateUserInput {
name: String!
email: String!
password: String!
role: UserRole
}
input UpdateUserInput {
name: String
email: String
bio: String
}
input UserFilter {
role: UserRole
active: Boolean
createdAt: DateRange
}
input DateRange {
from: DateTime
to: DateTime
}
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User
}
type Query {
users(filter: UserFilter): [User!]!
}
Query (Запросы)
Базовый запрос
query {
user(id: "123") {
id
name
email
}
}
Аргументы
query {
user(id: "123") {
name
posts(limit: 10, offset: 0) {
title
createdAt
}
}
users(role: ADMIN, active: true) {
id
name
}
}
Переменные
# Запрос с переменными
query GetUser($id: ID!, $postLimit: Int) {
user(id: $id) {
name
email
posts(limit: $postLimit) {
title
}
}
}
# Переменные отправляются отдельно
{
"id": "123",
"postLimit": 5
}
Алиасы
query {
alice: user(id: "1") {
name
email
}
bob: user(id: "2") {
name
email
}
}
# Ответ
{
"data": {
"alice": {...},
"bob": {...}
}
}
Фрагменты
query {
user(id: "1") {
...UserFields
posts {
...PostFields
}
}
}
fragment UserFields on User {
id
name
email
}
fragment PostFields on Post {
id
title
content
}
Вложенные фрагменты:
fragment UserFields on User {
id
name
profile {
...ProfileFields
}
}
fragment ProfileFields on Profile {
bio
avatar
}
Условные фрагменты:
query {
search(query: "test") {
... on User {
name
email
}
... on Post {
title
content
}
... on Comment {
text
}
}
}
Директивы
query GetUser($id: ID!, $includeEmail: Boolean!) {
user(id: $id) {
id
name
email @include(if: $includeEmail)
phone @skip(if: true)
}
}
# @include — включить если true
# @skip — пропустить если true
Mutation (Изменения)
Базовая мутация
mutation {
createUser(input: {
name: "Alice"
email: "alice@example.com"
password: "secret123"
}) {
id
name
email
}
}
Мутация с переменными
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
}
}
# Переменные
{
"input": {
"name": "Alice",
"email": "alice@example.com",
"password": "secret123"
}
}
Несколько мутаций
mutation {
createPost(input: {title: "Hello", content: "World"}) {
id
title
}
createComment(input: {text: "Nice!", postId: "1"}) {
id
text
}
}
# Мутации выполняются последовательно
Subscription (Подписки)
Определение подписки
type Subscription {
postCreated: Post!
userUpdated(id: ID!): User!
messageReceived(roomId: ID!): Message!
orderStatusChanged(orderId: ID!): Order!
}
Клиентская подписка
subscription {
postCreated {
id
title
author {
name
}
}
}
# С аргументами
subscription {
messageReceived(roomId: "room-123") {
id
text
sender {
name
}
}
}
Реализация на Python (Strawberry)
import strawberry
import asyncio
from typing import AsyncGenerator
@strawberry.type
class Post:
id: strawberry.ID
title: str
content: str
@strawberry.type
class Subscription:
@strawberry.subscription
async def post_created(self) -> AsyncGenerator[Post, None]:
# Подключение к WebSocket/Redis PubSub
while True:
await asyncio.sleep(1)
yield Post(id="1", title="New Post", content="...")
Реализация на Node.js (Apollo)
const { PubSub } = require('graphql-subscriptions');
const pubsub = new PubSub();
const resolvers = {
Subscription: {
postCreated: {
subscribe: () => pubsub.asyncIterator(['POST_CREATED'])
}
}
};
// Публикация события
pubsub.publish('POST_CREATED', {
postCreated: { id: '1', title: 'New Post' }
});
Резолверы
Базовые резолверы
# Python с Graphene
import graphene
class User(graphene.ObjectType):
id = graphene.ID()
name = graphene.String()
email = graphene.String()
posts = graphene.List(lambda: Post)
class Query(graphene.ObjectType):
user = graphene.Field(User, id=graphene.ID(required=True))
users = graphene.List(User)
def resolve_user(root, info, id):
return db.get_user(id)
def resolve_users(root, info):
return db.get_all_users()
class Post(graphene.ObjectType):
id = graphene.ID()
title = graphene.String()
author = graphene.Field(User)
def resolve_author(post, info):
return db.get_user(post.author_id)
Резолверы на Node.js
const resolvers = {
Query: {
user: (parent, args, context, info) => {
return db.user.findUnique({ where: { id: args.id } });
},
users: (parent, args, context, info) => {
return db.user.findMany();
}
},
User: {
posts: (user, args, context, info) => {
return db.post.findMany({ where: { authorId: user.id } });
}
},
Post: {
author: (post, args, context, info) => {
return db.user.findUnique({ where: { id: post.authorId } });
}
},
Mutation: {
createUser: async (parent, args, context, info) => {
const { name, email, password } = args.input;
// Валидация
if (!email.includes('@')) {
throw new Error('Invalid email');
}
// Хэширование пароля
const hashedPassword = await hash(password);
// Создание
return db.user.create({
data: { name, email, password: hashedPassword }
});
}
}
};
Контекст
# Python
class Context:
def __init__(self, request):
self.user = get_user_from_token(request.headers.get('Authorization'))
self.db = Database()
def create_context(request):
return Context(request)
# Использование в резолвере
def resolve_user(root, info, id):
if not info.context.user:
raise Exception("Unauthorized")
return info.context.db.get_user(id)
// Node.js
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => {
const token = req.headers.authorization;
const user = verifyToken(token);
return { user, db };
}
});
Оптимизация запросов
Проблема N+1
# ❌ Плохо: N+1 запрос
class User(graphene.ObjectType):
posts = graphene.List(Post)
def resolve_posts(user, info):
return db.posts.filter(author_id=user.id) # Запрос для каждого пользователя
# Запрос:
# query { users { posts { title } } }
# 1 запрос для users + N запросов для posts
DataLoader для batching
from promise.dataloader import DataLoader
class PostLoader(DataLoader):
async def batch_load_fn(self, user_ids):
# Один запрос для всех user_ids
posts = db.posts.filter(author_id__in=user_ids)
# Группировка по author_id
return [
[p for p in posts if p.author_id == uid]
for uid in user_ids
]
# Использование
def resolve_posts(user, info):
return info.context.post_loader.load(user.id)
// Node.js DataLoader
const { DataLoader } = require('dataloader');
const postLoader = new DataLoader(async (userIds) => {
const posts = await db.post.findMany({
where: { authorId: { in: userIds } }
});
return userIds.map(id =>
posts.filter(post => post.authorId === id)
);
});
// В резолвере
User: {
posts: (user, args, context) => {
return context.postLoader.load(user.id);
}
}
Кэширование
from functools import lru_cache
@lru_cache(maxsize=128)
def get_user_cached(user_id: str):
return db.get_user(user_id)
def resolve_user(root, info, id):
return get_user_cached(id)
// Redis кэширование
const cache = require('redis-cache');
const resolvers = {
Query: {
user: cache({
ttl: 300,
key: (args) => `user:${args.id}`
})(async (parent, args, context) => {
return db.user.findUnique({ where: { id: args.id } });
})
}
};
Валидация и ошибки
Пользовательские ошибки
# Union для ошибок
union CreateUserResult = User | CreateUserError
type CreateUserError {
message: String!
field: String
code: String!
}
type Mutation {
createUser(input: CreateUserInput!): CreateUserResult!
}
class CreateUserError(graphene.ObjectType):
message = graphene.String()
field = graphene.String()
code = graphene.String()
class CreateUserResult(graphene.Union):
class Meta:
types = (User, CreateUserError)
def resolve_create_user(root, info, input):
if not is_valid_email(input.email):
return CreateUserError(
message="Invalid email",
field="email",
code="INVALID_EMAIL"
)
try:
return db.create_user(**input)
except DuplicateError:
return CreateUserError(
message="Email already exists",
field="email",
code="DUPLICATE"
)
Apollo Server ошибки
const { UserInputError, AuthenticationError, ForbiddenError } = require('apollo-server');
const resolvers = {
Mutation: {
createUser: async (parent, args, context) => {
if (!context.user) {
throw new AuthenticationError('Unauthorized');
}
if (!args.input.email.includes('@')) {
throw new UserInputError('Invalid email', {
invalidArgs: ['email'],
code: 'INVALID_EMAIL'
});
}
return db.user.create({ data: args.input });
}
}
};
Introspection и документация
Introspection запрос
# Получить все типы
query {
__schema {
types {
name
kind
description
}
}
}
# Получить тип
query {
__type(name: "User") {
name
fields {
name
type {
name
kind
}
}
}
}
Документирование схемы
"""
Пользователь системы.
Представляет зарегистрированного пользователя с профилем.
"""
type User {
"""
Уникальный идентификатор пользователя.
Генерируется автоматически при создании.
"""
id: ID!
"""
Имя пользователя.
Обязательно для регистрации.
"""
name: String!
"""
Email адрес.
Должен быть валидным и уникальным.
"""
email: String!
"""
Роль пользователя в системе.
По умолчанию USER.
"""
role: UserRole!
}
"""
Роль пользователя определяет его права доступа.
"""
enum UserRole {
"""Администратор с полными правами"""
ADMIN
"""Обычный пользователь"""
USER
"""Гость с ограниченными правами"""
GUEST
}
Best Practices
Глубина запросов
// Ограничение глубины
const depthLimit = require('graphql-depth-limit');
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [depthLimit(5)]
});
# ❌ Слишком глубоко
query {
user {
posts {
author {
posts {
author {
posts { # 6 уровней!
title
}
}
}
}
}
}
}
Лимит сложности
const { createComplexityRule } = require('graphql-validation-complexity');
const server = new ApolloServer({
validationRules: [
createComplexityRule({
maximumComplexity: 1000,
variables: {},
onComplete: (complexity) => {
console.log(`Query complexity: ${complexity}`);
}
})
]
});
Пагинация
# Cursor-based пагинация (рекомендуется)
type Query {
users(first: Int, after: String): UserConnection!
posts(first: Int, after: String, filter: PostFilter): PostConnection!
}
type UserConnection {
edges: [UserEdge!]!
pageInfo: PageInfo!
}
type UserEdge {
node: User!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
# Запрос
query {
users(first: 10, after: "cursor123") {
edges {
node { id name }
cursor
}
pageInfo {
hasNextPage
endCursor
}
}
}
Rate Limiting
const rateLimit = require('graphql-rate-limit');
const server = new ApolloServer({
plugins: [
rateLimit({
identifyContext: (ctx) => ctx.user?.id || 'anonymous',
includeFieldCost: true,
rateLimit: {
max: 100,
window: '1m'
}
})
]
});
Заключение
GraphQL — это мощная альтернатива REST для современных API:
- Гибкие запросы — клиент получает только нужные данные
- Один запрос — все данные за один round-trip
- Строгая типизация — схема как контракт
- Эволюция без версионирования — обратная совместимость
- Отличная документация — introspection и автодокументирование
Используйте GraphQL, когда:
- Мобильные клиенты (экономия трафика)
- Сложные связанные данные
- Частые изменения требований
- Несколько клиентов с разными потребностями
Оставайтесь на REST, когда:
- Простые CRUD операции
- Нужно HTTP кэширование
- Публичный API для широкой аудитории
- Команда не знакома с GraphQL