🌐 Section 6 · Question #13

Should you use verbs in URL

The fundamental architectural principle of REST mandates a strict separation of concerns between entities and operations:


🟢 Junior Level

In canonical RESTful architecture, verbs should NOT be used in URIs.

The fundamental architectural principle of REST mandates a strict separation of concerns between entities and operations:

  • The URI (Uniform Resource Identifier) specifies the Resource (a noun: what or who).
  • The HTTP Method specifies the Action (a verb: what to do).

Procedural RPC vs. Canonical RESTful Design

Anti-Pattern (Procedural RPC with Verbs) Canonical RESTful Design Architectural Rationale
GET /api/v1/getUser?id=42 GET /api/v1/users/42 Retrieves representation of user 42 via safe read.
POST /api/v1/createUser POST /api/v1/users Creates a new subordinate resource within the collection.
POST /api/v1/updateUser PUT /api/v1/users/42 (or PATCH) Completely replaces or partially modifies user 42.
GET /api/v1/deleteUser/42 DELETE /api/v1/users/42 Removes user 42 from the system.

Why Verbs in URLs Are Dangerous (The GET Mutator Trap)

Exposing destructive or state-mutating actions over GET (e.g., GET /api/orders/10/cancel or GET /logout) introduces severe production hazards:

  1. Search Crawlers & Web Spiders: Bots (Googlebot, Bingbot) automatically follow all hyperlinks (<a href="...">) found in web applications. If actions are wired to GET, a spider will systematically delete or cancel records across your database.
  2. Browser Link Prefetching: Modern browsers speculatively prefetch resources for links visible in the viewport to accelerate perceived navigation, triggering accidental background mutations without user consent.
  3. Intermediary Caching: CDNs and forward proxies treat GET requests as safe and cacheable. The first user’s cancellation response will be cached, causing subsequent users’ requests to be served from cache without ever reaching your backend!

🟡 Middle Level

Modeling Non-CRUD Business Actions (Reification)

A classic interview challenge asks:

“How do you cancel an order, send an invoice, or reset a password in REST without using verbs in the URL?”

Architects apply three proven RESTful strategies:

1. Lifecycle State Mutation via PATCH

If the business action is fundamentally a state transition in an entity’s lifecycle:

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

{
  "status": "CANCELLED"
}

2. Action as a Resource (Reification)

Instead of a procedural verb, elevate the action into a first-class domain resource. This allows the system to attach audit metadata (e.g., cancellation timestamp, reason, refund amount, authorizer):

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

{
  "reason": "Customer requested refund",
  "refundAmount": 120.00
}

Response: 201 Created with a Location: /api/v1/orders/100/cancellations/canc-88 header. The cancellation can subsequently be audited via GET /api/v1/orders/100/cancellations.

3. Asynchronous Job / Command Resources

For heavy, long-running processes (e.g., batch data exports, video rendering):

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

Response: 202 Accepted with Location: /api/v1/reports/export-jobs/job-501. Clients poll status via idempotent GET.


Pragmatic Custom Methods (Google Cloud & GitHub Standards)

When domain workflows become exceptionally complex, even tech giants adopt a standardized pragmatic syntax.

Google Cloud API Design Guide: Google separates entities from custom verbs using a colon (:) at the end of the URI path:

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

This isolates the canonical resource path (/v1/documents/42) while cleanly identifying the custom non-CRUD procedure.


🔴 Senior Level

REST vs. RPC: When Verbs Are the Right Architectural Choice

Attempting to force every procedural computation into artificial REST resources causes REST Over-Engineering:

Stateless Tax Calculator: POST /api/v1/calculate-taxes
Spelling & Grammar Engine: POST /api/v1/spell-check
Currency Converter:        POST /api/v1/convert-currency

In these scenarios, there is no persistent domain entity (no “tax” or “word” with an ID to read, replace, or delete via GET/PUT/DELETE). Forcing these into pseudo-resources (POST /tax-calculation-requests) adds unnecessary overhead.

In pure computation or real-time orchestration workflows, a clean RPC (Remote Procedure Call) interface over gRPC or JSON-RPC is technically superior and more transparent than simulated REST.


Production Implementation: “Action as a Resource” in Spring Boot 3

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;
    }

    // 1. Reification: Action modeled as subordinate resource creation
    @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();

        return ResponseEntity.created(location).body(cancellation); // 201 Created + Location
    }

    // 2. Auditability: Inspecting the cancellation record
    @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.exception.ResourceNotFoundException;
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 (!"PENDING".equalsIgnoreCase(order.getStatus())) {
            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);
    }
}

