Перейти к основному содержимому

8.05. Синхронная коммуникация

Разработчику Архитектору Инженеру

Синхронная коммуникация

Что такое синхронная коммуникация?

Синхронная коммуникация — это способ взаимодействия, при котором отправитель отправляет запрос и ждёт ответа от получателя. Этот подход широко используется в микросервисной архитектуре для операций, которые требуют немедленного результата (например, проверка данных, выполнение расчётов).

Синхронная коммуникация предполагает блокирующее взаимодействие: инициатор запроса ожидает завершения операции и получения ответа до продолжения выполнения. Клиент и сервер связаны во времени — задержка ответа напрямую влияет на производительность вызывающей стороны.

Примерами синхронности являются HTTP и RPC:

  • HTTP/HTTPS - клиент отправляет HTTP-запрос (например, GET или POST), и сервер немедленно отвечает на этот запрос.
  • RPC (Remote Procedure Call) - вызов удалённой процедуры, где клиент ожидает результата выполнения функции на сервере.

Для HTTP(S) используется, как правило, REST, а для RPC - gRPC.


HTTP

image-9.png

HTTP — протокол прикладного уровня, реализующий модель запрос-ответ поверх надёжного транспорта (обычно TCP). HTTPS добавляет шифрование через TLS/SSL.

Архитектурные особенности

  • Состояние не сохраняется между запросами (stateless): каждый запрос должен содержать всю необходимую контекстную информацию.
  • Версии протокола:
    • HTTP/1.1: поддержка постоянных соединений (keep-alive), конвейеризация (pipeline) ограничена.
    • HTTP/2: мультиплексирование потоков поверх одного TCP-соединения, сжатие заголовков (HPACK), серверные отправки (server push).
    • HTTP/3: замена транспорта с TCP на QUIC (поверх UDP) для снижения задержки при установке соединения и устойчивости к потере пакетов.
  • Методы запросов определяют семантику операции: GET (получение), POST (создание/действие), PUT/PATCH (обновление), DELETE (удаление).
  • Коды состояния группируются по классам: 2xx (успех), 3xx (перенаправление), 4xx (ошибка клиента), 5xx (ошибка сервера).

Механизмы надёжности

  • Повтор запроса при сетевых сбоях осуществляется на уровне приложения (идемпотентные методы GET, PUT, DELETE допускают безопасный повтор).
  • Таймауты соединения и чтения настраиваются на клиенте и сервере.
  • Для критичных операций применяется идемпотентность через идентификаторы запросов (например, идемпотентные ключи в платежных системах).

Ограничения синхронной модели

  • Блокировка потока выполнения до получения ответа.
  • Ограниченное время ожидания (таймауты прокси, балансировщиков, клиентских библиотек).
  • Неэффективность для длительных операций (долгие вычисления, внешние интеграции) — требуется применение паттернов опроса (polling) или асинхронных обратных вызовов.

Типичные сценарии применения

  • Веб-интерфейсы и мобильные клиенты.
  • Интеграция между сервисами с низкой задержкой и простой семантикой запроса.
  • RESTful API с чёткими контрактами и идемпотентными операциями.

RPC

image-10.png

RPC (Remote Procedure Call) — абстракция, позволяющая вызывать функцию на удалённом узле так, будто она локальная. Синхронная реализация подразумевает блокирующее ожидание результата.

Архитектурные особенности

  • Стек вызова расширяется через сеть: клиент сериализует параметры → отправляет запрос → ожидает ответ → десериализует результат/исключение.
  • Типы реализаций:
    • gRPC (Google): бинарный протокол поверх HTTP/2, IDL на основе Protocol Buffers, поддержка потоковой передачи.
    • Apache Thrift: мультиязыковая поддержка, собственный бинарный протокол (также поддерживает HTTP, JSON).
    • JSON-RPC / XML-RPC: текстовые форматы поверх HTTP, простота отладки ценой производительности.
    • CORBA (устаревший): сложная распределённая объектная модель с поддержкой транзакций и безопасности.
  • Контракт интерфейса строго определяется в IDL (Interface Definition Language), что обеспечивает типобезопасность на этапе компиляции.

