🌐 Розділ 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).

Пов’язані теми