Схема учебного GraphQL API
GraphQL помогает клиенту описать, какие сведения он хочет получить, а серверу — установить проверяемый договор доступных типов и полей. Построим учебное API «Академия» для курсов, авторов и записи студентов. Один проект будет развиваться от выполнения запроса внутри программы до HTTP-клиента. Начнём со схемы: без неё легко спутать GraphQL с произвольной передачей JSON или принять язык запроса за устройство базы.
Предполагается знакомство с JavaScript, объектами, функциями и базовым HTTP. Используем Node.js ветки 24 LTS и GraphQL.js 17.0.2, стабильный выпуск, зафиксированный по первичному репозиторию. На первом шаге HTTP-сервера ещё нет: программа выполняет запрос к схеме локально и печатает результат. Это позволяет увидеть работу языка отдельно от заголовков, сетевых ошибок и браузерного интерфейса.
Предметный договор Академии
У нас три вымышленных курса: «Статический сайт» с ID c01, «HTTP API» с ID c02 и «Доступный интерфейс» с ID c03. У каждого есть название, уровень и внутренний идентификатор автора. Автор появится в схеме позже. Пока клиент может запросить ID, title и level. Доступность поля в объекте JavaScript не делает его автоматически доступным в API: схема перечисляет именно публичный договор.
Уровень описан enum Level с вариантами BEGINNER и INTERMEDIATE. Такой тип задаёт известное множество, а не любую строку. Если хранилище вернёт непредусмотренное значение, исполнение не должно молча выдать его как допустимый уровень. Схема помогает обнаружить несогласованность границы, но не создаёт миграции данных автоматически. Формат предметных значений и способ их хранения остаются задачей приложения.
Ниже приведён полный schema.graphql состояния lesson-01. В схеме есть тип Course и корневой Query, который задаёт доступные операции чтения.
enum Level { BEGINNER INTERMEDIATE }
type Course { id: ID! title: String! level: Level! }
type Query { courses: [Course!]! course(id: ID!): Course }
Восклицательный знак означает невозможность null в данной позиции результата. У courses запрещены null и для списка, и для каждого элемента. Пустой список допустим: отсутствие элементов не равно отсутствию значения списка. У course(id: ID!) результат допускает null, потому что конкретный ID может не существовать. Аргумент id обязателен и также не может быть null. Это четыре разные позиции договора, хотя запись выглядит компактно.
Схема и источник данных
В data.mjs находятся обычные JavaScript-объекты. Они не являются GraphQL-сервером сами по себе. buildSchema читает текст договора, а graphql принимает схему, документ запроса и функции получения данных. Корневой объект root содержит courses и course. Первая функция возвращает список, вторая ищет по ID и отдаёт null при отсутствии карточки. Эти функции связывают язык запроса с нашим предметным источником.
GraphQL не требует SQL и не определяет место хранения. Сейчас данные находятся в памяти; позже их можно получать из базы, файла или другого сервиса. Однако нельзя объявить такую замену бесплатной: новые источники приносят задержки, ошибки и правила доступа. Схема сохраняет форму договорённости с клиентом, но реализация функций должна учитывать реальную границу хранилища. Наш курс специально начинает с небольшого собственного набора, без удалённых ресурсов.
Клиент передаёт документ { courses { id title } }. В результате должны присутствовать только запрошенные поля, а level не появляется, хотя он доступен в схеме. Это проекция данных. Она не означает, что сервер никогда не прочитал уровень из исходного объекта. Выбор формы ответа и оптимизация чтения базы — связанные, но отдельные вопросы. Позже разберём вложенные связи и пакетирование, где стоимость получения данных становится заметнее.
Проверка документа перед выполнением
Неизвестное поле или отсутствие обязательного аргумента является ошибкой запроса. GraphQL.js разбирает документ и проверяет его по схеме прежде, чем выполнять допустимую операцию. Если клиент запросил price, которого здесь нет, добавление случайного свойства price в объекте данных не исправит договор. Нужно либо изменить документ на известные поля, либо осознанно расширить схему. Такое разделение полезно при совместной работе над клиентом и сервером.
GraphQL-ответ использует ключ data, а при проблемах также errors. На данном шаге ожидаем успешный data со списком названий и идентификаторов. Ошибки исполнения и частичный результат будут рассмотрены отдельно, поэтому не следует считать, что любой errors обязательно означает полное отсутствие полезных данных. Форма конверта — часть языка, а реальный HTTP-статус появится в третьем уроке вместе с выбранным транспортным договором.
Для самостоятельного прохождения откройте папку lesson-01, установите заявленную зависимость и запустите npm start. В архиве есть package.json, schema.graphql, data.mjs, academy.mjs и operations.graphql. Установленных пакетов и lock-файла выполненной установки нет: автор не запускал программу или зависимости. Ожидаемая печать результата описана по коду. Читатель может изучить проект сначала как текст, а выполнение своей среды отделить от редакционного просмотра сайта.
Затем измените выбор полей на id level. Предметные карточки остаются прежними, однако ответ получает другую проекцию. Попытка запросить title без ID также допустима: язык не заставляет включать ключ объекта в каждую выдачу. Но клиенту, который объединяет страницы списка, устойчивый идентификатор обычно полезен. Требования интерфейса определяют выбор документа, тогда как схема задаёт предел допустимых возможностей.
Другой вариант — course(id:"missing") { id title }. В нашей реализации ожидается null для course, потому что поиск ничего не нашёл. Это не внутреннее исключение и не возвращение пустого Course с выдуманным названием. Наличие nullable результата помогает выразить отсутствие предметного объекта. В следующих главах увидим, как такая граница работает при ошибке отдельного вложенного поля и почему слишком широкое использование non-null влияет на потерю соседних данных.
Следующий урок сохранит ту же схему и покажет псевдонимы, фрагменты и именованную операцию. Мы пока не добавляем новый источник или мутацию, чтобы выбор полей оставался главным предметом объяснения. Когда документ запроса станет понятным, перейдём к переменным и HTTP-конверту, не смешивая синтаксис GraphQL с правилами сетевого сервера.
Схема не является снимком текущего списка. Даже если в данных три курса, тип Query не перечисляет их названия. Он описывает форму допустимого ответа, поэтому пополнение собственного набора не требует нового определения Course для каждой карточки. Это различие между типом и экземпляром удобно удерживать с самого начала: новая предметная строка и новое публичное поле требуют разных изменений проекта.
Исходники урока
Оглавление серии · Скачать исходники. Полное самостоятельное состояние находится в lesson-01. Учебные приложения, зависимости, база и проверки при подготовке не запускались; описанный результат ожидается по исходникам. Материалы пока находятся в локальной редакционной версии.
Первичные источники: документация 1, документация 2.