CodeGym /Courses /Spring REST & MVC /RPC-like API vs REST: without commands

RPC-like API vs REST: without commands

Spring REST & MVC
Level 3, Lesson 3
Available

1. How an API slides into an RPC-like style

We already have the basic grammar: collection, individual resource, GET/POST/PUT/PATCH/DELETE. But the business almost never comes with the wording “add one more PATCH”. It says: finish a task, assign an owner, attach a file, delete a comment. And exactly at that moment the API most often slides back into a command catalog.

At the service level this is normal: inside the application, completeTask() or uploadAttachment() sound natural. The problem starts when those same verbs become public paths like /completeTask and /uploadFileToTask. The external contract once again describes buttons, not domain objects.

What interests us here is a practical question: how to translate the language of business actions into the already familiar resource grammar.

RPC-like API: without religion and “holy wars”

By RPC-like API here we mean not a specific technology, but a characteristic contract pattern: the path answers the question “what should I execute?” rather than “what am I working with?”. Externally, the difference usually looks like this:

Comparison axis RPC-like approach (commands) Resource-like approach (resources)
Main meaning of the URI “What should I do?” “What am I working with?”
How the API grows New feature → new action endpoint New feature → refinement of the resource/representation/subresource
How the client “holds the model” You have to remember the command catalog You can infer it from the resource structure
Most common HTTP method Usually everything turns into POST Methods are used by meaning (GET, POST, PATCH, DELETE...)

No one forbids commands inside the application. The only question is whether the client should learn your method names instead of a clear map of resources.

2. When an API grows into a command catalog

A command-style API almost always grows the same way: for every new business request, another path with a verb appears. In Task Tracker API, this quickly turns into something like:

import java.util.List;

// This is what a "command catalog" looks like: growth happens by adding new verbs to paths
List<String> endpoints = List.of(
    "/createTask",
    "/completeTask",
    "/assignTask",
    "/addCommentToTask",
    "/uploadFileToTask"
);

The problem is not only the number of routes. The client has to learn your command catalog, the documentation breaks down into a list of unrelated actions, and the API stops looking like a map of the domain. As soon as you notice this, the next step almost suggests itself: take each business action and ask which object appears, changes, or is read.

3. Translating actions into the resource world

Now for the most useful part: what to do when the business says “we need action X”, while we want to stay in the resource style. A good pragmatic answer almost always starts not with choosing a URI, but with a simple question: which domain object appears or changes here? If you answer that honestly, the design usually “falls into place” on its own.

Here is a small cheat sheet in the form of a decision tree. It is not about perfect theory, but about keeping your brain turned on:

flowchart TD
    %% Idea: start not with the URI, but with the question "what object appears/changes?"
    A["We need an action (business operation)"] --> B{"Does a new object appear?"}
    B -- Yes --> C["This is creating a resource → collection (for example, comments/attachments)"]
    B -- No --> D{"Does the state of an existing object change?"}
    D -- Yes --> E["This is a resource update → change a representation (for example, status/assignee)"]
    D -- No --> F{"Do we just need to get data?"}
    F -- Yes --> G["This is reading a resource/collection"]
    F -- No --> H["Check: are we trying to hide a complex process behind a 'button'?"]

Let us look directly at our domain. Here is how a “command” turns into a resource-based solution without magic:

Imagine the command: “Add a comment to a task”. If you think in commands, your hand reaches for /addCommentToTask. But if you ask “what appears?”, the answer is simple: a new comment appears. So a comment is a resource (a subresource of the task), and it has a place in the API as a collection of comments inside the task.

import java.util.Map;

// Mental translation: "operation name" -> "where it fits in the resource map"
Map<String, String> operationToResource = Map.of(
    // A comment is a new object, so this is creating an element in the comments collection
    "addCommentToTask", "/tasks/{taskId}/comments",
    // An attachment is also a new object (metadata + file), so attachments is a collection
    "uploadFileToTask", "/tasks/{taskId}/attachments"
);

Command: “Upload a file to a task”. A new object appears — attachment metadata. So this is also creating a subresource: the attachments collection in the context of the task.

Command: “Assign an owner”. Here no new object appears, but the state of the task changes (for example, the assigneeName field). So this is changing the existing resource task. In the resource model, the client works with the task, and assignment is just a change to its state, not a separate command from another world.

Command: “Complete a task” (complete). If we are honest, in our domain this is most likely a transition of the status to DONE. Again: the state of the task changes, not a separate entity called “Completion” appears. So the task remains the main resource, and “complete” is a special case of changing its state.

A very important discipline: inside the service you can continue using verb-based methods. That is normal. Just do not let the external API copy their names.

class TaskService {
    void completeTask(String taskId) {
        // Inside the service the verb is natural: it is part of implementing a business operation
        // Outside (in the HTTP contract) we do not want to turn this into an address like /completeTask
    }

    void assignTask(String taskId, String assigneeName) {
        // Likewise: an internal command is fine
        // Outside, this will usually be an update of the resource /tasks/{taskId} (for example, the assignee field)
    }
}

The problem is not verbs as such. The problem is that the verb becomes the address in the public contract, and the client starts living in your “command dictionary” instead of the domain.

4. Pragmatic REST: discipline without perfectionism

Pragmatic REST is needed here not to argue about style purity, but to avoid immediately sliding into two extremes: into a command catalog on one side and academic perfectionism on the other. If the resource model makes the API clearer to the client, it is worth keeping; if an argument about “perfect REST” adds nothing to the contract, it only creates noise.

It is useful to compare the three states that teams often fall into:

Style What it looks like from the outside What happens inside the project
RPC-like chaos Many verb paths, an “action endpoint” for every little thing The client learns commands, and the API grows like a list of buttons
Dogmatic REST Everything is strictly “by the book”, but sometimes excessively complex and inconvenient Odd solutions sometimes appear “for the sake of correctness”
Pragmatic REST The resource is at the center; actions are expressed through state/subresources, not a command catalog The API stays readable without getting in the way of real development

And here is an important point: sometimes in real commercial development you encounter operations that are hard to express without an “action”. For example, “export a report” or “start a recalculation” can be a separate process that lives as a “job” resource. But note: even there, a good design often turns the action into a resource (“export job”, “report export”) rather than /doExportNow.

In Task Tracker API, we deliberately keep the model simple: the main story is tasks, and among supporting elements are comments and attachments; tags are a lookup list of values. At this scale, pragmatic REST almost always means one thing: do not proliferate commands if the meaning can be expressed through a resource, its state, or a subresource.

5. Quick test: signs of a command-style API

When you design an API, it helps to have a quick self-check tool — like the smell of smoke in the kitchen. You do not necessarily understand right away what is burning, but it is important to notice in time that “something is definitely wrong”. For RPC-like design, such a test can be reduced to just three questions. And do not turn this into bureaucracy: it is just a short pause before you add a new endpoint.

The first question is: “Can I say in one sentence which resource I am addressing?” If you say “I am calling completeTask”, then the resource was not mentioned. If you say “I am working with a specific task”, then the resource was mentioned.

The second question: “Does a new object appear?” If yes, this is usually a collection — creating a new element — and you almost automatically end up with a subresource or a separate collection. A comment is a great example: it appears, it has its own id, and it lives in the context of the task.

The third question: “Am I adding a new button instead of describing a state change?” If you make /changeStatus and /assignTask, that looks a lot like “buttons”. If instead you say “the task has a status and an assignee, and we change the task state”, the model becomes more natural.

You can even make a toy check in code — not for the sake of a “correct algorithm”, but simply so that your brain pauses for a second:

record ApiCheck(String path) {
    boolean looksLikeCommand() {
        // This is not a "REST validator", but a rough heuristic: a verb at the start of the path = red flag
        return path.startsWith("/create")
            || path.startsWith("/update")
            || path.startsWith("/delete")
            || path.startsWith("/complete")
            || path.startsWith("/upload");
    }
}

// Example: the path starts with a command, so the heuristic triggers
ApiCheck a = new ApiCheck("/completeTask");
System.out.println(a.looksLikeCommand()); // true

Again: this is not a “REST detector”. It is a reminder: if you see that the path starts with a command verb, stop and try to rephrase the question: “what resource is this really?”

6. Common mistakes when trying to “avoid RPC-like”

Mistake #1: confusing “RPC-like is bad” with “verbs are forbidden altogether”.
Beginners sometimes take the rule “do not use /completeTask” and turn it into the dogma “a URI can never contain verbs”. As a result, a strange game of euphemisms begins: the path does not become clearer, it just becomes less honest. In this lecture, what matters is not banning the word itself, but asking about readability: is the resource and its place in the model visible?

Mistake #2: replacing one action endpoint with another without returning to the resource.
Sometimes a team realizes that /uploadFileToTask looks bad and makes, say, /taskFileUpload. That is cosmetics. If you have not answered the question “is this a subresource? a new object? a state change?”, you have simply changed the sign on the same command store.

Mistake #3: trying to “make REST” through mechanical renaming while keeping the command model.
It happens that the path already “looks like a resource”, but inside it is still a command: always POST, always different strange input forms, each endpoint lives its own life. REST is not only “how to name the path”; it is also about a stable model of operations around resources. We are not going into implementation details yet, but it is important to remember: replacing the vocabulary without changing the mindset does not fix anything.

Mistake #4: treating pragmatism as permission for chaos.
“Pragmatic REST” does not mean “let us do it as fast as possible”. It means “let us keep the resource model and not complicate it where that brings no value”. If, under the banner of pragmatism, you start adding /doThis, /doThat, /doSomethingElse, you are simply returning to the RPC-like style, only now you also have a moral indulgence.

Mistake #5: forgetting that commands inside the application are normal, but outside they are not.
Sometimes a developer starts fighting their own code: “If it is REST, then even inside I must not write completeTask().” Do not do that. Inside you are building an implementation, and there verb-based methods are a natural thing. Outside you are building a contract, and there a resource map and predictability matter more. When a compact resource map of the project is in front of you, that boundary becomes much more visible: service commands may live inside, but outside you do not get a button catalog — you get a map of the domain.

1
Task
Spring REST & MVC, level 3, lesson 3
Locked
Command Path Detector
Command Path Detector
1
Task
Spring REST & MVC, level 3, lesson 3
Locked
Translating business actions into resource language
Translating business actions into resource language
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION