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.

Проверьте себя
1. Почему курсорная пагинация предпочтительнее offset?
AОна проще в коде
BПо индексу WHERE id > cursor база прыгает к нужным строкам, а OFFSET листает и отбрасывает предыдущие
COffset не поддерживается в GraphQL
DКурсоры всегда числа
2. Как в GraphQL безопасно убрать устаревшее поле?
AВыпустить /v2 схемы
BСразу удалить его
CПометить @deprecated с причиной, дождаться по метрикам прекращения использования и затем удалить
DСкрыть его в UI