В прошлом уроке мы назвали схему контрактом между фронтендом и бэкендом. Теперь научимся её писать. Представьте анкету при записи в клинику: в ней перечислены все поля (имя, дата рождения, телефон), у каждого поля понятный формат ответа, а обязательные помечены красной звёздочкой. Схема GraphQL — такая же анкета, только для данных вашего API: она заранее описывает, какие бывают типы данных, из каких полей они состоят и какие поля обязательны. Сервер не вернёт и не примет ничего, чего нет в схеме. В этом уроке мы опишем схему блога — с пользователями, постами и комментариями — и будем использовать её на протяжении всего модуля.

SDL: язык, на котором пишут схему

Схему описывают на SDL (Schema Definition Language, «язык описания схемы»). Это не JavaScript и не JSON, а маленький декларативный язык: вы не пишете, как получить данные, а лишь описываете, какими они будут. Главный «кирпичик» — объявление типа:

graphql
type Post {
  id: ID!
  title: String!
  body: String!
  published: Boolean!
}

Разберём строку title: String!:

  • title — имя поля;
  • String — тип значения (текст);
  • ! — значение никогда не бывает null; об этом знаке поговорим подробно чуть ниже.

Есть договорённости об именах, которые соблюдают почти все: типы пишут в PascalCase (Post, CreatePostInput), поля и аргументы — в camelCase (createdAt), значения enum — в SCREAMING_SNAKE_CASE (NEWEST). Комментарии начинаются с #, сервер их игнорирует. Порядок объявления типов не важен: тип можно использовать раньше, чем он описан.

Скалярные типы: «атомы» данных

В конце любого запроса должны остаться конкретные значения: число, текст, «да/нет». Такие «листья» называются скалярами. В GraphQL есть пять встроенных:

ТипЧто хранитПример
Intцелое число (32 бита)42
Floatчисло с дробной частью19.9 (цена в AZN)
Stringтекст в UTF-8"Hello"
Booleanлогическое значениеtrue
IDуникальный идентификатор"42"

Три нюанса, на которых спотыкаются новички:

  • ID в ответе — всегда строка. Даже если в базе это число 42, клиент получит "42". Идентификатор нужен, чтобы находить и сравнивать записи, а не считать: складывать два ID бессмысленно.
  • Int — 32-битное. Максимум — 2 147 483 647 (около 2,1 млрд). Для чисел побольше (например, идентификаторов из соцсетей) берут String.
  • Дат в списке нет. Встроенного типа для даты и времени нет. Обычно дату передают строкой ISO 8601, например "2026-03-15T10:30:00Z", — так мы и поступим с полем createdAt. В больших проектах объявляют свой скаляр (scalar DateTime), например с помощью библиотеки graphql-scalars.

Объектные типы и связи между ними

Главный вид типов — объектный: именованный набор полей. Опишем блог. В нём три сущности: пользователь (User), пост (Post) и комментарий (Comment):

graphql
type User {
  id: ID!
  name: String!
  email: String!
  posts: [Post!]!
}

type Post {
  id: ID!
  title: String!
  body: String!
  published: Boolean!
  author: User!
  comments: [Comment!]!
  createdAt: String!
}

type Comment {
  id: ID!
  text: String!
  author: User!
  post: Post!
}