REST vs. GraphQL: відмінності, переваги та конвертація. Частина 1


Станіслав Сілін, Senior .NET Developer

27 Липня, 2023

Привіт, мене звати Станіслав Сілін, я Senior .NET Developer у Brightgrove. У цій статті я хочу продемонструвати новий спосіб створення веб API за допомогою GraphQL.    

 

Ми розглянемо його переваги в порівнянні з традиційним REST API і як трансформувати існуючі REST API для підтримки GraphQL.

Надмірна та недостатня вибірка

По-перше, нам потрібно з’ясувати, які проблеми можуть виникнути з REST API і як GraphQL може допомогти їх вирішити. Дві основні проблеми — це недостатня вибірка і надмірна вибірка.    

 

Вони є поширеними, коли ми працюємо з REST API. Недостатня вибірка виникає, коли початковий запит до API не надає достатньо даних, що змушує програму робити додаткові запити. Надмірна вибірка, з іншого боку, трапляється, коли запит API повертає більше даних, ніж потрібно клієнту.    

 

Розглянемо сценарій, коли розробник працює над профілем користувача, якому потрібно завантажити особисту інформацію (наприклад, ім’я, електронну пошту, фотографію профілю) і ролі користувача (наприклад, адміністратор, підписник тощо). У традиційному RESTful підході для отримання цих даних може знадобитися щонайменше дві різні кінцеві точки: одна для особистої інформації, а інша для ролей.  

 

Якщо ви отримуєте дані з кінцевого пункта особистої інформації, а вона не містить ролей користувачів, ви отримали неповну вибірку і повинні зробити додатковий запит до кінцевого пункта ролей.     

 

І навпаки, якщо кінцевий пункт персональної інформації повертає всі доступні дані про користувача, включаючи деталі, які не потрібні для поточного подання, ви отримали надлишкову вибірку, що призводить до перевитрати пропускної здатності і потенційного уповільнення роботи програми.    

 

GraphQL допомагає вирішити цю проблему досить легко. Замість кількох кінцевих пунктів є лише один, який приймає запити. Ці запити точно визначають, які дані потрібні клієнту, усуваючи неповну вибірку. Клієнт може запитувати різні типи даних в одному запиті.    

 

Давайте подивимося на наш приклад з профілем користувача. Замість того, щоб робити окремі запити для особистої інформації та ролей, ми можемо зробити один GraphQL запит:  

query {
user(id: 1) {
name
email
profilePicture
roles {
title
}
}
}

Цей запит запитує певні поля (name, email, profilePicture і roles) для користувача з ідентифікатором 1. Сервер поверне JSON-об’єкт, який відображає форму запиту і включає тільки запитувані поля.   

 

Гнучкість запитів GraphQL також вирішує проблему надмірної вибірки. Сервер ніколи не надішле непотрібні дані, оскільки клієнт точно вказує, які саме дані йому потрібні. У нашому прикладі, якщо нам не потрібне зображення профілю користувача для певного подання, ми можемо просто не включати його в запит:  

query {
user(id: 1) {
name
email
roles {
title
}
}
}

Сервер не надішле зображення профілю в цьому запиті, навіть якщо воно доступне.

Ключові відмінності від REST

Запит   

 

Строгий протокол GraphQL є невід’ємною частиною його потужності і ключовою відмінністю від REST API, де угоди можуть суттєво відрізнятися. На сьогоднішній день всі великі технологічні компанії мають свої власні найкращі практики REST, які іноді можуть бути абсолютно несумісними.   

 

У той же час, API-інтерфейси GraphQL організовані з точки зору типів і полів, а не кінцевих пунктів. GraphQL використовує потужну систему типів для опису можливостей API і всіх типів даних, які він може повертати, що полегшує роботу розробників і гарантує передбачувану поведінку API.   

 

Ось приклад того, як може виглядати схема: 

type Query {
users(page: Int!, size: Int!): [User!]!
user(id: Int!): User
}

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

type Role {
id: Int!
name: String!
}

