JavaRush /Курсы /Spring REST & MVC /CRUD и HTTP-методы в REST

CRUD и HTTP-методы в REST

Spring REST & MVC
3 уровень , 2 лекция
Открыта

1. CRUD и HTTP-методы: читаемость API

Когда начинающий разработчик впервые проектирует API, он часто мыслит так: “мне нужно создать задачу — значит, будет /createTask, нужно удалить — будет /deleteTask”. Это выглядит логично… если вы мысленно рисуете API как набор кнопок в админке. Но клиент API видит не кнопки и не ваш контроллер, а контракт, в котором важны предсказуемость и повторяемость паттернов.

Связка CRUD ↔ HTTP-методы — не догма и не религия. Это способ договориться о базовой грамматике: где у нас “существительные” (ресурсы в URI), а где “глаголы” (методы HTTP). Когда эта грамматика соблюдена, клиент быстро угадывает поведение endpoint’ов даже без документации, а вы сами не плодите бесконечные маршруты-исключения.

Ресурсы, коллекции и представления мы уже развели по ролям. Теперь важно понять, как на эту модель ложится знакомая HTTP-грамматика: какой метод работает с коллекцией, какой — с конкретным ресурсом, и почему это вообще делает API читаемым.

Если очень упрощать, мы хотим, чтобы API читалось как фраза:

GET /tasks — «дай задачи»
POST /tasks — «добавь задачу»
DELETE /tasks/{taskId} — «удали эту задачу»

И именно эта “читаемость глазами клиента” — главный практический смысл REST в нашем курсе.

CRUD как шпаргалка: Create, Read, Update, Delete

CRUD — это простая модель: создать, прочитать, изменить, удалить. Она хороша тем, что помогает не забыть базовые сценарии, которые почти всегда нужны бизнесу. Но важно понимать тонкость: CRUD — это про операции в голове, а REST — про то, как эти операции выражаются через ресурсы и HTTP.

Здесь легко попасть в ловушку: “если CRUD — четыре действия, значит, должно быть четыре endpoint’а”. На практике всё сложнее. Как минимум есть две оси: во‑первых, вы работаете либо с коллекцией, либо с одним ресурсом; во‑вторых, вы можете читать и менять разные представления (например, краткое и подробное), но это уже тема будущих модулей про DTO, туда сейчас не лезем.

Пока нам достаточно вот такого здравого уровня:

  • Create обычно означает “появился новый элемент в коллекции”.
  • Read означает “дай представление ресурса (одного или списка)”.
  • Update означает “поменяй состояние существующего ресурса”.
  • Delete означает “убери ресурс из адресуемого мира”.

CRUD полезен как стартовая карта. Но он не отвечает на вопрос “какой именно метод HTTP поставить и на какой URI”. За этим мы и идём дальше.

2. Коллекция и ресурс: смысл URI

Одна из самых недооценённых вещей в REST-дизайне — то, что один и тот же HTTP-метод на разных “целях” означает разные сценарии. GET по коллекции — это список, GET по одному ресурсу — детальный просмотр. И это не нюанс, а основа читаемости контракта: клиент должен понимать, просит он сейчас “всё” или “одно”.

Давайте зафиксируем на нашем домене:

  • Коллекция задач: /tasks
  • Одна задача: /tasks/{taskId}

И то же самое для подресурсов:

  • Коллекция комментариев задачи: /tasks/{taskId}/comments
  • Один комментарий конкретной задачи: /tasks/{taskId}/comments/{commentId}

Обратите внимание: URI не описывает действие, он описывает “о чём речь”. Действие приходит сверху — через метод HTTP. Это как адрес дома и то, что вы делаете: “прийти посмотреть” или “прийти заменить замок”. Адрес один, действия разные.

Мини-проверка для себя очень простая: если вы видите .../tasks — мысленно добавьте “все задачи”, если .../tasks/{taskId} — “конкретная задача”. Если это не складывается, значит, URI уже начал сползать в командный стиль.

3. CRUD-операции и методы HTTP

Read: GET для коллекции и для одного ресурса

GET — самый дружелюбный метод. Он буквально создан для того, чтобы получать представление ресурса. На Дне 2 мы уже обсуждали safe methods: GET не должен менять состояние сервера. Это очень практично, потому что клиент и промежуточные системы могут повторять GET, кешировать его, переиспользовать — и не бояться, что от “просмотра страницы” внезапно что-то удалится.