Механизмы надёжности

  • Автоматические повторы (retry) с экспоненциальной задержкой и ограничением количества попыток.
  • Таймауты на уровне вызова (deadline propagation) — критично для предотвращения каскадных отказов.
  • Трассировка распределённых вызовов через контекстные заголовки (например, trace ID в gRPC metadata).
  • Обнаружение сервисов (service discovery) и балансировка нагрузки на уровне клиента (client-side load balancing).

Проблемы синхронного RPC

  • Сетевые разделения приводят к блокировке или таймаутам вызывающего потока.
  • Каскадные отказы: медленный зависимый сервис блокирует все вызывающие его компоненты.
  • Сложность обеспечения согласованности при распределённых транзакциях.
  • Отсутствие встроенной очереди: пиковая нагрузка может привести к отказу сервиса.

Типичные сценарии применения

  • Высокопроизводительные внутренние API между микросервисами с низкой задержкой.
  • Системы, требующие строгой типизации и автоматической генерации клиентских библиотек.
  • Синхронные операции с предсказуемым временем выполнения (менее 100–500 мс).

HTTP(S) работают по модели «запрос-ответ», поэтому всегда подразумевает клиент-серверное взаимодействие, ведь клиент (веб-браузер или приложение) формирует и отправляет запрос на сервер, а сервер, получает запрос, обрабатывает его и отправляет ответ клиенту.

HTTP мы уже ранее изучали, поэтому перейдём к другим протоколам.


gRPC, GraphQL

gRPC (Google Remote Procedure Call) — это современный фреймворк для создания высокопроизводительных RPC (Remote Procedure Call) сервисов. В отличие от REST, gRPC использует протокол HTTP/2 и формат Protocol Buffers (Protobuf).

Полная документация доступна здесь - https://grpc.io/

Этот подход лучше использовать для высокопроизводительных систем с большим объёмом данных, и если требуется строгая типизация и автоматическая генерация кода.

Protocol Buffers (Protobuf) — это специальный формат для описания контрактов (схем данных) и сериализации. Protobuf более компактный и быстрый, чем JSON.

HTTP/2 используется для передачи данных, что обеспечивает высокую производительность благодаря мультиплексированию, сжатию заголовков и двунаправленным потокам.

GraphQL — это язык запросов и среда выполнения для API, разработанный Facebook в 2012 году. Он позволяет клиентам запрашивать только те данные, которые им нужны, что делает его более гибким по сравнению с традиционными REST API. Клиент может указать точную структуру данных, которые он хочет получить.

Документация здесь - https://graphql.org/

Чит-лист - https://cheatsheets.zip/graphql

В отличие от REST, где каждый ресурс имеет свой URL, GraphQL использует один эндпоинт для всех запросов. GraphQL использует схему, которая определяет доступные данные и их типы, предоставляя документацию.


Пример реализации

gRPC с Protocol Buffers

Определение контракта (.proto файл)

syntax = "proto3";

package user;

option csharp_namespace = "UserService";
option java_package = "com.example.user";
option java_outer_classname = "UserProto";

// Сообщения
message UserRequest {
int32 user_id = 1;
}

message UserResponse {
int32 user_id = 1;
string name = 2;
string email = 3;
bool is_active = 4;
}

message UserListRequest {
int32 page = 1;
int32 page_size = 2;
}

message UserListResponse {
repeated UserResponse users = 1;
int32 total_count = 2;
}

// Сервис
service UserService {
rpc GetUser(UserRequest) returns (UserResponse);
rpc ListUsers(UserListRequest) returns (UserListResponse);
rpc CreateUser(UserResponse) returns (UserResponse);
rpc UpdateUser(UserResponse) returns (UserResponse);
rpc DeleteUser(UserRequest) returns (google.protobuf.Empty);
}

Реализация на C#

Сервер:

using Grpc.Core;
using UserService;

public class UserServiceImpl : UserService.UserServiceBase
{
private readonly Dictionary<int, UserResponse> _users = new();

public override Task<UserResponse> GetUser(
UserRequest request,
ServerCallContext context)
{
if (_users.TryGetValue(request.UserId, out var user))
return Task.FromResult(user);

throw new RpcException(
new Status(StatusCode.NotFound, "User not found"));
}

public override Task<UserListResponse> ListUsers(
UserListRequest request,
ServerCallContext context)
{
var skip = (request.Page - 1) * request.PageSize;
var users = _users.Values
.Skip(skip)
.Take(request.PageSize)
.ToList();

return Task.FromResult(new UserListResponse
{
Users = { users },
TotalCount = _users.Count
});
}

public override Task<UserResponse> CreateUser(
UserResponse request,
ServerCallContext context)
{
var newUser = new UserResponse
{
UserId = _users.Count + 1,
Name = request.Name,
Email = request.Email,
IsActive = request.IsActive
};

_users[newUser.UserId] = newUser;
return Task.FromResult(newUser);
}
}

