With REST you can just use a normal HTTP caching proxy for all the GETs under certain paths, off the shelf.
Using a hybrid (JSON-RPC for writes and authenticated reads, REST for global reads) would have saved me a lot of time spent building and maintaining a JSON-RPC caching layer.
There is benefit to a GET/POST split, and JSON-RPC forces even simple unauthenticated reads into a POST.
The other issue with JSON-RPC is, well, json. It's not the worst, but it's also not the best. json has no great canonicalization so if you want to do signed requests or responses you're going to end up putting a string of json (inner) into an key's value at some point. Doing that in protobuf seems less gross to me.
That is explicity cache the information in your JavaScript frontend or have your backend explicitly cache. In that way it is easy to understand and your can also control what circumstances a cache is invalidated.
I'm not talking about browser caching, I'm talking about the reverse proxy that fronts your ("backend") service to the internet. High traffic global/unauthenticated reads, especially those that never change, should get cached by the frontend (of the "backend", not the SPA) reverse proxy and not tie up appservers. (In our case, app servers were extremely fat, slow, and ridiculously slow to scale up.)
Caching upstream on (vastly cheaper) instances permitted a huge cost savings for the same requests/sec.
When you're many devs, many APIs, many resources, it really pays to have a consistent, well-defined way to do this. GraphQL is very close to what you've described, with some more defined standards. GRPC is close as well, except the serialisation format isn't JSON, it's something more optimised.
As a team grows these sorts of standards emerge from the first-pass versions anyway. These just happen to be pre-defined ones that work well with other things that you could choose to use if you wanted to.
For example:
POST /api/listPosts HTTP/1.1
{ userId: "banana", fromDate: 2342342342, toDate: 2343242 }
Reponse: HTTP/1.1 200 OK
[ { id: 32432, title: "Happy banana", userId: "banana" }, ... ]
Or in case of an error: HTTP/1.1 500 Internal Server Error
{ type: "class name of exception raised server side", message: "Out of bananas" }
The types can be specified with TypeScript needed.GET /api/module/method?param1¶m2
or
POST /api/module/method Body: Json{ param1, param2 }