В нашем проекте это выражается максимально естественно:

  • GET /tasks — вернуть список задач (представление коллекции).
  • GET /tasks/{taskId} — вернуть одну задачу (представление ресурса).

И здесь начинается полезная мелкая дисциплина. Например, если вы хотите отфильтровать список, это всё ещё GET /tasks, просто с query-параметрами, а не с новым “командным” URI. Но детали фильтрации, пагинации и сортировки мы сознательно оставим на будущее — сегодня нам важна базовая грамматика.

Небольшой Java-черновик, который помогает увидеть два разных read-сценария:

import java.util.Map;

// Два варианта "прочитать": список и один конкретный ресурс.
// Важно: метод один (GET), а "смысл" задаётся тем, куда мы идём (коллекция vs ресурс).
Map<String, String> readEndpoints = Map.of(
    "read list", "GET /tasks",
    "read one", "GET /tasks/{taskId}"
);

Да, это “игрушечная” запись. Но она полезна тем, что вы сразу видите: список и один элемент — это разные endpoints, хотя метод один и тот же.

Create: POST как добавление элемента в коллекцию

Создание — место, где новички чаще всего выдают /createSomething и считают задачу выполненной. А REST-логика говорит: “создание чего-то нового” обычно означает добавление нового элемента в коллекцию. Значит, цель операции — коллекция, а метод — тот, который подходит для добавления.

Поэтому базовый паттерн выглядит так:

  • POST /tasks — создать новую задачу в коллекции задач.

Важно держать в голове следующее: POST — это не “метод для всего, что не GET”. POST хорошо читается как “добавить в контейнер или коллекцию новый элемент” или “создать дочерний ресурс”. Именно поэтому комментарии как подресурс задачи создаются так:

  • POST /tasks/{taskId}/comments — создать новый комментарий внутри коллекции комментариев конкретной задачи.

Обратите внимание на очень полезный психологический эффект: у вас исчезает необходимость придумывать глаголы в path. Вы не пишете /addCommentToTask, потому что контекст уже есть в URI: комментарии живут внутри задачи.

Мини-пример для фиксации этой мысли:

import java.util.Map;

// Создание почти всегда адресуется в "контейнер": в коллекцию ресурсов.
// Отдельно обратите внимание на подресурс comments: он "живёт" внутри task.
Map<String, String> createEndpoints = Map.of(
    "create task", "POST /tasks",
    "create comment", "POST /tasks/{taskId}/comments"
);

Про коды ответа (201 Created) и Location мы будем говорить в модуле про request/response и корректные ответы. Сейчас держим только главную идею: POST идёт в коллекцию.

Update: PUT и PATCH на высоком уровне

Обновления — это место, где в реальных проектах начинается философия, священные войны и комментарии на 200 строк в ревью. Чтобы этого не случилось у нас слишком рано, берём “достаточную” модель: PUT и PATCH нужны, чтобы изменить существующий ресурс, и обычно они применяются к одному конкретному ресурсу, а не к коллекции.

Базовая связка такая:

  • PUT /tasks/{taskId} — заменить изменяемое состояние задачи целиком (высокоуровнево: “сделать так, как в присланном представлении”).
  • PATCH /tasks/{taskId} — поменять часть состояния задачи (высокоуровнево: “внести частичные изменения”).

Пока не лезем в детали “что значит целиком” и “как трактовать null”. Это будет отдельная тема в курсе (и она реально важна), но не в этой лекции. Сейчас нам нужно запомнить только грамматику: изменения адресуются на конкретный ресурс, потому что мы меняем не “все задачи в мире”, а “вот эту задачу”.

И ещё одна практическая связка с Днём 2: PUT и PATCH — это про запись, поэтому они не safe. А вопрос идемпотентности мы здесь держим как фоновую дисциплину: повторный PUT с тем же содержимым обычно должен приводить к тому же результату. Но повторюсь: сегодня это на уровне “помнить, что такое бывает”, без реализации.

Небольшая карта обновлений:

import java.util.Map;

// Обновление обычно делаем по адресу конкретного ресурса (/tasks/{taskId}),
// потому что изменяем "вот эту" задачу, а не всю коллекцию целиком.
Map<String, String> updateEndpoints = Map.of(
    "full replace", "PUT /tasks/{taskId}",
    "partial change", "PATCH /tasks/{taskId}"
);

Delete: DELETE и аккуратный выход из мира ресурсов

