Чи варто використовувати дієслова в 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).