API Design Alternatives: Remote Procedural Calls
Brief history of REST
Roy Fielding introduced REST in his 2000 dissertation. In that dissertation, he stated "Listen, we just designed HTTP, so if you also find yourself designing a distributed hypermedia system you should use this cool architecture we worked out called REST to make things easier."
If you have never heard of a hypermedia system, then I am 99.999% certain you're not designing RESTful APIs; at least not according to Roy Fielding.
But, as the great William Shakespeare wrote in Romeo and Juliet, "What's in a name?". Just because the intent Roy Fielding had for RESTful APIs was to design distributed hypermedia systems doesn't mean that many of the principles couldn't have been carried into designing other types of systems - unfortunately for us, instead of coming up with a new standardized name for those APIs, REST simply took on a split identity; one identity holding strongly to the original intent and focus on designing hypermedia systems; the other practically erasing any mention of hypermedia systems.
Hypermedia systems
What does a distributed hypermedia system REST API look like? Imagine that you are designing a system for a library. In a library, your main resource is books. So, inevitably, you would need to have some sort of API that can request a catalogue of books.
GET /books
Accept: application/json
Now, when you request that resource - under the context of a hypermedia system - you can expect a response along the lines of:
{
"items": [
{
"id": "123",
"title": "The best article ever written",
"author": "Jake Simpson",
"_links": {
"self": {
"href": "/books/123"
},
"author": {
"href": "/authors/jake-simpson"
},
"purchase": {
"href": "/orders?book=123"
}
}
}
],
"_links": {
"self": {
"href": "/books"
},
"next": {
"href": "/books?page=2"
},
"create": {
"href": "/books",
"method": "POST"
}
}
}
While this looks like a completely standard JSON type API response, I'm sure you'll have noticed the "_links" within
the response payload. According to Roy Fielding's dissertation, the expectation is that from this point on in you're
journey you would only interact with the state via the hypermedia controls (e.g. GET _links.next).
Modern REST
Considering we don't see hypermedia links practically ever in any APIs, it's safe to say that the definition of what makes a REST API has changed over time. In almost every article discussing what a RESTful API is, we get the following:
- Stateless - client provides all necessary data for making the request.
- Uniform interface - fancy way of saying uses HTTP verbs for operations.
- Representational state - the resulting resource (generally a JSON object in today's world).
In many instances, there are additional details around all actions being resource based. Take a Todo App for example,
you don't have any todo's, you create a todo, you read the todo, you complete the todo (update), and then you
decide you no longer want todo (delete).
All these actions taken revolve around this thing that is a todo. It's simple and straightforward. The todo is the
representation state.
Unfortunately for us, life has never been simple. Or straightforward. Instead, it's complex and chaotic. REST has a strange dichotomy between being overly restrictive and not restrictive enough. This is no fault of REST, but more the individuals, teams and organizations that design their "RESTful" APIs; resulting in my belief that more often than not, REST is the incorrect solution to most non-trivial APIs.
There are many alternatives, but this is where I will pitch RPC as an alternative API design pattern.
Remote Procedural Call (RPC) APIs
Remote Procedural Call (RPC) APIs are designed with the idea that a client is calling a remote function that executes a set of procedures and returns a response. Fundamentally, one of the greatest advantages of RPC is that is not constrained to the concept of a resource. This provides far greater flexibility in designing complex API operations (a.k.a. any modern application).
So what are the rules for designing an RPC API?
- Remote - the procedure call must not be local.
- Callable - the function must be callable ... remotely.
Sounds a bit snarky, I know. But the reality of RPC is that, just like with any other function, you are agreeing to a contract - that being that there is a statement of availability for the remote procedural call; that call will perform an action, or a set of actions; and it will return an agreed upon response.
As to how you access that RPC is up to the engineer implementing the API. Want to use HTTP? Go for it. Want to use TCP? Why not!?
Now, this is an over-simplification. But that is the point. RPC doesn't have an explicit set of standards or expectations. It is completely up to the engineer of the API. However, just because it is up to the engineer to decide all these things doesn't mean that the API will be successful without proper documentation. In fact, it is far more crucial to provide detailed and complete documentation for any given API. As I mentioned before, RPC APIs are designed to be an explicit contract and should be documented as such.
Note: There is also an RPC framework known as "gRPC". This is not the same as what I am writing about here. Instead, Google decided to take RPC as a principle and build a framework around it.
Loan processing
With definitions behind us, let's take a look at an example with a modicum of complexity - a loan application. While there are complexities that we won't dive into here when it comes to loan processing, we can think of it in the simplest of ways as:
- Loan application form gets submitted
- Loan application goes under review
- it gets approved
- it gets rejected
RESTful approach
Let's take a look at what the API would look like in the RESTful world.
POST /loanApplication
Creates a new loan application. This step is more or less trivial and non-controversial.
POST /loanApplication
{
"first_name": "Bob",
"last_name": "Saget"
"annual_salary": 100_000,
"requested_loan_amount": 1_000_000
}
HTTP/1.1 201 Created
{
"id": "12444",
"first_name": "Bob",
"last_name": "Saget",
"annual_salary": 100_000,
"requested_loan_amount": 1_000_000,
"status": "in_review"
}
GET /loanApplication/12444
Now let's say that I am impatient and want to check the status of my application. In RESTful APIs, what we would
typically see is a GET request for the loan application; and because it's REST, the expected behavior would be to
return the resource itself which would include the status.
GET /loanApplication/12444
HTTP/1.1 200 OK
{
"id": "12444",
"first_name": "Bob",
"last_name": "Saget",
"annual_salary": 100_000,
"requested_loan_amount": 1_000_000,
"status": "in_review"
}
PUT /loanApplication/12444
Updates the loan application (approves/rejects).
This is where things start to fall apart for RESTful APIs. If we were strictly following REST standards, then this is what the implementation would look like.
PUT /loanApplication/12444
{ "status": "approved" }
HTTP/1.1 200 OK
{
"id": "12444",
"first_name": "Bob",
"last_name": "Saget",
"annual_salary": 100_000,
"requested_loan_amount": 1_000_000,
"status": "approved"
}
OR
PUT /loanApplication/12444
{ "status": "rejected" }
HTTP/1.1 200 OK
{
"id": "12444",
"first_name": "Bob",
"last_name": "Saget",
"annual_salary": 100_000,
"requested_loan_amount": 1_000_000,
"status": "rejected"
}
Now, while the above implementation follows REST strictly, what I typically end up seeing in these kinds of situations is something like the following.
PUT /loanApplication/12444/approve
HTTP/1.1 200 OK
{
"id": "12444",
"first_name": "Bob",
"last_name": "Saget",
"annual_salary": 100_000,
"requested_loan_amount": 1_000_000,
"status": "approved"
}
In this implementation, we start to fade into the dark side of RESTful APIs. Without it being explicitly pointed out,
you might have read PUT /loanApplication/12444/approve endpoint as being a perfectly valid endpoint - and technically
speaking, it is valid as an HTTP endpoint; but this begins to stray from REST semantics. This is because it's now
introducing a verb in the actual API endpoint. Approve isn't a resource, instead it's an action that is being
taken on the loan application resource.
The same thing goes for PUT /loanApplication/12444/reject. This fundamentally does not follow REST standards. And
I'm not even talking about the original, hypermedia-driven REST standards - but rather the 100% modern, mainstream
REST best practices that can be found in just about every blog or article on REST standards.
RPC approach
So what does this look like as an RPC API?
POST /createLoanApplication
Creates the loan application. Again, nothing too controversial here. The endpoint simply becomes the verb and
POST indicates that we have chosen to use HTTP as our transfer protocol.
POST /createLoanApplication
{
"first_name": "Bob",
"last_name": "Saget"
"annual_salary": 100_000,
"requested_loan_amount": 1_000_000
}
HTTP/1.1 201 Created
{
"id": "12444",
"first_name": "Bob",
"last_name": "Saget",
"status": "in_review"
}
POST /getLoanStatus
Again, let's say that I want to check on the status of my loan application. In the world of RPC, because we are not resource bound we can explicitly return just the loan's status. There is no expectation that the entire resource is returned, and so we can dictate as part of the contract "hey, send a request for the status here and provide the ID of the loan you'd like to get the status for, and we'll give you the requested status value".
POST /getLoanStatus
{ "id": "12444" }
HTTP/1.1 200 OK
{ "status": "in_review" }
POST /approveLoan
Approves the loan. This is where the fun really begins. Because RPC frees us from being bound to a resource, it feels more natural that this is a business concern. For example, the approval of a loan application might involve validating documents and allocating the funds. With RPC, that is completely ok. It is part of the contract of using that procedural call.
This is in contrast to RESTful APIs where it can feel wrong to go outside of scope of the resource that should be getting modified because it is far more implicit in RESTful APIs.
POST /approveLoan
{ "id": "12444" }
HTTP/1.1 200 OK
{ "status": "approved" }
POST /rejectLoan
Rejects the loan. This is similar to the loan approval in that there are procedural actions that occur upon rejection.
POST /rejectLoan
{ "id": "12444" }
HTTP/1.1 200 OK
{ "status": "rejected" }
While this example might be contrived, the idea is plain. Throughout the loan processing cycle, there are many, many things that can happen in relation to a loan application. And trying to model a RESTful API around that can make a very complex process even more difficult to design for.
Summary
The world of tech in 2025 is a complex and interoperable system. One in which the foundational principles of REST no longer work well. And in a lot of cases, instead of recognizing that fact and explicitly deciding to no longer follow REST, I often see individuals and teams bastardize their API designs in a way that becomes even more difficult to manage.
There are alternatives. In fact, there is no need to follow any singular thing. If it makes sense for a project to have its own framework, taking ideas and principles from other standards and specifications, that is fine - encouraged even. Often times, these generalized standards are only in place to help individuals that are new to a project have a baseline understanding of how to work; "oh, you use HTTP? Got it. You have a custom HTTP status code? Interesting. Where is the documentation for that?".
Designing and architecting is always up to the engineer. My goal isn't to say that everybody should transition to designing Remote Procedural Call APIs, or nobody should design RESTful APIs. It's simply this, when designing non-trivial APIs a Remote Procedural Call approach is far more flexible.