Удаление часто воспринимается как “ну это же просто, делаем /removeTask”. Но в REST-модели мы опять возвращаемся к тому же принципу: URI показывает ресурс, метод показывает действие. Если вы удаляете конкретную задачу, вы обращаетесь к адресу этой задачи и используете DELETE.

Базовый паттерн:

  • DELETE /tasks/{taskId} — удалить конкретную задачу.

И тот же паттерн прекрасно работает для подресурсов:

  • DELETE /tasks/{taskId}/comments/{commentId} — удалить конкретный комментарий конкретной задачи.
  • DELETE /tasks/{taskId}/attachments/{attachmentId} — удалить конкретное вложение конкретной задачи (когда мы дойдём до files).

Это выглядит скучно (и это комплимент). Скучная грамматика контракта и делает API удобным: клиенту не нужно запоминать десять “особенных” команд, он просто комбинирует два понятных измерения: метод + ресурс.

Вспомним ещё одну идею из HTTP-мира: DELETE считается идемпотентным на транспортном уровне. Это не значит, что бизнес “ничего не почувствует”, но означает: повторный DELETE не должен превращаться в “удаление дважды”, что бы это ни значило. Реальные ответы (204 vs 404) мы разберём позже, а сейчас достаточно помнить: DELETE — это нормальный способ “попрощаться” с ресурсом.

Мини-фиксация:

// Плохой пример: действие вшито в path и ещё и сделано через GET (небезопасно).
String wrong = "GET /deleteTask/{taskId}";

// Хороший пример: ресурс в URI, действие (удаление) — через метод DELETE.
String better = "DELETE /tasks/{taskId}";

4. Черновик API surface в коде

Есть одна практика, которая очень помогает новичкам: зафиксировать поверхность API не только в голове и в заметках, а в простом виде прямо в проекте. Не как “финальную документацию”, а как маленький ориентир, чтобы в любой момент проверить: “у нас вообще API сейчас выглядит как ресурсное, или я уже придумал /doMagic?”

Сделаем крошечный “контракт-черновик” на чистой Java. Он не использует Spring, не лезет в контроллеры и не конфликтует с будущими темами. Его задача — быть читаемым: списком endpoint’ов, которые мы подразумеваем на уровне дизайна.

На таком черновике особенно хорошо видна базовая сетка API. И тут же заметно, где проект чаще всего снова срывается в команды: бизнес редко просит “добавьте ещё один PATCH”, он просит complete, assign, upload. Именно такие сценарии и показывают, удерживаете вы ресурсную модель или снова строите каталог кнопок.

Описываем HTTP-методы как enum

Для начала заведём enum. Звучит почти смешно, но даёт полезный эффект: вы перестаёте писать методы строками и даже в черновиках допускаете меньше ошибок.

package com.example.tasktracker.api.contract;

// Явный enum полезен даже в "черновике контракта":
// так мы не опечатаемся в методе ("POTS" вместо "POST") и не размазываем строки по коду.
public enum HttpMethod {
    GET, POST, PUT, PATCH, DELETE
}

Описываем endpoint как маленькую запись

Теперь сделаем простую модель endpoint’а. Нам хватит метода и path.

package com.example.tasktracker.api.contract;

// Мини-модель endpoint'а: только метод и путь.
// Этого достаточно, чтобы обсуждать дизайн API, не привязываясь к Spring и контроллерам.
public record Endpoint(HttpMethod method, String path) {
}

Собираем «поверхность» задач и комментариев

А теперь — то, ради чего всё затевалось: соберём список. Обратите внимание: мы сознательно используем пути без /api/v1 (версионирование и правила URI будут отдельным днём). Здесь нам нужна только ресурсная структура.

package com.example.tasktracker.api.contract;

import java.util.List;

public final class TaskTrackerApiSurface {

    // Возвращаем "поверхность" API в виде списка endpoint'ов.
    // Это не реализация контроллеров, а просто фиксатор договорённостей по контракту.
    public static List<Endpoint> endpoints() {
        return List.of(
            // Read
            new Endpoint(HttpMethod.GET, "/tasks"),
            new Endpoint(HttpMethod.GET, "/tasks/{taskId}"),

            // Create
            new Endpoint(HttpMethod.POST, "/tasks"),

            // Update
            new Endpoint(HttpMethod.PUT, "/tasks/{taskId}"),
            new Endpoint(HttpMethod.PATCH, "/tasks/{taskId}"),

            // Delete
            new Endpoint(HttpMethod.DELETE, "/tasks/{taskId}")
        );
    }

