JavaRush /Курсы /Java Server /Куда класть данные запроса

Куда класть данные запроса

Java Server
8 уровень , 3 лекция
Открыта

1. Размещение данных — часть дизайна API

Когда вы только начинаете, кажется, что главное — «чтобы сервер понял». А если сервер понял, то можно и id в query, и status в header, и title в path, и ещё сверху продублировать всё в body — для надёжности, как скотч на чемодан. Проблема в том, что API читают не только компьютеры, но и люди. А люди, в отличие от компьютеров, устают, путаются и начинают ненавидеть ваш контракт тихой профессиональной ненавистью.

Суть простая: разные части HTTP-запроса изначально придуманы для разных ролей. Path отвечает за «куда идём» и «какой ресурс трогаем», query — за уточнения чтения («покажи только это», «отфильтруй вот так»), headers — за служебные правила обработки (формат, ожидания, особенности), а body — за основную полезную нагрузку при создании/изменении. Если вы соблюдаете эту логику, запрос читается почти как предложение на человеческом языке. Если нет — получается ребус, где /api/v1/reading-list/42?id=42 выглядит как попытка объяснить серверу очевидное, но с тревогой.

Важно ещё и то, что разные части запроса «живут разной жизнью». URL чаще видно в логах и истории запросов, заголовки легко перепутать по смыслу, query-строка быстро превращается в длинную простыню, а body обычно содержит то, ради чего запрос вообще делался. Поэтому вопрос «куда класть данные» — это не декоративная эстетика, а основа дисциплины, которая делает взаимодействие предсказуемым.

2. Path: идентичность ресурса

Path — это то, что чаще всего воспринимается как «адрес» в HTTP. И это хорошая интуиция: путь отвечает на вопрос «какой именно ресурс мы хотим тронуть». Поэтому path идеален, когда вы указываете идентичность объекта. Если вы хотите один конкретный элемент списка чтения — вы обращаетесь к нему как к конкретному «адресу», и id в этом случае живёт в path как часть маршрута.

Представьте, что reading-list — это «дом», а конкретный элемент списка — «квартира». Тогда /api/v1/reading-list — это дом, а /api/v1/reading-list/42 — квартира №42. И если вы хотите «в квартиру 42», бессмысленно писать на конверте «Дом reading-list, квартира 42, и ещё раз квартира=42 внизу мелким шрифтом». Адрес должен быть один и понятный.

Мини-иллюстрация на уровне «просто строки» (пока без реального клиента и сервера):

// Path — это "адрес" ресурса: меняем path -> меняем объект разговора.
String url1 = "/api/v1/reading-list";    // коллекция (дом)
String url2 = "/api/v1/reading-list/42"; // конкретный элемент (квартира)

// Для наглядности просто печатаем получившиеся URL.
System.out.println(url1); // /api/v1/reading-list
System.out.println(url2); // /api/v1/reading-list/42

Важная мысль: path-параметр (например, 42) обычно должен быть таким, чтобы его можно было легко прочитать, распарсить и использовать как идентификатор. В учебных примерах чаще всего это число (long) или строка (внешний ID). Но «запихивать» в path большие куски данных — плохая идея. Path — это место для «кто именно», а не для «вот подробное описание книги на 3 абзаца».

Есть ещё один полезный признак: если вы поменяете значение в path, вы почти наверняка поменяете «объект разговора». /api/v1/reading-list/41 и /api/v1/reading-list/42 — это два разных объекта. Это и есть идентичность.

3. Query: фильтры, поиск, уточнения

Query-параметры (?key=value&key2=value2) — это как уточняющие фразы в просьбе. Они не меняют базовый «адрес» ресурса, но меняют то, как вы хотите его прочитать или отобрать. Поэтому query отлично подходит для фильтрации списка, для поиска по части названия, для простых дополнительных условий. Ключевое слово — «уточнение»: вы всё ещё читаете /api/v1/reading-list, просто просите показать его не целиком, а в определённом виде.

Например, вы хотите получить список книг только со статусом PLANNED или только те, в названии которых встречается слово clean. Это не «другой ресурс», это тот же список, но с фильтром.

// Query — это "уточнения чтения": фильтры, поиск, опциональные флаги.
String url = "/api/v1/reading-list?status=PLANNED&title=clean";

// Важно: базовый ресурс тот же (/api/v1/reading-list), меняется только "как читать".
System.out.println(url); // /api/v1/reading-list?status=PLANNED&title=clean

