Введение: REST vs GraphQL — старая битва в новом мире Swift
Когда-то давно, в эпоху Objective-C и диспетчеризации по селекторам, выбор между REST и GraphQL не стоял так остро. REST был королём, а JSON — его верным оруженосцем. Но времена изменились. Swift стал языком с сильной системой типов, появились async/await, Codable и мощные фреймворки вроде Apollo iOS. Теперь вопрос «REST или GraphQL?» звучит иначе: «Что даст моему Swift-проекту больше профита?».
В этой статье мы не будем пересказывать википедию. Мы возьмём реальный пример iOS-приложения для образовательной платформы (да, той самой PythonLib) и посмотрим, как каждый подход ведёт себя в коде на Swift. Вы узнаете, когда REST всё ещё рулит, а когда GraphQL — настоящий спаситель.
Коротко о главном: чем REST отличается от GraphQL
Для тех, кто только вкатывается в тему — базовые понятия.
- REST — это архитектурный стиль, где каждый ресурс (пользователь, курс, урок) имеет свой URL. Вы получаете фиксированную структуру данных, которую сервер решил вам отдать. Часто — целую пачку лишнего.
- GraphQL — это язык запросов. Вы сами решаете, какие поля нужны. Одна точка входа (
/graphql) и один запрос может заменить несколько REST-запросов.
На бумаге GraphQL выглядит как серебряная пуля. Но в Swift-проектах есть нюансы, которые мы сейчас разберём на практике.
Пример 1: Получаем профиль пользователя и его курсы
REST-подход на Swift (URLSession + Codable)
Допустим, у нас есть классический REST API. Чтобы показать профиль и список курсов, нужно два запроса:
// Модели
struct UserProfile: Codable {
let id: Int
let name: String
let email: String
let avatarURL: String
}
struct Course: Codable {
let id: Int
let title: String
let progress: Double
}
// Сетевой слой
func fetchUserProfile(userId: Int) async throws -> UserProfile {
let url = URL(string: "https://api.pythonlib.ru/users/\(userId)")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(UserProfile.self, from: data)
}
func fetchCourses(userId: Int) async throws -> [Course] {
let url = URL(string: "https://api.pythonlib.ru/users/\(userId)/courses")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode([Course].self, from: data)
}
// Использование
let profile = try await fetchUserProfile(userId: 42)
let courses = try await fetchCourses(userId: 42)
Всё просто и предсказуемо. Но есть проблема: мы получаем все поля профиля, даже если нам нужны только имя и email. А ещё — два сетевых запроса. На медленном соединении это боль.
GraphQL-подход на Swift (Apollo iOS)
Теперь то же самое, но через GraphQL. Сначала определяем запрос в файле .graphql:
query GetUserWithCourses($userId: ID!) {
user(id: $userId) {
id
name
email
courses {
id
title
progress
}
}
}
Apollo генерирует типизированный Swift-код автоматически. В Xcode это выглядит так:
import Apollo
let apolloClient = ApolloClient(url: URL(string: "https://api.pythonlib.ru/graphql")!)
func fetchUserWithCourses(userId: Int) async throws -> GetUserWithCoursesQuery.Data {
return try await withCheckedThrowingContinuation { continuation in
apolloClient.fetch(query: GetUserWithCoursesQuery(userId: userId)) { result in
switch result {
case .success(let graphQLResult):
if let data = graphQLResult.data {
continuation.resume(returning: data)
} else if let errors = graphQLResult.errors {
continuation.resume(throwing: errors.first!)
}
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
// Использование
let data = try await fetchUserWithCourses(userId: 42)
let userName = data.user?.name
let courseTitles = data.user?.courses?.compactMap { $0?.title }
Видите разницу? Один запрос — и мы получили ровно то, что просили. Никаких лишних полей. Для мобильного разработчика это означает меньше трафика и более быстрый рендеринг экрана.
Типизация: Codable vs сгенерированные модели
В REST мы пишем модели руками. Это гибко, но есть риск ошибиться в названии поля или типе данных. Компилятор вас не спасёт, если сервер вернёт name как строку, а вы ожидаете Int.
GraphQL в связке с Apollo даёт строгую типизацию на уровне генерации кода. Если бэкенд изменит тип поля, ваш проект просто не соберётся. Для больших команд это must-have. Для маленьких проектов — иногда избыточный оверхед.
Совет PythonLib: Если вы пишете pet-проект или MVP, REST + Codable — ваш выбор. Быстро, дешево, сердито. Если же у вас команда из 5+ iOS-разработчиков и сложная бизнес-логика — GraphQL сэкономит вам часы на синхронизации.
Производительность: когда GraphQL может навредить
Звучит парадоксально, но GraphQL не всегда быстрее. Рассмотрим сценарий: у вас есть список из 100 курсов. В REST вы делаете один запрос к /courses и получаете всё разом. Сервер может закэшировать этот ответ на CDN.
В GraphQL вы тоже делаете один запрос, но сервер каждый раз вычисляет ответ динамически. Если в запросе есть вложенные связи (например, для каждого курса подгружать автора и отзывы), сложность запроса растёт. Без грамотной настройки batch loading и DataLoader GraphQL может породить N+1 проблему.
Для Swift-разработчика это значит: если ваш бэкенд не оптимизирован под GraphQL, вы рискуете получить ответ дольше, чем при REST. Всегда тестируйте реальные сценарии.
Кэширование: REST проще, GraphQL сложнее
В REST кэширование на уровне HTTP — это стандарт. ETag, Cache-Control, Last-Modified — всё работает из коробки. Вы можете использовать URLCache в Swift и получить кэш бесплатно.
let config = URLSessionConfiguration.default
config.urlCache = URLCache(memoryCapacity: 50_000_000, diskCapacity: 100_000_000)
let session = URLSession(configuration: config)
С GraphQL всё сложнее. Запросы — это POST (обычно), и HTTP-кэш не работает. Приходится использовать нормализованный кэш на клиенте. Apollo iOS предоставляет встроенный кэш, но его нужно правильно настроить:
let cache = NormalizedCache()
let store = ApolloStore(cache: cache)
let client = ApolloClient(networkTransport: