Choosing the Right API Architecture - A Deep Dive into RESTful API & gRPC Protocols
A Quick Recap of RESTful APIs
A Primer on gRPC
When to Choose RESTful APIs vs gRPC
RESTful APIs vs. gRPC: gRPC internally, then the REST for all else
When building applications with APIs, choosing the right architecture for the job is key.
APIs can be defined by SOAP, GraphQL, gRPC - the list goes on and on. In fact, any interface between two pieces of code is an API. After all, APIs are application programming interfaces.
Here, we’ll examine when RESTful APIs are often the first and best choice—they’re nice, neat GET and POST endpoints with developer-friendly URLs. REST still holds about 90% of the market (our friends at Postman keep track of those stats in a great annual report you can find). But there are also times when you may consider using a different API protocol like gRPC in your application.
A Quick Recap of RESTful APIs
Let’s start with what RESTful APIs aren’t. REST isn’t a standard adopted by any official body. Instead, it is an architectural style that has come to dominate “distributed hypermedia systems,” i.e., the web. First defined in 2000 by Roy Fielding in his Ph.D. dissertation, REST (Representational State Transfer) outlined a set of constraints and principles for designing networked applications.
This architectural style has become the de facto standard for web APIs due to its simplicity, flexibility, and alignment with the web's underlying technologies. These principles include client-server architecture, statelessness, and a straightforward uniform Interface: HTTP methods like GET, POST, PUT, and DELETE.
This last principle has come to define RESTful APIs and APIs in general. Here’s a quick JavaScript example that creates REST endpoints for an underlying CRUD app to manipulate the data:
const express = require('express');const app = express();const port = 3000;app.use(express.json());// Mock databaselet users = [{ id: 1, name: 'Alice', email: 'alice@example.com' },{ id: 2, name: 'Bob', email: 'bob@example.com' }];// GET all usersapp.get('/users', (req, res) => {res.json(users);});// GET a specific userapp.get('/users/:id', (req, res) => {const user = users.find(u => u.id === parseInt(req.params.id));if (!user) return res.status(404).send('User not found');res.json(user);});// POST a new userapp.post('/users', (req, res) => {const newUser = {id: users.length + 1,name: req.body.name,email: req.body.email};users.push(newUser);res.status(201).json(newUser);});// PUT (update) a userapp.put('/users/:id', (req, res) => {const user = users.find(u => u.id === parseInt(req.params.id));if (!user) return res.status(404).send('User not found');user.name = req.body.name;user.email = req.body.email;res.json(user);});// DELETE a userapp.delete('/users/:id', (req, res) => {const userIndex = users.findIndex(u => u.id === parseInt(req.params.id));if (userIndex === -1) return res.status(404).send('User not found');users.splice(userIndex, 1);res.status(204).send();});app.listen(port, () => {console.log(`RESTful API server running on port ${port}`);});
This example includes endpoints for all CRUD operations:
- GET /users - Retrieve all users
- GET /users/:id - Retrieve a specific user
- POST /users - Create a new user
- PUT /users/:id - Update an existing user
- DELETE /users/:id - Delete a user
Each endpoint follows RESTful principles of using HTTP methods semantically (GET for retrieval, POST for creation, etc.), resource-based URLs, returning HTTP status codes, and using JSON for request and response bodies.
This API can be consumed with this client code:
async function createUser(name, email) {try {const response = await fetch('http://localhost:3000/users', {method: 'POST',headers: {'Content-Type': 'application/json',},body: JSON.stringify({ name, email }),});if (!response.ok) {throw new Error(`HTTP error! status: ${response.status}`);}const data = await response.json();console.log('New user created:', data);return data;} catch (error) {console.error('Error creating user:', error);}}// Usageasync function main() {const newUser = await createUser('Charlie', 'charlie@example.com');if (newUser) {console.log('User creation successful');}}main();
So, when most developers design APIs, they create REST APIs. Many developers default to REST when designing and implementing APIs, which is the best solution for every scenario. Based on HTTP, REST APIs can have higher latency and lower throughput than more optimized protocols, especially in systems with many small, frequent requests.
A critical “flaw” in REST for the modern web is that it doesn't natively support bi-directional streaming, which can be crucial for real-time applications or scenarios requiring constant data flow between client and server.
Finally, there is the problem that we started this section with: REST is just a style. This means REST doesn't have a standardized way to define service interfaces, which can lead to inconsistencies in API design and make it harder to generate client libraries automatically.
When dealing with these kinds of challenges–particularly in microservices, real-time communication, or performance-critical systems–you might need a different approach.
Step up gRPC.
A Primer on gRPC
gRPC has had a different trajectory. gRPC was developed by Google and initially released in August 2016. gRPC evolved Google's internal RPC (Remote Procedure Call) system, with the gRPC design goal being a modern, open-source, high-performance, efficient communication framework.
(fun fact: it seems like no one knows what the g in gRPC stands for. Maybe simply google Remote Procedure Call, but Google says not.)
First, the obvious question–what is a remote procedure call? An RPC protocol allows a program to execute a procedure on another computer as a local procedure call. In other words, it enables a program to request a service from a program located on another computer in a network without having to deal with the internal workings of that network.
So, while all RPCs can be considered APIs, not all APIs are RPCs. RPCs are a specific approach to implementing remote APIs to make remote procedure calls feel as seamless as local ones.
gRPC is a specific type of RPC that uses Protocol Buffers for interface definition and data serialization and HTTP/2 for transport. Let’s work through a (somewhat) basic example. We’ll use the Go language for this. Go is known for its excellent concurrency support and efficient execution. gRPC, being a high-performance RPC (Remote Procedure Call) framework, aligns well with Go's strengths, and the combination allows for building scalable and fast microservices. Go was also developed by Google (as were Protocol Buffers), although there are gRPC libraries for most popular languages.
First, let's define our service using Protocol Buffers. Create a file named greeter.proto:
syntax = "proto3";option go_package = "example.com/grpc-greeter";package greeter;service Greeter {rpc SayHello (HelloRequest) returns (HelloReply) {}}message HelloRequest {string name = 1;}message HelloReply {string message = 1;}
This defines a simple Greeter service with a single SayHello method. Next, we'll generate the Go code from this .proto file:
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative greeter.proto
This command uses the Protocol Buffers compiler (protoc) to generate Go source code from the .proto file. It creates two Go files: one containing the data structures for your messages and another with gRPC service definitions. This generated code provides type-safe, efficient serialization and deserialization of your data and client and server interfaces for your gRPC service.
Now, let's implement the server:
// server/main.gopackage mainimport ("context""fmt""log""net"pb "example.com/grpc-greeter""google.golang.org/grpc")type server struct {pb.UnimplementedGreeterServer}func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {return &pb.HelloReply{Message: fmt.Sprintf("Hello, %s!", in.GetName())}, nil}func main() {lis, err := net.Listen("tcp", ":50051")if err != nil {log.Fatalf("failed to listen: %v", err)}s := grpc.NewServer()pb.RegisterGreeterServer(s, &server{})log.Printf("Server listening at %v", lis.Addr())if err := s.Serve(lis); err != nil {log.Fatalf("failed to serve: %v", err)}}
And here's the client implementation:
// client/main.gopackage mainimport ("context""log""time"pb "example.com/grpc-greeter""google.golang.org/grpc""google.golang.org/grpc/credentials/insecure")func main() {conn, err := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))if err != nil {log.Fatalf("did not connect: %v", err)}defer conn.Close()c := pb.NewGreeterClient(conn)ctx, cancel := context.WithTimeout(context.Background(), time.Second)defer cancel()r, err := c.SayHello(ctx, &pb.HelloRequest{Name: "World"})if err != nil {log.Fatalf("could not greet: %v", err)}log.Printf("Greeting: %s", r.GetMessage())}
To run this example, start the server in one terminal:
go run server/main.go
Then run the client in another terminal:
go run client/main.go
You should see the greeting "Hello, World!" printed by the client.
From these two toy examples, we can already see the difference between RESTful APIs vs. gRPC.
When to Choose RESTful APIs vs gRPC
When you look at that gRPC example, there is an obvious place where REST is the winner: simplicity. The gRPC example, while powerful, requires more setup and tooling. REST APIs, on the other hand, are relatively straightforward to implement and consume.
REST is often the better choice when:
- You need broad client support, especially for web browsers
- You want a more human-readable API for debugging or exploration
- You're building public APIs that need to be easily consumed by a variety of clients
- Your API needs to be cacheable at the HTTP level
If you are building a straightforward CRUD application for the web, RESTful APIs are almost always the way to go.
But there is one area where gRPC can shine: microservices. Microservices architecture allows for building complex, scalable applications. In this context, gRPC offers several significant advantages:
- Performance. gRPC's use of binary, compact Protocol Buffers and HTTP/2 (which supports multiplexing) results in significantly lower latency and higher throughput than REST/JSON over HTTP/1.1, which REST APIs use. In a typical microservices scenario, gRPC can be 7-10 times faster than REST for small payloads, and the gap widens for larger payloads.
- Bi-directional Streaming. gRPC supports bi-directional streaming, allowing for real-time, event-driven communication between services. This enables scenarios like real-time data processing pipelines, long-lived event streaming connections, chatbots, and interactive systems.
- Strong Typing and Code Generation. gRPC's use of Protocol Buffers provides strong typing and automatic code generation, crucial in large-scale microservices architectures. This reduces errors due to type mismatches, makes for easier refactoring and maintenance, and allows automatic client/server stub generation in multiple languages.
- Deadlines and Cancellation. gRPC provides built-in support for deadlines/timeouts and cancellations, critical for managing resource usage in microservices.
- Interceptors. gRPC interceptors provide a powerful way to implement cross-cutting concerns like logging, authentication, and monitoring.
- Load Balancing. While HTTP/2's long-lived connections can complicate traditional load balancing, gRPC provides client-side load balancing out of the box.
- Service Discovery. gRPC integrates well with service discovery systems like Eureka, etcd, or Consul.
It's important to note that gRPC isn't without its challenges. Browser support for gRPC is limited, often requiring a proxy to translate between gRPC and HTTP/REST. This can add complexity to your architecture if you need to support web and native clients. Additionally, the learning curve for gRPC can be steeper than REST, especially for developers already familiar with REST APIs.
So, when should you choose RESTful APIs vs gRPC? Here are some other key scenarios:
- Real-time systems: gRPC's streaming capabilities are a natural fit for applications requiring live updates or bidirectional communication.
- Multi-language environments: If you're working in a polyglot environment where services are written in different languages, gRPC's code generation features can ensure consistency across language boundaries.
- Performance-critical systems: When every millisecond counts, such as in high-frequency trading systems or large-scale data processing pipelines, gRPC's efficiency can make a significant difference.
- IoT and edge computing: For systems dealing with numerous resource-constrained devices, gRPC's low overhead can be crucial.
RESTful APIs vs. gRPC: gRPC internally, then the REST for all else
While REST remains a solid choice for many scenarios (and it’s why it’s central to our latest product Blackbird API development platform), gRPC's performance, strong typing, and advanced features make it an excellent choice for microservices architectures. Its efficiency in handling high-volume, low-latency communication and built-in support for streaming, load balancing, and service discovery address many challenges faced in complex distributed systems.
However, it's worth noting that gRPC isn't an all-or-nothing choice, nor is it the best choice in all cases. Many organizations successfully use gRPC for internal microservices communication while still exposing only REST APIs for external clients–thereby leveraging the strengths of both approaches. Although gRPC can be a useful protocol for large and complex intra-application interfaces, RESTful APIs are the most ubiquitous go-to for any public-facing interface. RESTful APIs are here to stay, so knowing how to use both matters.
Ultimately, the choice between REST and gRPC should be based on your specific use case, performance requirements, and the ecosystem in which your application operates. By understanding the strengths and limitations of both approaches, you can make an informed decision that best suits your needs.