// Запуск сервера
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddGrpc();

var app = builder.Build();
app.MapGrpcService<UserServiceImpl>();
app.Run();

Клиент:

using Grpc.Net.Client;
using UserService;

var channel = GrpcChannel.ForAddress("https://localhost:5001");
var client = new UserService.UserServiceClient(channel);

// Получение пользователя
var user = await client.GetUserAsync(new UserRequest { UserId = 1 });
Console.WriteLine($"User: {user.Name}, Email: {user.Email}");

// Создание пользователя
var newUser = await client.CreateUserAsync(new UserResponse
{
Name = "Тимур",
Email = "timur@example.com",
IsActive = true
});
Console.WriteLine($"Created user ID: {newUser.UserId}");

Реализация на Python

Сервер:

import grpc
from concurrent import futures
import user_pb2
import user_pb2_grpc

class UserServiceImpl(user_pb2_grpc.UserServiceServicer):
def __init__(self):
self.users = {}

def GetUser(self, request, context):
user = self.users.get(request.user_id)
if not user:
context.set_code(grpc.StatusCode.NOT_FOUND)
context.set_details('User not found')
return user_pb2.UserResponse()
return user

def ListUsers(self, request, context):
skip = (request.page - 1) * request.page_size
users_list = list(self.users.values())[skip:skip + request.page_size]

return user_pb2.UserListResponse(
users=users_list,
total_count=len(self.users)
)

def CreateUser(self, request, context):
user_id = len(self.users) + 1
new_user = user_pb2.UserResponse(
user_id=user_id,
name=request.name,
email=request.email,
is_active=request.is_active
)
self.users[user_id] = new_user
return new_user

# Запуск сервера
def serve():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
user_pb2_grpc.add_UserServiceServicer_to_server(UserServiceImpl(), server)
server.add_insecure_port('[::]:50051')
server.start()
server.wait_for_termination()

if __name__ == '__main__':
serve()

Клиент:

import grpc
import user_pb2
import user_pb2_grpc

def run():
channel = grpc.insecure_channel('localhost:50051')
stub = user_pb2_grpc.UserServiceStub(channel)

# Получение пользователя
response = stub.GetUser(user_pb2.UserRequest(user_id=1))
print(f"User: {response.name}, Email: {response.email}")

# Создание пользователя
new_user = stub.CreateUser(user_pb2.UserResponse(
name="Тимур",
email="timur@example.com",
is_active=True
))
print(f"Created user ID: {new_user.user_id}")

if __name__ == '__main__':
run()

Реализация на Java

Сервер:

import io.grpc.Server;
import io.grpc.ServerBuilder;
import io.grpc.stub.StreamObserver;
import com.example.user.*;

import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class UserServiceImpl extends UserServiceGrpc.UserServiceImplBase {

private final Map<Integer, UserResponse> users = new ConcurrentHashMap<>();

@Override
public void getUser(UserRequest request, StreamObserver<UserResponse> responseObserver) {
UserResponse user = users.get(request.getUserId());

if (user == null) {
responseObserver.onError(
io.grpc.Status.NOT_FOUND
.withDescription("User not found")
.asRuntimeException()
);
} else {
responseObserver.onNext(user);
responseObserver.onCompleted();
}
}

@Override
public void listUsers(UserListRequest request, StreamObserver<UserListResponse> responseObserver) {
int skip = (request.getPage() - 1) * request.getPageSize();
UserListResponse.Builder builder = UserListResponse.newBuilder();

users.values().stream()
.skip(skip)
.limit(request.getPageSize())
.forEach(builder::addUsers);

builder.setTotalCount(users.size());
responseObserver.onNext(builder.build());
responseObserver.onCompleted();
}

@Override
public void createUser(UserResponse request, StreamObserver<UserResponse> responseObserver) {
int userId = users.size() + 1;
UserResponse newUser = UserResponse.newBuilder()
.setUserId(userId)
.setName(request.getName())
.setEmail(request.getEmail())
.setIsActive(request.getIsActive())
.build();

users.put(userId, newUser);
responseObserver.onNext(newUser);
responseObserver.onCompleted();
}

public static void main(String[] args) throws IOException, InterruptedException {
Server server = ServerBuilder.forPort(50051)
.addService(new UserServiceImpl())
.build()
.start();

System.out.println("Server started on port 50051");
server.awaitTermination();
}
}

