Стоит ли использовать глаголы в 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):
- Поисковые роботы и краулеры (Googlebot, Яндекс) автоматически переходят по всем ссылкам
<a>на странице и могут случайно уничтожить данные. - Браузерный prefetching (предзагрузка ссылок для ускорения открытия страниц) выполнит запрос в фоновом режиме без ведома пользователя.
- Промежуточные прокси и 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-паттерны:
- Смена статуса через
PATCH.- Паттерн Action as a Resource (
POST /orders/42/cancellations).- Custom Methods по гайду Google (
POST /documents/42:publish).Если же операция представляет собой чистый алгоритм без персистентного состояния (калькулятор, валидатор), лучше открыто использовать RPC/gRPC, чем строить псевдо-REST.
4 каверзных вопроса с ответами
-
Чем опасен эндпоинт
GET /api/v1/users/10/deleteв реальной веб-инфраструктуре? Ответ: МетодGETпо спецификации RFC 9110 является безопасным (Safe) и кэшируемым. Браузерные плагины, ускорители предзагрузки страниц (prefetchers) и веб-краулеры поисковиков сканируют ссылки методомGETи случайно удалят пользователей. Кроме того, ответ может быть закэширован прокси-сервером. -
Как в REST API спроектировать операцию «Лайкнуть пост» без глаголов? Ответ: Добавление лайка оформляется как создание подчиненного ресурса:
POST /posts/{postId}/likes(или идемпотентныйPUT /posts/{postId}/likes/{userId}). Удаление лайка:DELETE /posts/{postId}/likes/{userId}. Это позволяет параллельно получать список тех, кто лайкнул:GET /posts/{postId}/likes. -
В чем разница между
POST /orders/10/cancelиPOST /orders/10/cancellations? Ответ: Первый вариант — это скрытый RPC-вызов (процедурный глагол). Второй вариант — это каноничный REST-паттерн «Action as a Resource»: клиент создает запись о факте отмены как отдельный доменный объект, у которого есть свой ID, временная метка и причина, доступные для последующего аудита черезGET /orders/10/cancellations. -
Когда использование глаголов или 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).