🌐 Раздел 6 · Вопрос #13

Стоит ли использовать глаголы в URL

Фундаментальный принцип REST гласит:


🟢 Junior Level

В каноническом RESTful дизайне использовать глаголы в URL категорически не рекомендуется.

Фундаментальный принцип REST гласит:

  • URI (Uniform Resource Identifier) определяет ресурс (существительное: кто/что).
  • HTTP Метод определяет действие (глагол: что сделать).

Сравнение подходов

Неправильно (RPC-стиль с глаголами) Правильно (RESTful дизайн) Пояснение
GET /api/v1/getUser?id=42 GET /api/v1/users/42 Чтение данных по идентификатору.
POST /api/v1/createUser POST /api/v1/users Создание нового ресурса в коллекции.
POST /api/v1/updateUser PUT /api/v1/users/42 (или PATCH) Полное или частичное обновление.
GET /api/v1/deleteUser/42 DELETE /api/v1/users/42 Удаление целевого ресурса.

Почему глаголы в URL — это опасно?

Если действие удаления или списания баланса привязано к методу GET (например, GET /api/orders/10/cancel):

  1. Поисковые роботы и краулеры (Googlebot, Яндекс) автоматически переходят по всем ссылкам <a> на странице и могут случайно уничтожить данные.
  2. Браузерный prefetching (предзагрузка ссылок для ускорения открытия страниц) выполнит запрос в фоновом режиме без ведома пользователя.
  3. Промежуточные прокси и CDN закэшируют ответ первого пользователя и не донесут запросы остальных до бэкенда.

🟡 Middle Level

Как моделировать бизнес-действия без глаголов (Паттерн «Действие как ресурс»)

На собеседованиях часто задают вопрос:

«Как в REST отменить заказ, отправить email или сменить пароль, если глаголы использовать нельзя?»

Существует 3 элегантных RESTful подхода:

1. Изменение состояния через PATCH (State Mutation)

Если действие сводится к смене статуса жизненного цикла объекта:

PATCH /api/v1/orders/100 HTTP/1.1
Content-Type: application/json

{
  "status": "CANCELLED"
}

2. Моделирование действия как самостоятельного ресурса (Action as a Resource)

Вместо глагола создается подчиненный ресурс, фиксирующий факт события. Это позволяет сохранять метаданные: автора отмены, причину, дату и сумму компенсации.

POST /api/v1/orders/100/cancellations HTTP/1.1
Content-Type: application/json

{
  "reason": "Found cheaper elsewhere",
  "refundRequired": true
}

Успешный ответ: 201 Created с телом созданного ресурса отмены.

3. Асинхронные задачи (Job / Command Resource)

Для «тяжелых» действий (экспорт в Excel, пересчет аналитики):

POST /api/v1/reports/export-jobs HTTP/1.1

Ответ: 202 Accepted с заголовком Location: /api/v1/reports/export-jobs/8841.

Прагматичный подход Google и GitHub (Custom Methods)

Когда предметная область перегружена сложными транзакционными действиями, даже ведущие технологические компании отступают от академического REST.

Google Cloud API Design Guide: Google стандартизировал использование кастомных методов через двоеточие в конце пути:

POST /v1/documents/42:publish HTTP/1.1
POST /v1/virtual-machines/10:restart HTTP/1.1

Это сохраняет URI ресурса чистым (/v1/documents/42), четко отделяя имя сущности от специфической не-CRUD команды.


🔴 Senior Level

REST vs RPC: когда глаголы необходимы

Попытка принудительно упаковать абсолютно любой сценарий в рамки RESTful ресурсов приводит к антипаттерну «REST Over-Engineering».

Если система выполняет вычисления без сохранения состояния или запускает сложные пакетные процессы:

Калькулятор налогов: POST /api/v1/calculate-taxes
Проверка орфографии: POST /api/v1/spell-check

Здесь нет сущности «налог» или «слово», которой можно присвоить постоянный идентификатор и выполнять над ней GET/PUT/DELETE. В таких сценариях честный RPC (Remote Procedure Call) на базе gRPC или JSON-RPC архитектурно чище и эффективнее, чем искусственное создание псевдо-ресурсов.

Реализация паттерна «Action as a Resource» в Spring Boot 3

Рассмотрим реализацию бизнес-операции отмены заказа без глаголов в URL, с сохранением аудита и возвратом средств:

package com.example.controller;

import com.example.dto.CancelOrderRequest;
import com.example.dto.OrderCancellationDto;
import com.example.service.OrderCancellationService;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

import java.net.URI;

@RestController
@RequestMapping("/api/v1/orders/{orderId}/cancellations")
public class OrderCancellationController {

    private final OrderCancellationService cancellationService;

    public OrderCancellationController(OrderCancellationService cancellationService) {
        this.cancellationService = cancellationService;
    }

    // Создание ресурса отмены заказа (Action as a Resource)
    @PostMapping
    public ResponseEntity<OrderCancellationDto> cancelOrder(
            @PathVariable Long orderId,
            @Valid @RequestBody CancelOrderRequest request) {

        OrderCancellationDto cancellation = cancellationService.cancel(orderId, request);

        URI location = ServletUriComponentsBuilder.fromCurrentRequest()
                .path("/{cancellationId}")
                .buildAndExpand(cancellation.id())
                .toUri();

        // 201 Created с каноническим адресом созданной отмены
        return ResponseEntity.created(location).body(cancellation);
    }

