REST проти GraphQL: справжні компроміси кожної архітектури
Пояснює конкретні проблеми, які вирішують REST та GraphQL, їхню внутрішню структуру, а також приховані компроміси, які потрібно врахувати перед вибором одного з них для вашого API.
Джерела: REST vs GraphQL: Which Problem Does Each One Actually Solve?
Коли ви починаєте проектувати спосіб взаємодії клієнта та сервера, майже завжди виникає одне питання: REST чи GraphQL?
Дискусії на цю тему часто представляють її як змагання, ніби одна зі сторін мусить перемогти. Деякі розробники стверджують, що REST вже вичерпав себе та його слід відмовитися. Інші наполягають на тому, що GraphQL додає рівні складності, які не потрібні більшості проектів. Жоден з цих підходів не є особливо корисним.
Технології не створюються для того, щоб набирати бали у суперечках. Вони існують для вирішення конкретних проблем, з якими стикаються розробники в певних умовах.
Використання GraphQL лише через те, що він вважається актуальним, може призвести до проблем із кешуванням та дорогими запитами з боку сервера, які ви навіть не планували. Навпаки, примусове застосування суворої структури REST у мобільному додатку з великою кількістю даних може уповільнити роботу екранів через низку послідовних мережевих запитів.
Щоб прийняти правильне рішення, корисно відкинути хайп та розглянути основні мотивації: що мав виправити кожен підхід, як він функціонує всередині та які компроміси з ним пов’язані.
Яку проблему вирішив REST?
До того, як REST став стандартом, веб-сервіси сильно залежали від протоколів на кшталт SOAP та XML-RPC. Вони вимагали суворих схем XML, довгих форматів повідомлень та складної налаштування. Клієнтам потрібно було знати точні назви віддалених процедур лише для отримання даних.
У 2000 році Рой Філдінг представив REST (Representational State Transfer).
У своїй суті REST запропонував просту, але ефективну концепцію: скористатися власною архітектурою Інтернету.
Замість того, щоб розглядати сервер як непрозорий механізм виконання функцій, REST моделює систему як сукупність ресурсів, кожен з яких можна адресувати за допомогою чіткого URL. Дії над цими ресурсами виражаються за допомогою стандартних HTTP-дій (GET, POST, PUT, DELETE), а результат кожного запиту повідомляється за допомогою знайомих HTTP-кодів стану, таких як 200 OK для успіху, 404 Not Found, коли ресурс відсутній, або 500 Server Error, коли виникають проблеми на серверній стороні.
Resource-Oriented Architecture (REST)
Client ─── GET /api/users/12 ──────────> Server (Find user #12)
Client <── 200 OK [ { user data } ] ──── Server
Client ─── DELETE /api/posts/99 ───────> Server (Delete post #99)
Client <── 204 No Content ───────────── Server
Чому важливий REST
- Простота та передбачуваність: Як тільки ви зрозумієте, що
/api/productsвказує на колекцію продуктів, а/api/products/42— на конкретну одиницю, інтерфейс стає зрозумілим без пояснень.
GET є ідемпотентними та мапуються на унікальні URL, інфраструктура на кшталт браузерів, CDN (наприклад, Cloudflare) та зворотних проксі (таких як Nginx) може автоматично кешувати відповіді без необхідності додаткової логіки програми.Ця модель довгий час добре слугувала розробникам. Але коли клієнтські додатки стали більш складними, почали з’являтися певні проблеми.
Нові проблеми: надмірне та недостатнє отримання даних
- Ім’я користувача
- URL, який веде до аватарка користувача
- Назви трьох найновіших дописів користувача у блогу
За допомогою звичайного REST API задоволення цієї простої вимоги часто призводить до двох окремих проблем.
1. Надмірне отримання даних
Клієнт запитує GET /api/users/12.
Сервер відповідає повною інформацією про користувача:
{
"id": 12,
"name": "Sarah Connor",
"avatarUrl": "https://cdn.example.com/sarah.jpg",
"email": "sarah@example.com",
"address": { "street": "...", "zip": "...", "country": "..." },
"billingHistory": [ ... ],
"preferences": { ... },
"createdAt": "2021-04-12T08:14:00Z"
}
На екрані були потрібні лише name та avatarUrl, проте були отримані також непов’язані дані про адресу, інформація про оплату та налаштування вподобань. При повільному мобільному з’єднанні передача непотрібних даних споживає батарею та уповільнює відображення контенту. Цей сценарій називається надмірним отриманням даних.
2. Недостатнє отримання даних (проблема N+1 у мережі)
Клієнту все ще потрібні дописи Сари, але у відповіді на запит /api/users/12 вони не були включені.
Тож він здійснює другий запит: GET /api/users/12/posts.
Припустимо, що цей кінцевий пункт повертає лише простий список ідентифікаторів постів. У такому разі клієнту доведеться здійснити три окремі подальші запити: перший — для отримання даних з /api/posts/101, другий — з /api/posts/102, і третій — з /api/posts/103.
На цьому етапі пристрій здійснює кілька послідовних мережевих запитів лише для того, щоб відобразити один екран. Цю модель називають недостатнім отриманням даних.
Яку проблему насправді вирішив GraphQL?
Приблизно у 2012 році Facebook зіткнувся саме з цим бутлеком, переводячи свій флагманський додаток від мобільних веб-версій до нативних додатків. Мобільні з’єднання були ненадійними та повільними, а інтерфейс Facebook складався з тісно взаємопов’язаних даних — користувачі, друзі, коментарі, лайки, реакції.
Розробка окремого REST-ендпоїнта для кожного екрана, щось на кшталт /api/mobile-profile-screen, швидко перетворилася на некерований підхід.
Щоб вирішити цю проблему, Facebook створив GraphQL.
GraphQL — це не технологія баз даних. Це мова запитів для API, яка поєднується з серверною средою виконання, що обробляє запити за допомогою системи типів, визначеної вами.
Замість кількох поїздок до різних ендпоїнтів клієнт надсилає один запит POST із структурованим запитом, який чітко вказує, яку саме форму даних потрібно. Сервер потім повертає JSON-дані, що точно відповідають цій формі.
Підхід GraphQL
Ось як той самий екран профілю міг би отримати свої дані за допомогою GraphQL:
GraphQL
query GetUserProfile {
user(id: "12") {
name
avatarUrl
posts(limit: 3) {
title
}
}
}
А сервер повертає саме те, про що просили, нічого більше:
JSON
{
"data": {
"user": {
"name": "Sarah Connor",
"avatarUrl": "https://cdn.example.com/sarah.jpg",
"posts": [
{ "title": "Building Reliable Systems" },
{ "title": "Understanding Memory Leaks" },
{ "title": "A Guide to Clean Architecture" }
]
}
}
}
Немає марної витрати пропускної здатності через невикористані поля, і не потрібні другий чи треті запити для отримання пов’язаних даних. Один мережевий запит передає саме ті дані, які потрібні для відображення.
Як це працює
Щоб по-справжньому зрозуміти наявні компроміси, корисно подивитися, як кожен підхід обробляє виконання на серверній стороні.
Модель REST на основі кінцевих точок
У фреймворку на кшталт Express.js маршрути безпосередньо пов’язують вхідні URL з функціями-обробниками:
JavaScript
// Express.js REST route
app.get('/api/users/:id', async (req, res) => {
const user = await db.users.findById(req.params.id);
if (!user) return res.status(404).json({ error: 'User not found' });
res.json(user);
});
Розробник бекенду має повний контроль над логікою запитів, викликами бази даних та формою відповіді. Налаштування продуктивності в цьому підході є простим: достатньо звернутися до відповідного обробника кінцевої точки та оптимізувати SQL-запит чи додати кешування.
Модель GraphQL на основі схеми та резолверів
GraphQL будується навколо схеми, разом із окремими функціями, які називаються резолверами:
JavaScript
// Type definitions
const typeDefs = `
type Post {
id: ID!
title: String!
}
type User {
id: ID!
name: String!
avatarUrl: String!
posts: [Post]
}
type Query {
user(id: ID!): User
}
`;
// Resolvers map fields to actual data sources
const resolvers = {
Query: {
user: async (_, { id }) => {
return await db.users.findById(id);
},
},
User: {
posts: async (parentUser) => {
// Runs when the client query includes the 'posts' field
return await db.posts.findByAuthorId(parentUser.id);
},
},
};
Коли надходить запит, двигун GraphQL перевіряє його за схемою, а потім проходить по резолверах, виконуючи їх у форматі, схожому на дерево.
Це надає командам фронтенду великої свободи у формуванні запитів. Але є один важливий момент, на який варто звернути увагу: клієнт визначає форму запиту, проте саме сервер мусить нести витрати на його обробку.
Яку ціну ви платите за GraphQL у закритих процесах
Ця гнучкість не є безкоштовною. Перехід від REST до GraphQL фактично переміщує складність з клієнта на сервер.
Проблема запитів N+1 на рівні бази даних
Розгляньмо ще раз вищевказану налаштування резолвера. Що станеться, якщо клієнт попросить список з 20 користувачів разом із їхніми дописами?
GraphQL
query {
users {
name
posts {
title
}
}
}
- Резолвер
usersвиконується один раз, отримуючи всі 20 рядків за один раз (SELECT * FROM users). - Потім резолвер
postsвиконується окремо для кожного окремого користувача, який було отримано. - Кінцевий результат: ваша база даних мусить обробити 1 запит для списку користувачів плюс ще 20 додаткових запитів для дописів — загалом 21 запит.
За допомогою REST зазвичай це об’єднується в один SQL-запит зі з’єднанням у коді кінцевої точки (SELECT * FROM users JOIN posts...). За допомогою GraphQL потрібні інструменти для групування, такі як DataLoader, щоб зібрати ці розрізнені запити та об’єднати їх перед надсиланням до бази даних.
Проблеми з кешуванням HTTP
Розгляньмо виклик REST наступного типу:
GET /api/articles/42
CDN може з задоволенням зберігати цю відповідь у кеші, скажімо, протягом 10 хвилин. Якщо протягом цього часу 5 000 користувачів звернуться до /api/articles/42, ваш сервер-джерело мусить обробити лише першого — решту бере на себе CDN.
GraphQL порушує цю модель, оскільки майже кожна операція надсилається як POST до єдиного кінцевої точки — /graphql.
Оскільки сам запит знаходиться всередині тіла запиту, а не у URL, звичайні HTTP-кеші, проксі та CDN не можуть визначити, що два запити отримують ідентичні дані. Щоб обійти цю проблему, потрібен складний нормалізований кеш на клієнті (наприклад, Apollo Client або Relay) або спеціально створений шлюз кешування.
Глибина запиту як вектор атаки
У REST немає простого способу для зловмисника запустити процес перетину даних у відносинах на довжину, яку можна довільно збільшити.
Однак у GraphQL зловмисник може надіслати щось на кшталт цього:
GraphQL
query EvilQuery {
user {
posts {
author {
posts {
author {
posts {
title
}
}
}
}
}
}
}
Без захисту, такого як оцінка складності запиту чи обмеження глибини, подібний запит може сповільнити роботу вашої бази даних або вичерпати доступну пам’ять сервера.
REST та GraphQL поруч
FeatureRESTGraphQLОтримання данихПовертає дані фіксованої структури для кожної URLПовертає саме ті поля, які просить клієнтМережеві викликиЧасто потребує кількох поїздокЗазвичай вирішується за одну заявкуHTTP-кешуванняВбудоване, просте та ефективнеВажко досягти; потрібні додаткові інструменти на клієнті чи шлюзіКрива навчанняПлавна, ґрунтується на знайомих конвенціях HTTPКрутіша, вимагає розуміння схем та резолверівЗвітування про помилкиТрадиційні коди статусу HTTP (404, 500, 401)Майже завжди 200 OK, з проблемами, вказаними у полі errorsЗавантаження файлівПросте через multipart form dataПотребує спеціальної логіки мутацій чи попередньо підписаних URL для завантаженняІдеальне застосуванняСервіси у стилі CRUD, мікросервіси, відкриті веб-APIБагаті інтерфейси та додатки для кількох платформ
Здійснення виклику: як приймати рішення
Коли REST є кращим варіантом
- Ваша модель ресурсів є простою. Для типових продуктів, заснованих на механізмах CRUD, таких як панелі адміністрування, панелі керування чи базові інтерфейси магазинів, REST дозволяє прискорити процес розробки та випуску.
- Для вас дуже важливе кешування. Для публічного контенту, який активно читають, такого як блоги, сторінки новин чи каталоги, вбудоване кешування HTTP через CDN є найкращим рішенням з точки зору швидкості та витрат.
- Ви створюєте API для зовнішніх користувачів. Зовнішні розробники вже можуть використовувати кінцеві точки REST за допомогою
curlчиfetch, не вивчаючи спеціалізовані бібліотеки клієнтів.
Коли GraphQL є більш доцільним
- Ви підтримуєте багато різних типів клієнтів. Уявіть продукт із клієнтами для iOS, Android, смарт-годинників та вебу, кожен з яких потребує різного набору даних на основі одних і тих самих джерел.
Підсумок
Жодна з цих технологій не усунула іншу, роблячи її застарілою.
- REST задовольнив потребу у послідовному, масштабованому та придатному для кешування способі доступу до ресурсів у Інтернеті.
- GraphQL задовольнив потребу клієнтів у можливості формувати власні запити до даних, що зменшує кількість марних посилань через повільні чи ненадійні мобільні з’єднання.
Вибір між ними не полягає у слідуванні трендам, а у розумінні справжніх перешкод, з якими стикається ваша система. Якщо ваші найбільші проблеми — це продуктивність у мобільних додатках та постійно зростаючий список кінцевих точок, специфічних для фронтенду, GraphQL може стати ефективним рішенням. Якщо ж для вас найважливішими є простота, надійне кешування та легкий у обслуговуванні бекенд, REST залишається надійним та перевіреним варіантом.