DEV Community

Cover image for Why you (probably) don't need a REST API
Melvin Fengels
Melvin Fengels

Posted on • Originally published at pragmatic-code.hashnode.dev

Why you (probably) don't need a REST API

REST is the gold standard of API design. We all know it, we all use it, and we all (pretend) to love it.

But let's look at the reality of development today. Most of us aren't building public-facing APIs for unknown consumers. We are building backends for a single, specific frontend that we own.

As fullstack developers, we often find ourselves asking: Why am I spending time debating HTTP verbs and resource hierarchies when I just want to call a function? Why am I strictly decoupling two applications that will live and die together?

It’s time to revisit a "new" (technically very old) alternative: The RPC API.

What is a REST Api

REST is Resource-centric. You model your API around nouns (Users, Orders, Items). You interact with these nouns using standard HTTP verbs (GET, POST, PUT, DELETE) to perform CRUD operations.

  • The URL: Describes what you are acting on (e.g., /api/users/123).
  • The Verb: Describes what action you are taking.

A Note for the Purists:
Technically, Roy Fielding’s original definition of REST focuses heavily on Hypermedia (HATEOAS) and the transfer of state via HTML. What the industry calls "REST" today is technically "RESTful JSON APIs." For the sake of this article, when I say REST, I mean the modern JSON-over-HTTP standard we all use.

What is an RPC Api

RPC is Action-centric. Instead of mapping your logic to HTTP resources and verbs, you organize your Api into Services and Methods.

  • The Structure: You group related logic into Services (e.g., AuthService, CartService). Each service has specific Methods (functions) that perform an action.
  • The Mental Model: You are simply calling a function on the server, exactly like you would call a function in your local code.
  • The Abstraction: While the underlying transport might still be HTTP (usually a POST), you stop caring about the URL or the HTTP method. You care about the Service, the Function, and the Arguments.

The Inherent complexity of REST

Building a REST API sounds simple on paper: Everything is a resource. But in practice, this model introduces a massive amount of cognitive overhead.

The Mapping Problem

When you build a feature, you think in terms of actions: "I need to cancel this order."
REST forces you to translate that simple action into a resource state.

  • Is "Cancel" a PATCH to /orders/1 updating a status field?
  • Or is "Cancellation" a resource of its own, requiring a POST to /orders/1/cancellation?
  • What HTTP status code do I return? 200 OK? 202 Accepted? 204 No Content?

Suddenly, you aren't writing logic; you are debating semantics.

The "Generic Resource" Trap

REST encourages you to build generic resources that serve many masters. Your GET /items endpoint is expected to return a standard "Item" object.

This leads to the classic Over-fetching vs. Under-fetching dilemma:

  • Over-fetching: The mobile list view only needs the title and price, but your API returns a massive JSON object with 50 fields, wasting bandwidth.
  • Under-fetching: You fetch the Item, but then you need three more requests to get the Author, Comments, and Reviews.

The Filter Nightmare

And then there is filtering. A simple requirement like "Get me all red items under $50 sorted by popularity" turns your clean URL into this:

GET /items?color=red&max_price=50&sort_by=popularity&sort_order=desc

While modern frameworks handle the parsing for you, the URL structure itself becomes the bottleneck. Trying to pass complex, nested filter objects or arrays via the URL quickly becomes messy, unreadable, and hits length limitations. Eventually, many teams give up and create a POST /items/search endpoint—effectively admitting that the REST model has broken down.

The "Band-Aid" Solution: OpenAPI

Because REST is hard, we built tools to make it easier. The current industry standard is OpenAPI.

If you work Schema First, OpenAPI is genuinely helpful:

  1. Frontend: It generates a client SDK for you, so your API calls look like functions (api.orders.cancel(id)).
  2. Backend: It generates interfaces and controllers, so you don't have to manually type out validation logic.

But OpenAPI is a partial solution. It solves the Implementation, but it doesn't solve the Design.

Even with the best code generation in the world, you still have to design the schema itself. You still have to spend mental energy mapping your application's actions to resources, verbs, and HTTP status codes. You still have to think about making your endpoint return the correct amount of data. You are automating the syntax of REST, but you are still stuck with the difficult part, the semantics.

The Developer Experience

Let's look at this through the lens of a fullstack developer.

In your application code, when you need data or need to perform an action, you call a function.
const user = userService.getUser(id);

You don't really think about "resources." You think about what you want to do and what service could do it.

Why does this simplicity have to stop at the network boundary?

With REST, we introduce an artificial Translation Layer that we have to maintain on both sides.

  1. Frontend Intent: "I want to get a user."
  2. The Translation Layer: How do I map this to HTTP? -> "Okay, that's a GET request to /users/:id."
  3. Backend Controller: Receive GET /users/:id.
  4. Reverse Translation: What function does this URL map to? -> "Okay, call userService.getUser(id)."

With RPC, we remove the translation entirely.

  1. Frontend Intent: client.users.get(id)
  2. Backend Execution: userService.get(id)

As a developer, I usually don't actually care how the data gets to me. I just want the data. By treating the API as just another set of functions, we align the backend architecture with the way we actually write code.

Shifting the Mindset

If you are a seasoned REST developer who has read this far, you probably already had one or more of these thoughts (or you will have them the moment you try your first RPC-style API).

It feels uncomfortable at first. Here is why, and how to get past it.

1. "Wait, where did my semantic meaning go?"

The Thought: You will instinctually feel wrong using a POST request to fetch data. You miss the clarity of seeing DELETE /users/1 and knowing exactly what it does.