Здесь важно не скатиться в две крайности. Первая — пытаться идентифицировать один конкретный ресурс через query: /api/v1/reading-list?id=42. Формально это может работать, но смыслово вы сделали запрос к коллекции, а потом попросили «ну пожалуйста, верни только один». Обычно читается хуже, чем /api/v1/reading-list/42. Вторая крайность — превращать query в чемодан без ручки и запихивать туда всё, включая «основные данные создания». Когда query-строка начинает выглядеть как небольшая новелла, ваш URL становится трудночитаемым и ломает главную идею query: быть коротким набором условий.

Чтобы зафиксировать разницу между path и query, полезно смотреть на неё не философски, а практически:

Вопрос Path Query
Что описывает? «Какой ресурс?» «Какие уточнения чтения?»
Обычно обязателен? Да, чтобы попасть в нужный объект Обычно нет, часто опционален
Пример /api/v1/reading-list/42 /api/v1/reading-list?status=PLANNED
Плохой запах «в path лежат фильтры» «в query лежит идентификатор одного объекта»

И ещё одна мелочь, которая полезна для мозга новичка: query почти всегда особенно уместен в GET, потому что GET обычно читается как «дай мне данные», и удобно, когда все параметры чтения видны прямо в URL. Мы не уходим в технические детали про GET и body, но практическая привычка такая: фильтры и поиск для чтения списка — в query.

4. Headers: служебные сведения

Headers часто воспринимаются новичками как «ещё одно место, куда можно положить данные». Технически — да, положить можно почти что угодно. Но смыслово headers — это не «данные ресурса», а служебные правила обработки запроса и ответа. Представьте, что URL — это адрес, body — письмо внутри конверта, а headers — это наклейки на конверте: «Хрупкое», «Доставить экспрессом», «Формат письма такой-то». Наклейки важные, но вы же не пишете на наклейке весь текст письма.

Из предыдущей лекции у нас уже есть два главных примера: Accept и Content-Type. Они про формат, а не про доменную сущность (книгу). Если клиент хочет получить JSON, он говорит: Accept: application/json. Если клиент отправляет JSON в body, он говорит: Content-Type: application/json.

Вот пример «чтения списка с фильтром»: фильтр — в query, формат ответа — в header.

# Читаем коллекцию с фильтром (фильтр в query).
GET /api/v1/reading-list?status=PLANNED HTTP/1.1
Host: localhost:8080
# Accept — это ожидание по формату ответа (служебная информация).
Accept: application/json

Обратите внимание, что status=PLANNED не уехал в header. Он не «служебный». Это прикладное условие чтения списка, то есть query.

И вот пример «создания элемента»: основные данные — в body, формат того, что вы отправляете — в Content-Type, а формат того, что вы хотите получить — в Accept.

# Создаём элемент: данные сущности в body.
POST /api/v1/reading-list HTTP/1.1
Host: localhost:8080
# Content-Type — формат того, что лежит в body запроса.
Content-Type: application/json
# Accept — формат, который клиент хочет получить в ответе.
Accept: application/json

{"title":"Clean Code","author":"Robert C. Martin","status":"PLANNED"}

Мы сегодня не разбираем, как именно устроено содержимое body и почему там JSON (это будет отдельной большой темой). Нам важна дисциплина: headers — для метаданных, body — для полезной нагрузки, query/path — для адресации и уточнений.

5. Body: полезная нагрузка

Body — это то место, куда логично класть «главные данные операции», особенно когда вы создаёте или изменяете ресурс. Когда клиент говорит «создай мне новый элемент списка чтения» или «обнови этот элемент», ему нужно передать набор полей: название, автора, статус, комментарий. Это явно не похоже на «уточнение чтения», и уж точно не похоже на «служебный заголовок». Это содержимое, ради которого операция вообще затеяна. Значит — body.

Если попробовать описать body одним предложением, то это «внутренности запроса». Именно поэтому body чаще всего встречается у методов, которые меняют состояние: POST, PUT, PATCH. А если вы попытаетесь передавать эти данные через query, вы почти гарантированно получите запрос, который неприятно читать, трудно расширять и легко сломать, потому что каждое поле нужно кодировать в строку.

На уровне учебного кода можно представить это так: URL даёт маршрут, а body содержит «данные новой сущности».

// URL — это маршрут (куда и к какому ресурсу обращаемся).
String url = "/api/v1/reading-list";