Клиент:

import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import com.example.user.*;

public class UserClient {
public static void main(String[] args) {
ManagedChannel channel = ManagedChannelBuilder
.forAddress("localhost", 50051)
.usePlaintext()
.build();

UserServiceGrpc.UserServiceBlockingStub stub =
UserServiceGrpc.newBlockingStub(channel);

// Получение пользователя
UserResponse user = stub.getUser(
UserRequest.newBuilder().setUserId(1).build()
);
System.out.println("User: " + user.getName() + ", Email: " + user.getEmail());

// Создание пользователя
UserResponse newUser = stub.createUser(
UserResponse.newBuilder()
.setName("Тимур")
.setEmail("timur@example.com")
.setIsActive(true)
.build()
);
System.out.println("Created user ID: " + newUser.getUserId());

channel.shutdown();
}
}

GraphQL

Схема типов (schema.graphql)

type User {
id: ID!
name: String!
email: String!
isActive: Boolean!
createdAt: String!
posts: [Post!]!
}

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

type Query {
user(id: ID!): User
users(page: Int = 1, pageSize: Int = 10): [User!]!
post(id: ID!): Post
posts(page: Int = 1, pageSize: Int = 10): [Post!]!
}

type Mutation {
createUser(name: String!, email: String!, isActive: Boolean = true): User!
updateUser(id: ID!, name: String, email: String, isActive: Boolean): User!
deleteUser(id: ID!): Boolean!
createPost(title: String!, content: String!, authorId: ID!): Post!
}

type Subscription {
userCreated: User!
postCreated: Post!
}

Реализация на C# (Hot Chocolate)

Модели:

public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public bool IsActive { get; set; }
public DateTime CreatedAt { get; set; }
public List<Post> Posts { get; set; }
}

public class Post
{
public int Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int AuthorId { get; set; }
public DateTime PublishedAt { get; set; }
public User Author { get; set; }
}

Резолверы:

public class Query
{
private readonly List<User> _users = new();
private readonly List<Post> _posts = new();

public User GetUser(int id) => _users.FirstOrDefault(u => u.Id == id);

public List<User> GetUsers(int page = 1, int pageSize = 10)
{
int skip = (page - 1) * pageSize;
return _users.Skip(skip).Take(pageSize).ToList();
}

public Post GetPost(int id) => _posts.FirstOrDefault(p => p.Id == id);

public List<Post> GetPosts(int page = 1, int pageSize = 10)
{
int skip = (page - 1) * pageSize;
return _posts.Skip(skip).Take(pageSize).ToList();
}
}

public class Mutation
{
private readonly List<User> _users = new();
private readonly List<Post> _posts = new();

public User CreateUser(string name, string email, bool isActive = true)
{
var user = new User
{
Id = _users.Count + 1,
Name = name,
Email = email,
IsActive = isActive,
CreatedAt = DateTime.UtcNow
};
_users.Add(user);
return user;
}

public User UpdateUser(int id, string? name = null, string? email = null, bool? isActive = null)
{
var user = _users.FirstOrDefault(u => u.Id == id);
if (user == null) throw new Exception("User not found");

if (name != null) user.Name = name;
if (email != null) user.Email = email;
if (isActive != null) user.IsActive = isActive.Value;

return user;
}

public bool DeleteUser(int id)
{
var user = _users.FirstOrDefault(u => u.Id == id);
if (user == null) return false;
return _users.Remove(user);
}

public Post CreatePost(string title, string content, int authorId)
{
var author = _users.FirstOrDefault(u => u.Id == authorId);
if (author == null) throw new Exception("Author not found");

var post = new Post
{
Id = _posts.Count + 1,
Title = title,
Content = content,
AuthorId = authorId,
PublishedAt = DateTime.UtcNow,
Author = author
};
_posts.Add(post);
return post;
}
}