Dynamic State Transitions via HATEOAS

Instead of exposing static procedural endpoints, HATEOAS delivers permitted state transitions dynamically inside _links:

// GET /api/v1/orders/42 (Order is in PENDING state)
{
  "orderId": 42,
  "status": "PENDING",
  "_links": {
    "self": { "href": "/api/v1/orders/42" },
    "cancel": { "href": "/api/v1/orders/42/cancellations", "method": "POST" },
    "payment": { "href": "/api/v1/orders/42/payments", "method": "POST" }
  }
}

When the order moves to SHIPPED, the backend removes the cancel link, instructing clients that cancellation is no longer a permitted state transition.


4 Tricky Questions

1. Why is having a mutating action like GET /api/v1/users/10/delete catastrophic in production?

Answer: Under RFC 9110, GET is a Safe and Idempotent method intended strictly for reading:

  1. Search Crawlers: Web crawlers (Googlebot, Bingbot) and security scanners automatically traverse all accessible GET hyperlinks, triggering mass data destruction.
  2. Browser Prefetching: Browsers proactively execute GET requests on speculative links in the background, deleting user data without manual interaction.
  3. Caching: Reverse proxies and CDNs will cache the response, preventing subsequent deletion requests from reaching the origin server.

2. How should you design a “Like a Post” operation in REST without using verbs?

Answer: Model the “Like” as a subordinate domain resource:

  • Add Like: POST /api/v1/posts/{postId}/likes (or idempotent PUT /api/v1/posts/{postId}/likes/{userId}).
  • Remove Like: DELETE /api/v1/posts/{postId}/likes/{userId}.
  • Inspect Likes: GET /api/v1/posts/{postId}/likes (returns collection of users who liked the post). This preserves clean REST semantics and enables full querying capabilities on the relationship.

3. What is the fundamental difference between POST /orders/10/cancel and POST /orders/10/cancellations?

Answer:

  • POST /orders/10/cancel is a procedural RPC endpoint masquerading as HTTP. It treats the endpoint as an arbitrary method execution and returns an ad-hoc result.
  • POST /orders/10/cancellations follows the Reification pattern. It treats the cancellation as an independent domain entity (with an ID, timestamp, reason, and authorizer) and returns 201 Created with a canonical URI (Location: /orders/10/cancellations/1), supporting future auditing via GET.

4. When is using verbs or an RPC approach superior to forcing RESTful resources?

Answer: When an operation represents an ephemeral computation, transformation, or orchestration rather than a persistent domain resource:

  • Examples: Currency conversion (POST /convert), text translation (POST /translate), tax estimation (POST /calculate-tax), or PDF document rendering (POST /render-pdf).
  • Creating artificial CRUD resources for temporary calculations adds pointless abstraction layers. Utilizing gRPC, JSON-RPC, or pragmatic action controllers (POST /calculate) is cleaner, faster, and more honest.

🎯 Interview Cheat Sheet

30-Second Elevator Pitch

In canonical REST architecture, verbs should not appear in URIs. The URI identifies the resource (noun), while the HTTP method declares the action (verb).

Using verbs in URIs—especially over GET—breaks web caching, security contracts, and invites accidental data loss from browser prefetching and search crawlers.

To model complex business actions:

  1. Mutate state via PATCH /orders/42 {"status": "CANCELLED"}.
  2. Apply Reification (POST /orders/42/cancellations returning 201 Created).
  3. Use Custom Methods per Google Cloud guidelines (POST /documents/42:publish).
  4. For purely algorithmic calculations (tax calculators), prefer honest RPC/gRPC over simulated REST.

Red Flags (What NOT to Say)

  • ❌ “Verbs in URLs are fine as long as they make the Java method name clear.” (Thinking in procedural RPC rather than Resource-Oriented Architecture).
  • ❌ “Any operation can be bound to GET as long as parameters are in query strings.” (GET must remain strictly Safe and read-only).
  • ❌ “It is impossible to model an order cancellation in REST without using /cancel.” (Cancellation is easily modeled via PATCH or a /cancellations sub-resource).
  • ❌ “REST must be used for 100% of all microservice endpoints.” (Stateless computational engines and high-throughput internal IPC are better served by gRPC).