// Body — это payload: основная полезная нагрузка операции (данные сущности).
String body = """
    {"title":"Clean Code","author":"Robert C. Martin","status":"PLANNED"}
    """;

// Смотрим: URL короткий и читаемый, а "тяжёлые" данные живут в body.
System.out.println(url);  // /api/v1/reading-list
System.out.println(body); // {"title":"Clean Code",...}

И ещё одно практическое наблюдение: если вы видите запрос, где в URL лежат поля сущности (title, author, comment), это почти всегда сигнал, что кто-то перепутал «адрес» и «содержимое письма». URL предназначен быть читаемым маршрутом и условиями чтения, а не сериализацией всего объекта.

6. Выбор: path/query/headers/body

Если собрать всё в одну формулу, получится простое правило, которое реально спасает начинающего backend-разработчика от «HTTP-хаоса». Сначала вы определяете смысл конкретного значения, а уже потом выбираете место в запросе. Это похоже на раскладывание вещей по полкам: сначала понимаем, что за вещь, потом решаем, в какой ящик ей жить. Иначе получится квартира, где носки лежат в холодильнике, а еда — в шкафу с инструментами.

Вот короткая «карта решений» в виде таблицы с примерами:

Что это за значение? Куда класть? Пример
Идентификатор конкретного ресурса Path /api/v1/reading-list/42
Уточнение чтения списка (фильтр/поиск) Query /api/v1/reading-list?status=PLANNED
Служебные правила обработки, формат, ожидания Headers Accept: application/json
Основные данные для создания/обновления Body { "title": "...", "author": "..." }

И для тех, кому проще думать «если… то…», можно представить это как мини-блок-схему:

flowchart TD
    A["Нужно передать значение в запросе"] --> B{"Это идентичность ресурса? (какой именно объект?)"}
    B -->|Да| P["Кладём в path /api/v1/reading-list/{id}"]
    B -->|Нет| C{"Это уточнение чтения? (фильтр, поиск, условие)"}
    C -->|Да| Q["Кладём в query ?status=PLANNED"]
    C -->|Нет| D{"Это служебные правила? (формат, ожидания)"}
    D -->|Да| H["Кладём в headers Accept/Content-Type"]
    D -->|Нет| E["Скорее всего это payload Кладём в body"]

Отдельно стоит запомнить «правило одного смысла»: одно и то же значение обычно не должно одновременно жить в path, query и body. Если вы пишете /api/v1/reading-list/42?id=42 и ещё в body добавляете "id": 42, то вы как будто сами себе не доверяете. Сервер в итоге может выбрать «какой id важнее», а клиент будет гадать, почему иногда срабатывает одно, иногда другое. HTTP-ошибки тут не нужны — достаточно человеческой ошибки, чтобы всё стало нестабильным.

И как антипример — вот такой запрос (делать так не надо, даже если «работает»):

// Плохой пример: одно и то же значение размазано по разным частям запроса.
String badUrl = "/api/v1/reading-list/42?id=42"; // id и в path, и в query
String badBody = "id=42";                        // и ещё раз тот же смысл в body

System.out.println(badUrl);  // /api/v1/reading-list/42?id=42
System.out.println(badBody); // id=42

Здесь один смысл размазан по трём местам. Это ухудшает читаемость и рождает вопросы: а если значения разные, что правда? И кто виноват, если правда окажется «не та»?

7. Примеры ReadLater без «каши»

Чтобы это не осталось абстракцией «про HTTP вообще», давайте зафиксируем несколько примеров в нашем домене личного списка чтения. Важно: сейчас мы смотрим только на запрос и размещение данных, не пытаясь собрать полный сценарий с ответами и статусами — это будет отдельным шагом. Здесь наша задача — добиться того, чтобы по одному взгляду на запрос было понятно, что он делает и где какие значения живут.

Пример «получить один элемент списка чтения» выглядит естественно, когда id в path. Формат ответа — в Accept:

# Читаем конкретный ресурс: идентификатор в path.
GET /api/v1/reading-list/42 HTTP/1.1
Host: localhost:8080
# Хотим получить JSON-ответ.
Accept: application/json

Пример «получить список и отфильтровать по статусу» выглядит естественно, когда фильтр в query. Это по-прежнему чтение коллекции, просто с уточнением:

# Читаем коллекцию: фильтр по статусу в query.
GET /api/v1/reading-list?status=IN_PROGRESS HTTP/1.1
Host: localhost:8080
Accept: application/json

