1. Создание как отдельная операция
Читать то, что уже существует, — только половина истории. Создание ресурса — это момент, когда «чего-то ещё нет», но клиент уже хочет, чтобы «оно появилось». В консольной Java мы часто просто делаем new и добавляем в список, и кажется, что жизнь прекрасна. В HTTP-мире создание — это договор: клиент описывает, что он хочет создать, сервер решает, как именно это появится, и возвращает результат так, чтобы клиент мог дальше с ним работать.
В домене ReadLater создание выглядит очень приземлённо: пользователь хочет добавить книгу в личный список чтения. До этого у него мог быть пустой список. После создания в коллекции появляется новый элемент, у которого появляется идентификатор (чтобы его потом можно было прочитать, обновить или удалить) и набор полей (например, название и автор).
Представьте обычную реальность: вы приходите в кафе и говорите «Хочу латте и круассан». Вы не говорите «Создайте заказ номер 42». Номер заказа назначает система кафе. В HTTP создание работает похожим образом: клиент сообщает состав заказа (данные будущего ресурса), а сервер «пробивает чек» — то есть создаёт новый элемент и даёт ему идентификатор.
Чтобы это сделать предсказуемо и без «внутренней телепатии», в HTTP есть метод, который очень часто используется для создания нового элемента в коллекции: POST.
2. POST и путь коллекции
Ресурсная пара здесь уже знакома: коллекция /reading-list и отдельный элемент /reading-list/42. Для POST из этого различия нужен один главный вывод: создавать логично через путь коллекции, потому что у нового элемента ещё нет собственного адреса по id.
Поэтому типичная форма создания выглядит так:
POST /reading-list
А не так:
POST /reading-list/42
Во втором варианте клиент как будто пытается заранее назвать id. На базовом уровне это лишняя ответственность: сервер сам создаёт элемент и назначает ему идентификатор. Бывают контракты, где клиент приносит id сам, но для нашей прикладной модели честнее и понятнее считать, что id назначает сервер.
Вот маленькая «мысленная табличка» для проверки себя:
| Что хочет клиент | На что он целится | Почему |
|---|---|---|
| «Хочу получить список» | /reading-list | нужен набор |
| «Хочу получить один элемент» | /reading-list/42 | нужен конкретный элемент |
| «Хочу создать новый элемент» | /reading-list | новый элемент появляется внутри набора |
Если кратко: POST — это разговор с коллекцией. Мы стучимся «в дверь отдела кадров», а не «к конкретному сотруднику», которого ещё даже не наняли.
3. Тело запроса POST
Путь и метод отвечают на вопрос «с каким ресурсом и каким действием мы работаем». Но создание без данных обычно бессмысленно: чтобы добавить книгу в список чтения, нужно хотя бы понять, какую книгу мы добавляем.
Поэтому у POST чаще всего есть body — то самое тело запроса, которое мы уже видели как часть HTTP request. Сегодня нам не важен конкретный формат тела (мы не обсуждаем ни JSON, ни формы, ни кодировки), нам важна идея: клиент передаёт описание будущего ресурса.
На уровне домена ReadLater тело запроса «на человеческом» может содержать примерно такие данные:
title: название книги
author: автор
comment: комментарий пользователя (опционально)
Можно представить это как простую строку (не как реальный протокол, а как иллюстрацию мысли):
// Метод HTTP-запроса
String method = "POST";
// Путь коллекции: создание происходит «внутри набора»
String path = "/reading-list";
// Тело запроса: данные будущего ресурса (в реальности формат будет другой, но идея та же)
String body = "title=Clean Code; author=Robert C. Martin; comment=Найти бумажное издание";
Обратите внимание на важный сдвиг мышления. Мы не отправляем «команду» типа addBook. Мы отправляем описание ресурса: «в моём списке чтения должна появиться запись с такими полями». Сервер уже сам решит, что именно хранить, какой выдать идентификатор, какие значения по умолчанию применить, и что вернуть в ответ.
Чтобы чуть сильнее связать это с Java-мышлением, можно представить, что body — это «сырьё» для создания объекта. В консольной программе вы могли бы собрать такие данные в объект, но сейчас мы не привязываемся к формату передачи — просто показываем, что для создания нужен набор полей.
Например, мы можем вообразить «на стороне сервера» структуру данных, которую он хотел бы получить:
// Данные, необходимые для создания элемента списка чтения.
// Это не про JSON/сериализацию, а про «контейнер входных данных».
record CreateReadingItemData(String title, String author, String comment) {
}
Этот record здесь не про JSON и не про сериализацию, а просто про мысль: создание — это набор данных, который приходит извне. В реальном HTTP эти данные будут приходить в виде текста в body, а уже потом сервер будет превращать его в структуру.
4. Назначение идентификатора при создании
В консольной Java очень легко незаметно для себя смешать две ответственности: «я решил добавить элемент» и «я сам придумал, какой у него id». В backend-мире лучше привыкать к другой картине: сервер — хозяин ресурса, он отвечает за уникальность и за то, чтобы ресурс существовал как сущность системы, а не как фантазия клиента.
Поэтому при создании почти всегда есть шаг «сервер назначает id». Это можно показать на простом Java-примере без всякой сети: как будто это кусочек внутренней логики сервера.
import java.util.concurrent.atomic.AtomicLong;
// Последовательность для генерации уникальных идентификаторов на стороне сервера
AtomicLong idSequence = new AtomicLong(1);
// Сервер «выдаёт» новый ID для созданного элемента
long newId = idSequence.getAndIncrement();
System.out.println("newId=" + newId); // newId=1
Почему это важно именно в контексте POST? Потому что это объясняет, почему клиент не должен целиться в /reading-list/42, когда он создаёт элемент. Клиент ещё не знает, будет ли это 42, 43 или 100500 (и хорошо бы, чтобы его это вообще не волновало).
Теперь соберём это в маленькую историю. Клиент отправляет POST /reading-list и «прикладывает» данные. Сервер:
1. читает данные из body запроса;
2. создаёт новый элемент коллекции;
3. назначает ему идентификатор;
4. возвращает результат так, чтобы клиент мог его дальше адресовать как отдельный ресурс.
Вот простая схема процесса (без статусов и заголовков — они будут отдельно позже, а сейчас только логика):
sequenceDiagram
participant C as Client
participant S as Server
participant R as "ReadingList (collection)"
C->>S: POST /reading-list + body данные книги
S->>R: add(newItem)
R-->>S: created item with id=42
S-->>C: ответ: созданный элемент (включая id=42)
Заметьте, как аккуратно «рождается» путь отдельного элемента: после создания появляется смысл в /reading-list/42, потому что теперь есть ресурс, который можно адресовать.
5. Ответ после POST
После создания клиенту нужно понять две вещи: во-первых, получилось ли создание вообще, во-вторых, что именно получилось. Даже если клиент «и так знает, что отправлял», сервер может применить какие-то правила, дополнить данные, нормализовать их или просто назначить идентификатор, которого раньше не было.
Поэтому типичный «здравый» ответ после POST содержит представление созданного ресурса. В нашем домене это мог бы быть текст вроде «создан элемент списка чтения с id=42, title=..., author=...». В реальном API это будет структурированное тело ответа (но мы сейчас не обсуждаем формат).
На уровне базовой ментальной модели полезно думать так: POST — это «заявка на создание», а ответ сервера — это «квитанция», где есть номер и итоговые данные.
Вот мини-пример на Java, который иллюстрирует идею «сервер вернул то, что создал» (это не HTTP-код, а логика):
// ID назначен сервером при создании
long createdId = 42L;
// Сервер может вернуть итоговые данные ресурса (включая те, что пришли от клиента)
String createdTitle = "Clean Code";
String createdAuthor = "Robert C. Martin";
// Клиент получает «квитанцию»: id + итоговые поля созданного ресурса
System.out.println("Created: id=" + createdId + ", title=" + createdTitle);
// Created: id=42, title=Clean Code
Самое важное здесь — не формат, а смысл: после POST появляется новый элемент, и клиент получает возможность обращаться к нему как к отдельному ресурсу.
Если хочется очень коротко связать GET и POST в один «поток», то получается такая логика:
POST /reading-list создаёт элемент и возвращает его (с id).
GET /reading-list/id потом позволяет прочитать этот элемент, используя выданный идентификатор.
Мы пока не обсуждаем, какой именно статус вернётся и какие заголовки будут важны. Здесь нам нужен сам принцип: после POST клиент должен увидеть, что ресурс появился и как к нему обратиться дальше. Числовые статусы, Location и остальная response-семантика — это уже отдельный слой HTTP-контракта; без этого создание легко превращается в «чёрную дыру».
6. POST и ресурсный путь
К этому месту полезно держать только одно напоминание: POST /reading-list читается лучше, чем /addBookToReadingList, потому что путь продолжает называть ресурс, а действие выражает сам метод. Если спрятать создание в адресе, API снова скатывается в набор команд.
Для обычного CRUD-сценария этого принципа достаточно: есть коллекция, после создания у элемента появляется свой адрес, а методы показывают, что именно мы с этим ресурсом делаем.
Повторы POST и дубли
У POST есть одна очень практическая неприятность: повтор того же запроса может создать второй такой же элемент. Это не баг метода, а нормальная семантика создания — каждый POST снова просит добавить ресурс в коллекцию.
Здесь достаточно запомнить сам риск дублей: если клиент, UI или сеть повторят создание, результат может размножиться сильнее, чем вы ожидали. Поэтому вопрос «что будет при повторе запроса?» для HTTP-методов вообще не декоративный.
7. Типичные ошибки при работе с POST
Ошибка №1: отправлять POST на путь отдельного ресурса с «придуманным» id.
Это выглядит логично, если вы мысленно хотите контролировать всё, но обычно ломает базовую модель: клиент не должен угадывать идентификатор. В ресурсном мышлении создание происходит на коллекции, а конкретный id появляется как результат. Если очень хочется управлять id, это должно быть отдельное осознанное решение контракта, а не «ну я так чувствую».
Ошибка №2: пытаться выразить создание только путём, без тела запроса.
Новички иногда пишут что-то вроде POST /reading-list/clean-code, надеясь, что «адрес сам всё скажет». Это быстро превращает API в странный набор правил. Для создания обычно нужны данные: название, автор, комментарий, статус и т.д. Путь должен оставаться простым и стабильным, а данные — приезжать в body.
Ошибка №3: превращать POST в универсальный метод “на всё, что непонятно”.
Если у вас есть привычка «не знаю что делать — отправлю POST», API получается туманным. Клиенту сложно предсказывать поведение, а серверу — сложно поддерживать контракт. Даже без глубокого знания остальных методов уже полезно держать в голове простую мысль: POST — это прежде всего создание нового элемента в коллекции, а не «волшебная кнопка».
Ошибка №4: не думать о результате создания для клиента.
Иногда сервер «создаёт что-то внутри», но клиенту возвращает пустоту или невнятный текст. В итоге клиент не знает, что появилось, какой у ресурса идентификатор и как к нему обратиться дальше. Даже не углубляясь в статусы и заголовки, полезно держать правило: после создания клиент должен получить достаточно информации, чтобы продолжить работу с новым ресурсом.
Ошибка №5: не замечать риск дублей при повторной отправке запроса.
Если вы мысленно считаете POST чем-то вроде «установить значение», вы удивитесь, когда повтор создаст второй элемент. Это не «подстава HTTP», а нормальная семантика создания. Важно заранее принять эту модель, чтобы потом не проектировать API и клиентское поведение на ложном предположении «повторы всегда безопасны».
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