The Death of JSON in Backend: Why I migrated my entire stack to gRPC and Protobuf
I’ll start this post with a confession that might earn me some enemies: I hate JSON.
It’s not an irrational hate, the kind that appears out of nowhere. It’s a hate built, brick by brick, over years of debugging malformed payloads, fields that should have been numbers but arrived as strings, and that classic null where you expected an empty array. JSON is the digital equivalent of a phone conversation with your grandmother: you think you understood what she said, but when you get there, the carrot cake didn’t have that expected chocolate frosting (Brazilians will understand).

For years, I accepted this torture as part of the social contract of being a backend developer. “It’s the standard,” they said. “Everyone uses it,” they repeated. And I, like a good little sheep, kept serializing and deserializing text as if we were in 2005 and broadband was infinite.
Until I had enough.
The Problem: Text is Expensive
Let’s do a mental exercise. Imagine you need to send the number 42 from your Go backend to your Flutter app.
In JSON, this becomes:
{"answer": 42}
That’s 14 bytes of text. Seems like very little, right? Now imagine you are sending a list of 10,000 financial transactions, each with 15 fields. Suddenly, that “human-readable text” turns into megabytes of waste.
JSON parsing is a CPU sacrifice ritual. Your server needs to:
- Convert the Go structure to text.
- Serialize that text into UTF-8.
- Send it over the network.
- The client receives the text.
- The client parses the text back into a structure.
- The client prays that the types are correct.
Each of these steps consumes processing cycles. And as I said in my About Me post: bits consume energy. Wasting them is morally offensive.
The Solution: Protobuf and the Beauty of Binary
Protocol Buffers (Protobuf) is the serialization format created by Google to solve exactly this problem. Instead of text, you define a contract (a .proto file), and the compiler generates native code for any language.
That same {"answer": 42} in Protobuf? 2 bytes. It’s not magic, it’s math. Binary is simply more efficient than text.
But the bandwidth savings are just the icing on the cake. The real gift is strong typing.
syntax = "proto3";
message Transaction {
string id = 1;
int64 amount_cents = 2;
string currency = 3;
int64 timestamp = 4;
TransactionType type = 5;
}
enum TransactionType {
UNKNOWN = 0;
CREDIT = 1;
DEBIT = 2;
}
With this file, I generate code for Go, Dart (Flutter), and TypeScript (SolidJS) with a single command. If I change the type of amount_cents from int64 to string, the compiler screams at me before I deploy. There is no longer that nightmare of “it worked in Postman but broke in the app.”
gRPC: The Perfect Marriage with HTTP/2
Protobuf is the format. gRPC is the communication protocol that uses this format. And this is where things get interesting for those who care about mobile performance.
gRPC runs over HTTP/2 by default (and already has experimental support for HTTP/3). This means:
Multiplexing: Multiple calls on the same TCP connection. No opening and closing connections for every request.
Header compression: HTTP/2 uses HPACK to compress repetitive headers. In REST, you send
Content-Type: application/jsonin every request. In gRPC, this is negotiated once.Bidirectional streaming: Want a real-time chat? WebSockets? Forget it. gRPC does this natively.
For a Flutter mobile app consuming data from a Go backend, the difference is brutal. Fewer bytes transferred means less battery consumed, shorter loading times, and fewer chances of the user giving up and going to watch TikTok.
The Stack: Go + Flutter + SolidJS
My current architecture works like this:
- Backend in Go: I use
protoc-gen-goandprotoc-gen-go-grpcto generate the stubs. The server is ridiculously fast because Go is already fast, and now it doesn’t need to waste time parsing JSON anymore. - Mobile in Flutter: I use Dart’s
grpcpackage. The code generated by Protobuf is type-safe. If the backend changes a field, the app won’t compile until I update it. That is security. - Web in SolidJS: Here comes the only catch. Browsers don’t speak gRPC natively (yet). The solution is to use gRPC-Web, a proxy that translates HTTP/1.1 calls to gRPC. It’s not perfect, but it’s still better than pure REST.
The .proto file is the single source of truth. I edit it once, run the compiler, and all platforms are synchronized. No more playing that game of maintaining three different versions of DTOs in three different languages.
The Elephant in the Room: Debugging
I’ll be honest: debugging gRPC is harder than debugging REST. You can’t simply open the browser and look at the payload in the Network tab. Binary isn’t human-readable.
For this, I use grpcurl (the curl of the gRPC world) and Kreya (an alternative to Postman for gRPC). I also configure structured logging on the server to capture deserialized requests.
It’s an initial setup cost. But once you get used to it, you don’t go back.
When NOT to Use gRPC
Like everything in technology, it’s not all roses. gRPC is overkill for:
Public APIs: If you are creating an API for third parties to consume, JSON/REST is still the standard. Nobody wants to learn your Protobuf schema just to integrate. Don’t be annoying.
Small projects: If your backend is a simple CRUD with 5 endpoints, the additional complexity isn’t worth it.
SEO and crawlers: Google bots don’t understand gRPC. If you need your content to be indexed, REST + JSON is the way.
In my case, I’m focused on internal systems where I control all the ends. I don’t have to worry about external compatibility.
Conclusion: JSON Isn’t Dead, But It Should Be on Leave
Look, JSON won’t disappear. It is the English of the API world: it’s not the best language (I need to write a post about how ugly I think English is), but everyone speaks it. For human consumption, quick debugging, and prototyping, it still has its place.
TODO: Write a post about how ugly I think English is
But if you are building high-performance systems, if you care about your user’s cell phone battery, if you are tired of discovering typing bugs in production… maybe it’s time to consider the binary alternative.
I migrated. My server thanks me. My app thanks me. My mental sanity? Well, that was already compromised since the 7-1 the Brazilian team took in the 2014 World Cup.
In the future, I will write a comparative post. Nothing is more technical than shutting up and doing it, right?
Before I forget:
TODO: Write a post comparing REST x gRPC in practice
That’s it, I’m out, bye!
