Перейти к содержанию

Представление ресурса

Адрес /api/courses/c001 уже обозначает курс JavaScript. В предыдущих уроках он возвращал объект JSON. Однако курс не является строкой JSON: строка лишь описывает выбранные свойства курса в определённый момент. Если завтра сервис изменит внутреннее хранение, запись способна остаться той же. Если читатель попросит HTML, он получит другую форму тех же сведений.

В этом уроке отделим ресурс, идентификатор и представление. Это поможет определить, что именно клиент читает и заменяет, а также подготовит правильный выбор валидатора. Используем снимок s1 и общедоступное чтение. Все сообщения и документы ниже — авторские иллюстрации договора; ни браузерный интерфейс, ни сервер здесь не запускались.

Один курс и несколько форм

Ресурс нашего API — конкретный курс с устойчивым идентификатором c001. URI указывает на него в пространстве сервиса. Представление содержит данные, которые клиент получает для определённого действия. Сейчас мы определяем JSON с четырьмя полями:

{"id":"c001","title":"Современный JavaScript","topic":"frontend","lessons":40}

Этот объект удобен программе: lessons является числом, topic — машинным идентификатором направления. Клиент может показать карточку, выполнить сравнение или сформировать ссылку. Ему не нужно извлекать количество уроков из русской фразы. При этом JSON не раскрывает внутренний путь Markdown-файла, сведения редактора и историю изменений.

Тот же ресурс имеет учебное HTML-представление. Ниже приведён целиком маленький документ для человека, без стилей и дополнительных ресурсов. Он не является новой страницей нашего рабочего сайта: это возможный выход отдельного API.

<!doctype html>
<html lang="ru">
<head>
  <meta charset="utf-8">
  <title>Современный JavaScript</title>
</head>
<body>
  <main>
    <h1>Современный JavaScript</h1>
    <p>Направление: frontend.</p>
    <p>Количество уроков: 40.</p>
  </main>
</body>
</html>

Человеку документ сообщает тот же учебный факт: название, направление и число уроков. Байты, структура и способ обработки иные. Заголовок Content-Type должен описать реальный выбранный формат. Нельзя выдавать этот HTML как application/json, даже если внутри есть похожие слова.

Представление не обязано содержать всё состояние ресурса. Маленькая карточка и расширенный отчёт могут раскрывать разные свойства. Но если API обещает одну определённую форму JSON, клиент должен знать, какие поля обязательны. Иначе слово «представление» превращается в оправдание произвольных пропусков, и программа больше не способна рассчитывать на результат.

Адрес не является именем файла

Путь API не обязан совпадать с каталогом файлов сервера. Реализация может получить c001 из базы, Markdown, памяти или другого сервиса. Для клиента важна устойчивость публичного адреса. Поэтому мы не выбираем /internal/storage/row-17.json только потому, что такой файл удобен первому прототипу.

Напротив, публичная ссылка курса на сайте и адрес его записи в API могут быть разными ресурсами с разными задачами. Страница учебной серии содержит навигацию и текст, а JSON API — данные каталога. Добавляя API к старому сайту, не нужно автоматически заменить адрес каждого урока на путь API. Сохранение прежних URL относится к договору сайта; новый программный интерфейс расширяет систему отдельным пространством.

Название также не должно служить обязательным ключом записи. Если путь содержит только текущий заголовок, переименование создаёт вопрос о сохранённых ссылках и коллизиях. Наше c001 не сообщает содержание курса, зато стабильно обозначает его. Человек получает название в представлении, программа хранит устойчивый идентификатор.

Можно добавить понятный slug, но его назначение придётся определить отдельно: часть идентичности, декоративный компонент или адрес с перенаправлением. Без такого решения клиент не знает, что делать после переименования. На первом этапе достаточно одного простого идентификатора, чтобы другие темы серии не зависели от случайных правил транслитерации.

Что заменяет PUT

Представление чтения и входной документ изменения отличаются. Сервер возвращает id, а клиент не получает права его переназначить. Поэтому в договоре PUT заменяет редактируемое представление из трёх полей:

{"title":"JavaScript для веба","topic":"frontend","lessons":40}

Здесь все поля обязательны. Сервис не трактует отсутствие lessons как «оставить старое число» и не переносит id из тела. После принятого изменения чтение того же /api/courses/c001 показывает прежний идентификатор и новый заголовок. Внутренние сведения аудита остаются вопросом реализации, поскольку не входят в данное представление.

Такое разделение защищает контракт от случайного массового присваивания. Если клиент получит объект чтения, добавит внутренний флаг и отправит его обратно, сервер не обязан доверять всем ключам. В нашем API неизвестные входные поля дают предметный отказ. Клиент формирует именно разрешённый документ изменения, а не сериализует всё, что оказалось в памяти формы.

Назвать этот вход «полной заменой» можно только в пределах определённого представления. Это не обещание уничтожить весь объект базы данных. Если документация не объясняет границу, два клиента способны по-разному понять PUT: один сохранит служебные поля, другой ожидает их стирания. Явное описание трёх полей устраняет это расхождение до выбора фреймворка.

Ссылка на вариант и ссылка после создания

В HTTP существует Content-Location, который может описывать адрес представления в конкретном ответе. Это не то же самое, что Location после создания. Первое поле уточняет связь переданного содержимого с ресурсом, второе в нашем POST сообщает место новой записи. Их сходное название не означает взаимозаменяемость.

Наш начальный API не требует отдельного Content-Location, поскольку выбранная форма уже описана типом и условиями запроса. Добавлять поле стоит, когда его значение имеет полезный согласованный смысл, например когда вариант доступен по собственному адресу. Выдуманная ссылка на внутренний файл не делает договор яснее и может раскрыть устройство хранения.

Аналогично размер данных и размер изображения курса относятся к представлению только при явно объявленных полях. Если карточка получит ссылку на обложку, это ещё один ресурс с собственным типом и правилами доступа. JSON сообщает адрес, но не гарантирует, что браузер уже получил изображение. При проектировании полезно держать границы каждого сообщения видимыми.

Такая дисциплина позволяет расширять каталог постепенно. Новый вариант не должен случайно менять смысл прежних идентификаторов, а внутренняя оптимизация не должна заставлять клиента угадывать адрес файлов. Сначала определите, что получает потребитель, затем выбирайте способ генерации.

Версия представления

Для последующих условных запросов каждому выбранному представлению нужен подходящий валидатор. JSON получит учебную сильную метку "c001-json-r1", HTML — "c001-html-r1". Совпадение предметных сведений не делает два набора байтов одинаковыми. Клиент сохраняет метку вместе с тем форматом, который получил.

Если HTML-шаблон изменится, а данные курса останутся прежними, байты HTML станут другими. Сильный валидатор HTML должен измениться, хотя JSON может сохранить прежнюю метку. Именно поэтому одной внутренней версии строки базы недостаточно для любого сильного ETag: в его правильность входит генерация выбранного представления.

Наш начальный стенд использует фиксированную сериализацию JSON и один вариант без сжатия. Так удобно объяснить причину изменений. В реальном сервисе форматирование, локализация и Content-Encoding добавляют варианты. Сильный валидатор обязан отражать нужные различия, либо система должна осознанно выбрать слабую метку с другими правилами сравнения. Представления и валидаторы в HTTP.

Не стоит использовать непрозрачный ETag как редактируемый номер курса. Его точное значение приходит с ответом и затем возвращается в условии запроса. Когда пользователь меняет заголовок, он отправляет новый title, а не предполагаемую строку следующей метки. Такой порядок позволяет серверу свободно изменить способ вычисления валидатора.

Попробуйте теперь объяснить три изменения: переименование курса, обновление HTML-шаблона и перенос данных в другую базу. Первое меняет предметные данные, второе только соответствующее представление, третье может вообще не затронуть публичный договор. Выбор конкретной формы ответа станет следующим шагом: согласуем её через Accept и объявим отказ, когда приемлемого варианта нет.

Оглавление курса