Путь и параметры запроса
Контракт JSON описывает данные курса, но клиенту ещё нужно правильно выбрать цель. В нашем API путь обозначает коллекцию или запись, а параметры запроса задают выборку из коллекции. Такое разделение помогает сохранить идентичность ресурса и воспроизводить состояние каталога по адресу.
В этом уроке определим, какие сведения принадлежат пути, а какие — query. Рассмотрим кодирование значений и ошибочные параметры. Пока не вычисляем страницы: пагинация появится после валидации и повторов. Все адреса относятся к учебному origin https://catalog.example, а примеры не отправлялись по сети.
Идентичность в пути
Одна запись читается по /api/courses/c001. Сегмент c001 выбирает курс JavaScript. Если заменить его на c002, цель станет другой записью, даже при одинаковом методе и Accept. Отдельный параметр id=c001 для этого действия нам не нужен.
GET /api/courses/c002 HTTP/1.1
Host: catalog.example
Accept: application/json
Предполагаемое тело содержит курс HTML/CSS с 32 уроками. Запись существует независимо от того, встречалась ли она на текущей странице списка. Клиент способен перейти по её адресу прямо из сохранённой ссылки. Такой договор удобен для формы редактирования и позволяет не хранить скрытый «текущий id» в сессии сервера.
У нашего идентификатора нет символов, требующих специального кодирования сегмента. Это упрощение исходных данных, а не общее правило всех URL. Если использовать произвольное название курса как сегмент, пробелы, символы slash и Unicode потребуют явного построения и разбора. Поэтому устойчивый короткий id избавляет первый контракт от лишней сложности.
Граница сегмента имеет значение. Значение с закодированным slash нельзя бездумно декодировать несколько раз и превращать в новый маршрут. Реализация маршрутизации, прокси и приложение должны одинаково понимать адрес. На уровне учебного договора id имеет ограниченную форму c и три цифры; остальные формы не принимаются как произвольный путь к файлу.
Настройки списка
Для выбора только фронтенд-курсов обращаемся к коллекции с query. Путь остаётся прежним, потому что предметом чтения является список, а не одна запись.
GET /api/courses?topic=frontend&limit=2 HTTP/1.1
Host: catalog.example
Accept: application/json
Параметр topic задаёт направление, limit ограничивает количество элементов одного ответа. Сам по себе limit не выбирает порядок и не означает номер страницы. До добавления пагинации определяем только допустимость: целое значение от одного до 50, значение по умолчанию два. Другие настройки должны получить самостоятельные правила, а не случайно появиться в обработчике.
В query значения приходят в текстовой форме. Сервер должен разобрать limit как целое по своему договору и затем проверить диапазон. Это отличается от JSON-поля lessons, которое уже имеет числовой тип после JSON-разбора. Универсальная функция «привести всё к числу» часто принимает неожиданные пробелы, дроби и экспоненты; лучше определить допустимую запись для параметра отдельно.
В нашем API topic допускает ровно одно значение из перечня. Запрос topic=frontend&topic=quality отвергается как неоднозначный, а не выбирает первый или последний вариант. Если понадобится многозначный фильтр, его следует ввести сознательно: повторяющиеся параметры, список или другой формат. Пока одиночность является частью договора.
Кодирование относится к значению
URI содержит зарезервированные символы. Знак & разделяет параметры, а = связывает имя и значение. Если пользовательский текст содержит эти символы, простое склеивание строк меняет структуру query. Правильный построитель адреса кодирует значение в его компоненте, а не просит человека заранее заменять символы вручную.
Покажем небольшой полный пример построения адреса средствами браузерного JavaScript. Он служит отдельной иллюстрацией и не добавляет к серверу новый фильтр: topic остаётся уже определённым параметром.
const url = new URL('/api/courses', 'https://catalog.example');
url.searchParams.set('topic', 'frontend');
url.searchParams.set('limit', '2');
const requestUrl = url.href;
Ожидаемая строка адреса содержит путь и два параметра в указанном порядке. Программа не отправляет запрос: результат только формирует значение. Для будущего свободного поиска с текстом C# & API такой же подход сохранит ampersand внутри значения. Сам поиск сейчас не входит в принятый контракт, поэтому полученный дополнительный параметр без расширения API должен вызвать отказ.
Обратите внимание на плюс: правила form-urlencoded, которые используют URLSearchParams, могут представлять пробел как +; обычный компонент URI не объявляет плюс универсальным пробелом во всех контекстах. Нельзя применять одну ручную замену к пути, query и фрагменту. Пользуйтесь инструментом для нужного компонента и согласованным разбором на сервере. Компоненты и кодирование URI.
Тело не дополняет фильтр GET
У нашего GET нет тела. Если клиент хочет задать topic, он помещает его в объявленный query, а не отправляет JSON с тем же полем. Общее наличие возможностей передачи содержимого не создаёт универсальной семантики тела GET. Некоторые посредники и серверы его отвергают, а общий контракт метода не превращает содержимое в новую цель.
Поэтому документ {"topic":"frontend"} в теле не является альтернативой адресу с topic. Будущая библиотека клиента может позволить собрать такой объект в памяти, но сервер не обязан применять его к чтению коллекции. Явное ограничение уменьшает различия между программами, которые иначе отправляли бы настройки разными путями.
Если в дальнейшем сложный поиск потребует большого структурированного запроса, можно спроектировать отдельную операцию. Она будет иметь согласованные метод, тело и правила результата. Не стоит незаметно вводить её в существующий GET, потому что один клиент столкнулся с неудобством длины адреса. Стабильный API расширяется новыми объявленными возможностями.
Для текущей выборки достаточно маленького query: направление, порядок, размер и продолжение. Каждая настройка имеет отдельный смысл, проверку и значение по умолчанию. Сохраняя этот набор, можно объяснить любую строку адреса без чтения внутреннего кода сервера.
Неизвестные и пустые параметры
Наш сервис отвергает неизвестный параметр, например colour=purple, статусом 400. Это осознанная строгость учебного API. Она обнаруживает опечатки: клиент с lmit=2 не получает случайно огромную страницу, думая, что ограничение действует. Другой сервис может игнорировать расширения, но тогда должен сообщить такую политику своим потребителям.
Пустое topic= не равно отсутствию topic. Отсутствие означает отсутствие фильтра, пустая строка не входит в перечень направлений и является ошибкой. Иначе клиент не сможет отличить намерение показать всё от повреждённого значения формы. Значение по умолчанию применяем только к действительно отсутствующему параметру, а не ко всем ошибочным входам.
Порядок одиночных параметров не меняет выбранную выборку. Два адреса с topic и limit в разных последовательностях должны обозначать одинаковые настройки нашего API. Но это не обещает, что любой кеш автоматически нормализует query. Если нужны устойчивые ссылки и ключи, клиенту удобно формировать параметры в одном порядке, а серверу — определять семантику после разбора.
Фрагмент после # не передаётся как часть цели HTTP-запроса. Его можно использовать на странице для локального положения, но серверный фильтр API через него не получит значение. Поэтому адрес /api/courses#topic=frontend не равен нашему query-примеру. Клиентская маршрутизация может читать фрагмент самостоятельно, однако это другой механизм с собственным договором.
Наконец, не размещайте секретные значения в параметрах только потому, что так проще сформировать ссылку. Адрес способен попасть в историю, журнал и копирование. Позже аутентификация получит отдельную модель; здесь это напоминание о назначении query, а не готовая схема защиты. Теперь цель и настройки определены. Следующий урок поставит проверки перед сохранением и покажет, как разные виды неприемлемого запроса сохраняют каталог неизменным.