Пример «поиск по названию» (не идентификация одного объекта, а критерий) — тоже query:

# Поиск/фильтр — это уточнение чтения, поэтому query.
GET /api/v1/reading-list?title=clean HTTP/1.1
Host: localhost:8080
Accept: application/json

Пример «создать новый элемент» — это уже payload в body, а формат payload описан Content-Type:

# Создание: данные новой сущности отправляем в body.
POST /api/v1/reading-list HTTP/1.1
Host: localhost:8080
Content-Type: application/json
Accept: application/json

{"title":"Clean Code","author":"Robert C. Martin","status":"PLANNED"}

И маленький бонус для тех, кто любит «пощупать руками» на уровне Java: класс URI в JDK помогает увидеть, что path и query — это разные части одной и той же строки, и у них разные роли.

import java.net.URI;

// В одном URL можно чётко разделить: path (адрес ресурса) и query (уточнения чтения).
URI uri = URI.create("http://localhost:8080/api/v1/reading-list/42?verbose=true");

// Path отвечает на вопрос "какой ресурс?".
System.out.println(uri.getPath());   // /api/v1/reading-list/42
// Query отвечает на вопрос "с какими параметрами/уточнениями читать?".
System.out.println(uri.getQuery());  // verbose=true

Даже в таком примере видно, что verbose=true — это не «адрес ресурса», а дополнительное условие. Именно такой разницы мы и добиваемся на уровне дизайна запросов: чтобы адрес и уточнения не смешивались.

8. Типичные ошибки размещения данных

В этой теме ошибки обычно выглядят «невинно»: запрос же отправился, сервер что-то ответил, вроде всё живо. Но потом оказывается, что API читать неудобно, расширять страшно, а клиент и сервер периодически спорят, «какая часть запроса главнее». Хорошая новость: почти все эти ошибки лечатся одним вопросом — «какой смысл у этого значения?».

Ошибка №1: идентификатор ресурса кладут в query вместо path.
Запрос вида /api/v1/reading-list?id=42 работает, но смыслово выглядит как «я пришёл к коллекции и попросил выбрать один элемент». Когда речь про один конкретный объект, path читается проще и честнее: /api/v1/reading-list/42. Так вы сразу показываете, что оперируете отдельным ресурсом, а не списком.

Ошибка №2: фильтры и условия запихивают в path.
Иногда встречается что-то вроде /api/v1/reading-list/status/PLANNED как способ фильтрации. Это быстро превращает URL в цепочку «почти-команд», где сложно отличить маршрут от условий чтения. Для фильтров и поиска у query уже есть нормальное место: /api/v1/reading-list?status=PLANNED. Так вы сохраняете понятный ресурс (reading-list) и добавляете опциональные уточнения.

Ошибка №3: основные данные создания/обновления передают через query-строку.
Запрос вида POST /api/v1/reading-list?title=Clean%20Code&author=...&comment=... выглядит как попытка написать «письмо» на адресной стороне конверта. Это неудобно читать, неудобно расширять, легко ломать при специальных символах, и в целом это не то, для чего query задумывался. Основные данные операции должны жить в body, а query оставляют для коротких уточнений чтения.

Ошибка №4: прикладные данные ресурса кладут в headers.
Headers — это не «ещё один body». Когда вы пытаетесь передать title или status заголовками, вы делаете контракт менее очевидным: клиенту нужно угадывать, какие заголовки обязательны, какие опциональны, и почему вдруг «поле книги» оказалось не в body и не в URL. Заголовки хороши для форматов и правил обработки (Accept, Content-Type), а прикладные данные лучше держать там, где их ожидают: в query (если это фильтр чтения) или в body (если это данные создаваемого/обновляемого ресурса).

Ошибка №5: один и тот же смысл дублируют в разных местах запроса.
Самый опасный вариант — когда id написан и в path, и в query, и в body. Сегодня они одинаковые, завтра кто-то ошибся, и значения разъехались. После этого начинается гадание: что «истина»? Хорошая дисциплина — выбрать одно место по смыслу и не размазывать значение по всему запросу.

1
Задача
Java Server, 8 уровень, 3 лекция
Недоступна
Сборка URL для одного ресурса и для фильтрации
Сборка URL для одного ресурса и для фильтрации
1
Задача
Java Server, 8 уровень, 3 лекция
Недоступна
Шаблон POST-запроса с body
Шаблон POST-запроса с body
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