Настройка сервера:

var builder = WebApplication.CreateBuilder(args);

builder.Services
.AddGraphQLServer()
.AddQueryType<Query>()
.AddMutationType<Mutation>()
.AddSubscriptionType<Subscription>()
.AddType<UserType>()
.AddType<PostType>();

var app = builder.Build();
app.MapGraphQL();
app.Run();

Типы GraphQL:

public class UserType : ObjectType<User>
{
protected override void Configure(IObjectTypeDescriptor<User> descriptor)
{
descriptor.Field(u => u.Id).Type<NonNullType<IDType>>();
descriptor.Field(u => u.Posts)
.ResolveWith<Query>(q => q.GetPosts(default!, default!))
.Type<ListType<NonNullType<PostType>>>();
}
}

public class PostType : ObjectType<Post>
{
protected override void Configure(IObjectTypeDescriptor<Post> descriptor)
{
descriptor.Field(p => p.Author)
.ResolveWith<Query>(q => q.GetUser(default!))
.Type<NonNullType<UserType>>();
}
}

Реализация на Python (Strawberry)

Схема и резолверы:

import strawberry
from typing import List, Optional
from datetime import datetime

@strawberry.type
class User:
id: int
name: str
email: str
is_active: bool
created_at: str

@strawberry.field
def posts(self) -> List["Post"]:
return [post for post in posts if post.author_id == self.id]

@strawberry.type
class Post:
id: int
title: str
content: str
author_id: int
published_at: str

@strawberry.field
def author(self) -> User:
return next(user for user in users if user.id == self.author_id)

# Хранилище данных
users: List[User] = []
posts: List[Post] = []

@strawberry.type
class Query:
@strawberry.field
def user(self, id: int) -> Optional[User]:
return next((u for u in users if u.id == id), None)

@strawberry.field
def users(self, page: int = 1, page_size: int = 10) -> List[User]:
start = (page - 1) * page_size
return users[start:start + page_size]

@strawberry.field
def post(self, id: int) -> Optional[Post]:
return next((p for p in posts if p.id == id), None)

@strawberry.field
def posts(self, page: int = 1, page_size: int = 10) -> List[Post]:
start = (page - 1) * page_size
return posts[start:start + page_size]

@strawberry.type
class Mutation:
@strawberry.mutation
def create_user(self, name: str, email: str, is_active: bool = True) -> User:
user = User(
id=len(users) + 1,
name=name,
email=email,
is_active=is_active,
created_at=datetime.utcnow().isoformat()
)
users.append(user)
return user

@strawberry.mutation
def update_user(
self,
id: int,
name: Optional[str] = None,
email: Optional[str] = None,
is_active: Optional[bool] = None
) -> User:
user = next((u for u in users if u.id == id), None)
if not user:
raise Exception("User not found")

if name: user.name = name
if email: user.email = email
if is_active is not None: user.is_active = is_active

return user

@strawberry.mutation
def delete_user(self, id: int) -> bool:
global users
initial_count = len(users)
users = [u for u in users if u.id != id]
return len(users) < initial_count

@strawberry.mutation
def create_post(self, title: str, content: str, author_id: int) -> Post:
author = next((u for u in users if u.id == author_id), None)
if not author:
raise Exception("Author not found")

post = Post(
id=len(posts) + 1,
title=title,
content=content,
author_id=author_id,
published_at=datetime.utcnow().isoformat()
)
posts.append(post)
return post

@strawberry.type
class Subscription:
@strawberry.subscription
async def user_created(self) -> User:
# Реализация через asyncio.Queue или Redis Pub/Sub
pass

schema = strawberry.Schema(query=Query, mutation=Mutation, subscription=Subscription)

Запуск сервера (FastAPI):

from fastapi import FastAPI
from strawberry.fastapi import GraphQLRouter

graphql_app = GraphQLRouter(schema)
app = FastAPI()
app.include_router(graphql_app, prefix="/graphql")

if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)

Реализация на Java (GraphQL Java)

Схема:

import graphql.schema.GraphQLObjectType;
import graphql.schema.GraphQLSchema;
import graphql.schema.idl.RuntimeWiring;
import graphql.schema.idl.SchemaGenerator;
import graphql.schema.idl.SchemaParser;
import graphql.schema.idl.TypeDefinitionRegistry;

