Best practices и эволюция схемы
Финальный свод зрелых практик: пагинация вместо «отдать всё», эволюция схемы без версий через @deprecated и защита сервера от тяжёлых и злонамеренных запросов.
GraphQL спроектирован так, чтобы расти без болезненных версий вроде /v2. Но за гибкость надо платить дисциплиной: пагинацией, лимитами и аккуратными депрекейтами.
Мы прошли путь от первого запроса до клиентского кэша. Соберём практики, которые отличают учебный сервер от продакшен-готового.
Пагинация: не отдавай всё
Поле users: [User!]!, возвращающее все записи, — мина замедленного действия. Промышленный стандарт — курсорная пагинация (паттерн Connections): клиент просит «первые N после курсора», сервер отдаёт страницу и курсор для следующей.
type Query {
users(first: Int!, after: String): UserConnection!
}
type UserConnection {
edges: [UserEdge!]!
pageInfo: PageInfo!
}
type UserEdge { node: User!, cursor: String! }
type PageInfo { hasNextPage: Boolean!, endCursor: String }
Курсор лучше offset: по индексированному WHERE id > cursor база прыгает сразу к нужным строкам, тогда как OFFSET заставляет её пролистывать и отбрасывать предыдущие.
Эволюция без версий: @deprecated
В REST новую несовместимую версию выкатывают как /v2. GraphQL поощряет аддитивную эволюцию: добавляй новые поля, а старые помечай директивой @deprecated с причиной — клиенты мигрируют постепенно, ничего не ломается:
type User {
fullName: String!
name: String! @deprecated(reason: "Используй fullName")
}
Инструменты покажут предупреждение, метрики подскажут, когда поле перестали запрашивать — и только тогда его безопасно удалить.
Защита от тяжёлых запросов
Гибкость = риск: один глубокий или широкий запрос может перегрузить сервер. Базовая оборона — лимит глубины (depth limiting) и анализ сложности (cost analysis), которые отсекают запрос до резолверов.
query { Глубина:
user { 1 user
posts { 2 posts
comments { 3 comments
author { 4 author
posts { ... } 5 posts <-- лимит, скажем, 5
}
}
}
}
}
depthLimit(5) -> запрос глубже отклоняется ДО выполнения
Смоделируем проверку глубины запроса на JS:
// дерево запроса: объект = вложенность, true = лист
const query = {
user: { posts: { comments: { author: { posts: true } } } }
};
function depth(node) {
if (node === true) return 1;
let max = 0;
for (const k in node) max = Math.max(max, depth(node[k]));
return 1 + max;
}
function guard(query, limit) {
const d = depth(query);
return d > limit
? "Отклонено: глубина " + d + " > лимита " + limit
: "OK, глубина " + d;
}
console.log(guard(query, 5)); // OK, глубина 5
console.log(guard(query, 3)); // Отклонено: глубина 5 > лимита 3
Попробуй сам ▶ — добавь ещё уровень вложенности в query и посмотри, как растёт глубина и срабатывает лимит. Так сервер защищается от запросов-бомб.
Частые ошибки
- Списки без пагинации.
allUsers: [User!]!однажды вернёт миллион записей и положит и сервер, и клиент. - Удалять поля без депрекейта. Резкое удаление поля ломает живых клиентов; сначала
@deprecated, затем — по метрикам — удаление. - Оставить introspection и безлимитную глубину в проде. Открытая интроспекция отдаёт всю схему атакующему, а отсутствие лимитов открывает дорогу запросам-бомбам.
Best practices
- С самого начала проектируй списки с курсорной пагинацией (Connections) — переделывать потом дорого.
- Эволюционируй схему аддитивно: новые поля +
@deprecatedна старые, удаление — только по данным об использовании. - В проде включай depth/complexity-лимиты, выключай интроспекцию без необходимости и используй persisted queries для доверенных клиентов.
Итоги
Зрелый GraphQL — это пагинация вместо «отдать всё» (курсоры лучше offset), эволюция схемы без версий через @deprecated и защита сервера лимитами глубины и сложности. Эти практики превращают гибкий, но потенциально опасный API в надёжный продакшен-сервис. На этом наш путь от первого запроса до боевой схемы завершён — теперь у тебя есть карта всего GraphQL.