The Shift: Realize that you don't rely on "transport layer semantics" anywhere else in your code.
When you write a JavaScript function, you don't need a special flag to tell you it deletes a user. You rely on Naming. deleteUser(id) is semantic enough. Why do we demand more from our network calls than our actual code?

2. "This feels way too tightly coupled."

The Thought: You will feel "dirty" writing a backend endpoint that returns exactly the three fields your specific button needs. You have been trained to decouple, to make things generic, to build for the future.

The Shift: Ask yourself: "Who am I decoupling this for?"
If you are the only consumer, generalized decoupling is often just premature optimization.
If your backend exists solely to serve your frontend, stop treating them like strangers. By acknowledging they are coupled, you stop guessing what data "might" be needed later and start optimizing for what is needed now.

3. "This feels fragile. If I change the endpoint, everything breaks."

The Thought: In REST, if you rename a field, the endpoint usually still returns 200 OK (it just lacks the data). In RPC, if you change a method signature, the call fails completely.

The Shift: You are conflating Fragility with Versioning.
If you introduce a breaking change (like renaming a required field), you should be creating a new version of that endpoint, regardless of whether it is REST or RPC. If you don't, you have broken the contract in both scenarios.

The difference is what happens when you do make that mistake:

  • REST: Returns a 200 OK, but the data is missing. The user sees a blank profile. This is a silent bug.
  • RPC: The call explodes. If you use TypeScript, the build fails. RPC isn't more fragile; it's just honest. It forces you to handle the versioning issue immediately rather than letting you ship a broken UI.

4. "But how do I handle errors without HTTP Status Codes?"

The Thought: In REST, I know that 404 means not found and 500 means server error. My monitoring tools rely on this.

The Shift: You have two choices here.

  1. Pure RPC: Return 200 OK for everything and include an error field in the payload. Think of it how errors are handled in Rust, errors are values. If you design your object like a Result in Rust, you are forced to manually handle possible errors.
  2. Pragmatic RPC: There is no law saying RPC must return 200. You can still throw a 401 if the user isn't logged in. You can still throw a 500 if the database explodes. Don't let the dogma of "Pure RPC" stop you from using HTTP status codes if they are useful to you.

5. "But with a GET request, I get caching for free"

The Thought: With REST, a GET request is safe and cacheable by default. Browsers, CDNs, proxies, and even HTTP libraries can automatically cache responses for you. If everything becomes a POST, don’t you lose this for free?

The Shift: This is valid for static content, but for application state, it is often even the wrong thing.

  1. The Scale Reality: Ask yourself: "How busy are my servers really?" Unless you are serving millions of concurrent users, the cost of hitting the database for a fresh user profile is negligible. Don't complicate your architecture to solve a scaling problem you don't have yet.

  2. The Stale Data Problem: In modern SaaS apps, "stale data" is the enemy. We often find ourselves fighting against HTTP caching (adding no-store headers) to ensure the user sees the latest state.

  3. Better Alternatives: If you identify a bottleneck and decide you do need caching, move it to the client. Modern frontend tools (like TanStack Query or SWR) handle this in memory. They give you a smart, application-aware cache that is far more powerful than the blunt instrument of HTTP caching.

The Reality Check

I am not the only one noticing this shift. The explosive popularity of frameworks like tRPC (TypeScript RPC) or React Server Actions proves that developers are hungry for this simplicity.

These tools are not trying to be "RESTful." They are explicitly trying to blur the line between backend and frontend, turning the network boundary into an implementation detail rather than an architectural wall.

The Tooling Landscape (Beyond Node.js)

I’ve mentioned tRPC and React Server Actions, but those are specific to the TypeScript ecosystem. What if you are writing Go, Python, or C#?

You might think of gRPC. It is the industry standard for high-performance RPC. However, gRPC uses binary buffers (Protobuf) which are fantastic for server-to-server communication but can be cumbersome to consume directly from a web browser (requiring proxies like gRPC-Web).

So, what is the "sweet spot" for a generic, JSON-based RPC API?

1. The "JSON-RPC" Standard

There is an actual specification for this called JSON-RPC 2.0. It defines a standard way to format a JSON body to call a method.

  • Request: { "jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": 1 }
  • Response: { "jsonrpc": "2.0", "result": 19, "id": 1 }

Many languages have libraries that implement this out of the box. It’s boring, stable, and works everywhere.

2. "RPC-Style" OpenAPI

You don't need a special framework to build an RPC API. You can simply use your existing REST tools (like ASP.NET Core, FastAPI, or Spring Boot) but adopt an RPC style.

  • Define your endpoints as POST verbs.
  • Name your URLs as actions: /api/orders/cancel.
  • Use OpenAPI to generate the client.

You are effectively building a "REST" API that violates REST principles to gain RPC benefits. This is a perfectly valid pragmatic choice that works in every language.

3. Emerging Tools (Buf Connect / ConnectRPC)

Tools like ConnectRPC are trying to bridge the gap. They allow you to write gRPC-style definitions (Protobuf) but serve them as standard JSON-over-HTTP. This gives you the type-safety and code generation of gRPC, but with the ease of use of a standard web request.

Conclusion

RESTful APIs are here to stay. They are arguably the best tool we have for Public APIs intended for unknown consumers. If you are building the next Stripe or GitHub API, use REST.

But not every API is a public product.

If you are a fullstack developer building a backend for a single frontend, stop fighting the protocol. Stop worrying about whether "Archive" is a PUT or a PATCH. Give yourself permission to just write functions.

Your code will be cleaner, your velocity will be faster, and you might just find that you don't miss those HTTP verbs at all.

Top comments (0)