type User {
id: Int!
name: String!
email: String!
roles: [Role!]!
}

Керування версіями    

 

Керування версіями має важливе значення для підтримки API, особливо коли API зазнає значних змін. У традиційних REST API керування версіями часто передбачає або зміну URL-адреси кінцевого пункта, або включення номера версії в заголовок запиту.  

 

Недоліком такого підходу є те, що він може призвести до поломки програми для будь-яких клієнтів, які не оновили її до нової версії API. Однак, GraphQL використовує принципово інший підхід до керування версіями, що допомагає уникнути проблем, пов’язаних з традиційними методами керування версіями.    

 

Замість того, щоб створювати нові версії всього API, розробники можуть просто розширити існуючу схему GraphQL новими полями для підтримки нової функціональності. Клієнти, які не були оновлені для розуміння цих нових полів, просто проігнорують їх, і API залишиться зворотньо сумісним.    

 

Якщо нам потрібно видалити поле, GraphQL підтримує депрецесію, яка сигналізує розробникам, що поле більше не використовується і буде видалено в майбутньому. Навіть після того, як поле застаріло, воно все ще працює, тому клієнти, які використовують це поле, не отримають негайного збою. Це дає клієнтам час для переходу від використання застарілого поля.    

 

Система сильних типів у GraphQL допомагає забезпечити сумісність змін з існуючими запитами. Вона запобігає несумісним змінам, таким як зміна типу поля.    

 

По суті, підхід GraphQL до керування версіями дозволяє уникнути необхідності в номерах версій і розриві змін. Він є більш гнучким і поблажливим і дозволяє розробникам додавати нові функції до API, не порушуючи роботу існуючих користувачів. В результаті еволюція API стає більш плавним процесом, що сприяє кращому досвіду розробників і забезпечує більш надійні додатки.    

 

Генерація клієнтів    

 

У GraphQL генерація клієнтів використовує схему API та функцію самоаналізу. Враховуючи те, що схема GraphQL сильно типізована, а типи і зв’язки чітко визначені, можна використовувати цю інформацію для автоматичної генерації клієнтського коду.    

 

Клієнт може бути на різних мовах, таких як TypeScript, C# тощо. Це зменшує кількість шаблонного коду, який потрібно писати розробникам, заощаджуючи час і мінімізуючи ризик помилок при ручному кодуванні.    

 

Генерація клієнтів гармонійно працює з семантикою вилучення полів у GraphQL. Коли поле застаріло, воно все ще може бути включене в схему і згенерований клієнтський код, але воно позначається як застаріле.    

 

Розробники можуть бачити попередження про застарілість безпосередньо у своєму IDE під час кодування. Це допомагає їм плавно і контрольовано відмовитися від використання застарілих полів. З часом, коли розробники перестануть використовувати застаріле поле, його можна буде безпечно видалити зі схеми, не порушуючи роботу клієнтів. 

Перетворення існуючого REST API в GraphQL API

Початкове налаштування   

 

Давайте розглянемо перетворення існуючого REST API в GraphQL API. Початкові кінцеві пункти виглядають так:  

app.UseEndpoints(
o =>
{
o.MapSwagger();
o.MapGet("/users", 
async (int page, int size, UserService service) => await service.GetUsers(page, size));

o.MapGet("/users/{id}",
async (int id, UserService service) => await service.GetUser(id));

o.MapGet("/users/{id}/roles",
async (int id, RoleService service) => await service.GetRolesByUserId(new[] { id }));

o.MapPost("/users", 
async (UserCreationRequest request, UserService service) => await service.CreateUser(request.Name, request.Name));
});

Ось приклад кінцевих пунктів, які обробляють сторінку від недостатньої та надмірної вибірки. Перше, що вам потрібно зробити, це встановити пакет HotChocolate.AspNetCore NuGet.  

dotnet add package HotChocolate.AspNetCore

HotChocolate — це набір інструментів, який дозволяє нам отримувати та обробляти GraphQL запити. Він має гарну інтеграцію з пайплайном AspNet Core. Повну документацію можна знайти тут.    

 

Тепер перейдемо до бутстрапного GraphQL-сервера:    

+ services
+ .AddGraphQLServer()
+ .AddQueryType<Query>();

// ...

app.UseEndpoints(
o =>
{
+ o.MapGraphQL();
o.MapSwagger();
o.MapGet("/users", 
async (int page, int size, UserService service) => await service.GetUsers(page, size));
// ...
+ public class Query 
+ {
+ public DateTimeOffset Utc() => DateTimeOffset.UtcNow;
+ }

Коли ми запускаємо наш додаток, ми можемо отримати доступ до кінцевого пункта GraphQL http://localhost:1234/graphql. Якщо відкрити його через браузер, ми побачимо IDE GraphQL. Тут ми можемо перевірити схему GraphQL і виконати запити.   

 

 

Початкове налаштування готове, і ми можемо приступити до перетворення наших REST API.  

 

Створення запитів

 

Давайте спочатку перетворимо наступну кінцеву точку: 

 o.MapGet("/users/{id}",
async (int id, UserService service) => await service.GetUser(id));

Для цього ми можемо просто створити новий метод у нашому класі Query:  

public class Query 
{
public DateTimeOffset Utc() => DateTimeOffset.UtcNow;

+ public Task<User?> GetUser(int id, [Service] UserService userService)
+ => userService.GetUser(id);
}

Готово! Якщо ми відкриємо нашу пункта і перевіримо схему GraphQL, вона буде виглядати так:   

type Query {
utc: DateTime!
user(id: Int!): User
}

type User {
id: Int!
name: String!
email: String!
}

HotChocolate автоматично визначає, які типи відкриті, і додає їх до схеми.    

 

Йдемо далі і трансформуємо ще один кінцевий пункт. Однак тепер ми зробимо це трохи інакше. Ми все ще можемо додати його до класу Query, але розміщувати все в одному класі — не найкраща ідея. Ми можемо розділити його, щоб виглядати дуже близько до того, що ми маємо при використанні контролерів з AspNet Core.    

 

Щоб це працювало, створіть новий клас UserQueryExtensions і визначте там нові “кінцеві пункти”. Далі я буду використовувати визначення “GraphQL field”, щоб вказати еквівалент “REST endpoints”, але для GraphQL.    

 

Це буде виглядати так:  

services
.AddGraphQLServer()
.AddQueryType<Query>()
+ .AddTypeExtension<UserQueryExtensions>();

// ..
public class Query 
{
public DateTimeOffset Utc() => DateTimeOffset.UtcNow;

- public Task<User?> GetUser(int id, [Service] UserService userService)
- => userService.GetUser(id);
}

+ [ExtendObjectType(typeof(Query))]
+ public class UserQueryExtensions
+ {
+ public Task<IEnumerable<User>> GetUsers(int page, int size, [Service] UserService userService)
+ => userService.GetUsers(page, size);
+ 
+ public Task<User?> GetUser(int id, [Service] UserService userService)
+ => userService.GetUser(id);
+ }

Тут ми створили клас, який розширює інший тип GraphQL. У нашому випадку це кореневий тип Query. Крім того, я перемістив призначене для користувача GraphQL-поле з Query і додав його в нове розширення.    

 

Зверніть увагу на розділ конфігурації на початку. Ви повинні додати ці розширення в налаштування вручну. Інакше HotChocolate не зможе їх виявити.    

 

Тепер схема GraphQL матиме наступний вигляд:     

type Query {
utc: DateTime!
+ users(page: Int!, size: Int!): [User!]!
user(id: Int!): User
}

type User {
id: Int!
name: String!
email: String!
}

Як бачите, з точки зору GraphQL змінилася лише одна річ. Тепер у нас є нове поле GraphQL. Це саме те, що ми хотіли  

Це все на сьогодні. Приєднуйтесь до мене у другій частині статті, щоб дізнатися більше про складні розширення типів і створення мутацій. Дякую за увагу і до наступного разу!