    // Просмотр информации об отмене заказа
    @GetMapping("/{cancellationId}")
    public ResponseEntity<OrderCancellationDto> getCancellation(
            @PathVariable Long orderId,
            @PathVariable Long cancellationId) {
        return ResponseEntity.ok(cancellationService.findById(orderId, cancellationId));
    }
}
package com.example.service;

import com.example.dto.CancelOrderRequest;
import com.example.dto.OrderCancellationDto;
import com.example.entity.Order;
import com.example.entity.OrderCancellation;
import com.example.repository.OrderCancellationRepository;
import com.example.repository.OrderRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.Instant;

@Service
public class OrderCancellationService {

    private final OrderRepository orderRepository;
    private final OrderCancellationRepository cancellationRepository;

    public OrderCancellationService(OrderRepository orderRepository,
                                    OrderCancellationRepository cancellationRepository) {
        this.orderRepository = orderRepository;
        this.cancellationRepository = cancellationRepository;
    }

    @Transactional
    public OrderCancellationDto cancel(Long orderId, CancelOrderRequest req) {
        Order order = orderRepository.findById(orderId)
                .orElseThrow(() -> new ResourceNotFoundException("Order not found: " + orderId));

        if (!order.isCancellable()) {
            throw new IllegalStateException("Order in status " + order.getStatus() + " cannot be cancelled");
        }

        order.setStatus("CANCELLED");

        OrderCancellation cancellation = new OrderCancellation();
        cancellation.setOrder(order);
        cancellation.setReason(req.reason());
        cancellation.setCancelledAt(Instant.now());

        OrderCancellation saved = cancellationRepository.save(cancellation);
        return OrderCancellationDto.fromEntity(saved);
    }

    @Transactional(readOnly = true)
    public OrderCancellationDto findById(Long orderId, Long cancellationId) {
        OrderCancellation cancellation = cancellationRepository.findByOrderIdAndId(orderId, cancellationId)
                .orElseThrow(() -> new ResourceNotFoundException("Cancellation record not found"));
        return OrderCancellationDto.fromEntity(cancellation);
    }
}

🎯 Шпаргалка для интервью

30-секундный ответ

В чистом REST API глаголы в URL использовать не следует. В RESTful парадигме URI идентифицирует ресурс (существительное во множественном числе), а действие над ресурсом передается с помощью HTTP-метода (GET, POST, PUT, PATCH, DELETE).

Использование глаголов ломает семантику кэширования, безопасность (браузерный prefetch по GET) и превращает REST в примитивный RPC. Для сложных не-CRUD операций применяются REST-паттерны:

  1. Смена статуса через PATCH.
  2. Паттерн Action as a Resource (POST /orders/42/cancellations).
  3. Custom Methods по гайду Google (POST /documents/42:publish).

Если же операция представляет собой чистый алгоритм без персистентного состояния (калькулятор, валидатор), лучше открыто использовать RPC/gRPC, чем строить псевдо-REST.

4 каверзных вопроса с ответами

  1. Чем опасен эндпоинт GET /api/v1/users/10/delete в реальной веб-инфраструктуре? Ответ: Метод GET по спецификации RFC 9110 является безопасным (Safe) и кэшируемым. Браузерные плагины, ускорители предзагрузки страниц (prefetchers) и веб-краулеры поисковиков сканируют ссылки методом GET и случайно удалят пользователей. Кроме того, ответ может быть закэширован прокси-сервером.

  2. Как в REST API спроектировать операцию «Лайкнуть пост» без глаголов? Ответ: Добавление лайка оформляется как создание подчиненного ресурса: POST /posts/{postId}/likes (или идемпотентный PUT /posts/{postId}/likes/{userId}). Удаление лайка: DELETE /posts/{postId}/likes/{userId}. Это позволяет параллельно получать список тех, кто лайкнул: GET /posts/{postId}/likes.

  3. В чем разница между POST /orders/10/cancel и POST /orders/10/cancellations? Ответ: Первый вариант — это скрытый RPC-вызов (процедурный глагол). Второй вариант — это каноничный REST-паттерн «Action as a Resource»: клиент создает запись о факте отмены как отдельный доменный объект, у которого есть свой ID, временная метка и причина, доступные для последующего аудита через GET /orders/10/cancellations.

  4. Когда использование глаголов или RPC оправдано вместо REST? Ответ: Когда операция не оперирует персистентными ресурсами предметной области, а выполняет вычисления, преобразования данных или оркестрацию (POST /api/v1/convert-currency, POST /api/v1/render-pdf). В таких случаях использование gRPC или RPC-стиля в JSON является честным и эффективным архитектурным решением.

Красные флаги (чего категорически нельзя говорить)

  • ❌ «Глаголы в URL полезны, потому что сразу понятно, какой метод Java-сервиса вызовется» (мышление в парадигме RPC, нарушающее базовые принципы REST).
  • ❌ «Любую сложную операцию можно сделать через GET, если передать параметры в query» (грубейшее нарушение безопасности HTTP: GET никогда не должен менять состояние).
  • ❌ «В REST нельзя отменить заказ без глагола /cancel» (отмена прекрасно моделируется через PATCH статуса или ресурс /cancellations).
  • ❌ «REST подходит для 100% задач в бэкенде» (для real-time стриминга или сложных RPC вычислений лучше подходят WebSockets и gRPC).

Связанные темы