    // Утилитный класс: экземпляры не нужны.
    private TaskTrackerApiSurface() {
    }
}

Если хочется добавить comments как подресурс — чисто на уровне дизайна, — можно расширить список ещё несколькими строками в том же стиле, но не превращать это в “финальную карту”. Наша цель тут учебная: почувствовать, как CRUD складывается в ресурсную модель.

Маленькая печать в консоль

Иногда полезно просто распечатать это и увидеть глазами. Сделаем мини-демо-класс. Да, в Spring Boot проекте обычно запускается основной класс приложения, но отдельный main() для учебных экспериментов — это нормально.

package com.example.tasktracker.api.contract;

public class ApiSurfaceDemo {

    public static void main(String[] args) {
        TaskTrackerApiSurface.endpoints()
                .forEach(System.out::println);
        // Endpoint[method=GET, path=/tasks]
        // Endpoint[method=GET, path=/tasks/{taskId}]
    }
}

Дальше, когда мы будем реализовывать контроллеры, DTO, validation и errors, этот “список” может либо удалиться, либо превратиться в документацию, либо остаться как подсказка. Но на текущем этапе он хорош тем, что дисциплинирует мышление: вы начинаете видеть API как поверхность, а не как “методы в контроллере”.

5. Типичные ошибки при выборе HTTP-методов

Ошибки в выборе HTTP-методов редко выглядят как “ой, компилятор ругается”. Обычно они выглядят так: “через месяц никто не понимает, как этим пользоваться”, а иногда — и так: “клиент случайно удалил данные, просто открыв ссылку”. Поэтому полезно заранее знать типовые грабли, чтобы не собирать коллекцию этих грабель у себя в проде (и не делать вид, что так и задумывалось).

Ошибка №1: использовать GET для изменения состояния.
Самый популярный антипример: GET /deleteTask/{id} или GET /completeTask/{id}. На уровне “мне же нужно вызвать действие” это кажется нормальным, но ломает сам смысл GET как безопасного чтения. Клиент или даже браузер/кеш могут повторить GET автоматически, и вы получите неожиданные побочные эффекты. Если операция меняет состояние — это не GET, даже если вам очень хочется.

Ошибка №2: делать POST универсальной кнопкой “сделай что-нибудь”.
Вторая крайность после злоупотребления GET — злоупотребление POST. Появляется API вида POST /createTask, POST /updateTask, POST /deleteTask. Формально “работает”, но теряется смысл методов, и контракт перестаёт читаться. POST /tasks как создание элемента коллекции читается проще, чем любой /create..., потому что вы не смешиваете действие и ресурс в одном месте.

Ошибка №3: путать операции над коллекцией и операции над одним ресурсом.
Например, DELETE /tasks — “удалить все задачи” — звучит как “о, удобно”, но это уже очень опасный endpoint и почти никогда не нужен в публичном API “по умолчанию”. В учебном проекте мы вообще стараемся держать операции естественными: удаление — по конкретному taskId. Как только вы начинаете массово воздействовать на коллекции, резко растёт сложность ошибок, идемпотентности и ожиданий клиента.

Ошибка №4: пытаться “выразить update” через отдельные глагольные маршруты.
Вместо PUT /tasks/{taskId} появляются /tasks/{taskId}/rename, /tasks/{taskId}/changeDescription, /tasks/{taskId}/assignUser. Иногда так действительно делают, но это быстрый путь к разрастанию глагольных маршрутов, когда на каждую мелочь появляется отдельный endpoint. В нашем курсе мы сначала учимся базовой дисциплине: update — это изменение представления ресурса через PUT или PATCH.

Ошибка №5: слишком рано пытаться идеально различить PUT и PATCH до уровня философии.
На этом этапе достаточно понимать намерение: PUT — “заменить целиком”, PATCH — “поменять часть”. Не нужно уже сегодня пытаться придумать идеальную merge-логику, JSON Patch, конфликтующие правила для null и “отсутствия поля”. Это реальная сложность, и мы до неё дойдём, но сейчас она только мешает сформировать базовую грамматику.

1
Задача
Spring REST & MVC, 3 уровень, 2 лекция
Недоступна
CRUD-поверхность для ресурса articles
CRUD-поверхность для ресурса articles
1
Задача
Spring REST & MVC, 3 уровень, 2 лекция
Недоступна
Исправление неверных маршрутов
Исправление неверных маршрутов
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