import java.io.InputStream;

public class GraphQLSchemaProvider {

public GraphQLSchema createSchema() {
SchemaParser parser = new SchemaParser();
TypeDefinitionRegistry typeRegistry = parser.parse(
this.getClass().getResourceAsStream("/schema.graphqls")
);

RuntimeWiring wiring = RuntimeWiring.newRuntimeWiring()
.type("Query", typeWiring -> typeWiring
.dataFetcher("user", new UserDataFetcher())
.dataFetcher("users", new UsersDataFetcher())
.dataFetcher("post", new PostDataFetcher())
.dataFetcher("posts", new PostsDataFetcher())
)
.type("Mutation", typeWiring -> typeWiring
.dataFetcher("createUser", new CreateUserMutation())
.dataFetcher("updateUser", new UpdateUserMutation())
.dataFetcher("deleteUser", new DeleteUserMutation())
.dataFetcher("createPost", new CreatePostMutation())
)
.type("User", typeWiring -> typeWiring
.dataFetcher("posts", new UserPostsDataFetcher())
)
.type("Post", typeWiring -> typeWiring
.dataFetcher("author", new PostAuthorDataFetcher())
)
.build();

SchemaGenerator generator = new SchemaGenerator();
return generator.makeExecutableSchema(typeRegistry, wiring);
}
}

DataFetcher для запросов:

import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;

import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

public class UserDataFetcher implements DataFetcher<User> {

private final Map<Integer, User> users = new ConcurrentHashMap<>();

@Override
public User get(DataFetchingEnvironment environment) {
Integer id = environment.getArgument("id");
return users.get(id);
}
}

public class UsersDataFetcher implements DataFetcher<List<User>> {

private final Map<Integer, User> users = new ConcurrentHashMap<>();

@Override
public List<User> get(DataFetchingEnvironment environment) {
Integer page = environment.getArgument("page");
Integer pageSize = environment.getArgument("pageSize");

int skip = (page - 1) * pageSize;
return users.values().stream()
.skip(skip)
.limit(pageSize)
.collect(Collectors.toList());
}
}

Модели:

public class User {
private int id;
private String name;
private String email;
private boolean isActive;
private String createdAt;

// getters/setters
}

public class Post {
private int id;
private String title;
private String content;
private int authorId;
private String publishedAt;

// getters/setters
}

Запуск сервера (Spring Boot):

import graphql.GraphQL;
import graphql.schema.GraphQLSchema;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class GraphQLConfig {

@Bean
public GraphQL graphQL(GraphQLSchemaProvider schemaProvider) {
GraphQLSchema schema = schemaProvider.createSchema();
return GraphQL.newGraphQL(schema).build();
}
}

@RestController
@RequestMapping("/graphql")
public class GraphQLController {

@Autowired
private GraphQL graphQL;

@PostMapping(produces = "application/json")
public ResponseEntity<Object> graphql(@RequestBody String query) {
ExecutionInput executionInput = ExecutionInput.newExecutionInput()
.query(query)
.build();

ExecutionResult executionResult = graphQL.execute(executionInput);
return ResponseEntity.ok(executionResult.toSpecification());
}
}

Примеры запросов и мутаций

Запрос пользователя с постами:

query {
user(id: "1") {
id
name
email
isActive
posts {
id
title
publishedAt
}
}
}

Список пользователей с пагинацией:

query {
users(page: 1, pageSize: 5) {
id
name
email
posts {
id
title
}
}
}

Создание пользователя:

mutation {
createUser(name: "Тимур", email: "timur@example.com", isActive: true) {
id
name
email
createdAt
}
}

Создание поста:

mutation {
createPost(
title: "GraphQL vs REST",
content: "Сравнительный анализ...",
authorId: "1"
) {
id
title
author {
name
email
}
}
}

Фрагменты для повторного использования:

fragment UserFields on User {
id
name
email
isActive
}

query {
user(id: "1") {
...UserFields
posts {
id
title
}
}
}

Параметризованный запрос:

query GetUserById($userId: ID!) {
user(id: $userId) {
id
name
posts {
id
title
content
}
}
}

# Variables:
# {
# "userId": "1"
# }