REST vs. GraphQL vs. gRPC vs. WebSocket
Architecting a distributed system requires a good understanding of available technologies. Each technology can be used as a toolbox tool to solve real-world problems. In this article, we will discuss the most popular modern standards, frameworks, and protocols and the different trade-offs between each of them.
Overview of REST (REpresentational State Transfer)
Representational state transfer (REST) is a software architectural style that describes the architecture of the Web. It was derived from the following constraints:
Uniform Interface
- Identification of resources: The interface must uniquely identify each resource involved in the client and server interaction.
- Manipulation of resources through representations: The resources should have uniform representations in the server response. API consumers should use these representations to modify the resource state on the server.
- Self-descriptive messages: Each resource representation should carry enough information to describe how to process the message. It should also provide information on the additional actions that the client can perform on the resource.
- As the engine of the application states, the client should have only the initial URI of the application. The client application should dynamically drive all other resources and interactions through hyperlinks.
Independence
Because of the separation between client and server, the REST protocol allows for autonomous development across several project sections. Furthermore, the REST API is adaptable to operational syntax and platforms. This allows testing in a variety of contexts throughout development.
Scalability
- Statelessness mandates that each request from the client to the server must contain all of the information necessary to understand and complete the request.
- The server cannot take advantage of any previously stored context information on the server.
Cacheable
The cacheable constraint requires that a response implicitly or explicitly label itself as cacheable or non-cacheable.
Layered system
Every REST-enabled component has no access to components other than the one with whom it communicates. This means that a client who connects to an intermediary component does not know with whom that component will engage later. This encourages developers to design separate components that are easy to upgrade.
When to use REST:
- Limited bandwidth and resources
- Ease of coding is a requirement.
- Cachebility
- Generic API consumed by many clients
REST in practice
Let's imagine we have an online store, and we want to retrieve a list of categories for the store and the name and ID of each category.
We will send a request from our client similar to the one below.
GET /categories
and the response we will get from that endpoint will look like this:
HTTP/1.1 200 OK
Content-Type: application/json
[
{
"id": 1,
"name": "Electronics"
},
{
"id": 2,
"name": "Clothing"
},
{
"id": 3,
"name": "Food"
}
]
Now let's say we want to get products for the "Electronics" category.
We will send the following request to our REST API:
GET /categories/1/products
And the response we will receive from that endpoint will be the following:
HTTP/1.1 200 OK
Content-Type: application/json
[
{
"id": 1,
"name": "iPhone X",
"price": 999.99,
"category_id": 1
},
{
"id": 2,
"name": "MacBook Pro",
"price": 1499.99,
"category_id": 1
},
{
"id": 3,
"name": "iPad Pro",
"price": 799.99,
"category_id": 1
}
]
Introduction to GraphQL
In essence, "GraphQL" is a query language and a server-side runtime (typically served over HTTP). GraphQL is the modern approach to client-server communication and aims to improve how developers build web applications. Furthermore, GraphQL is a language for querying data. Unlike most query languages (such as SQL), you don’t use GraphQL to query a particular type of data store (such as a MySQL database). Instead, you use GraphQL to query data from various sources over HTTP.
How it differs from REST
REST and GraphQL are both APIs used in web development.
REST relies on set endpoints to retrieve data, which may result in either an excessive or insufficient obtaining of data. On the other hand, GraphQL offers a more flexible and effective data fetching method by enabling clients to request only the data they require in a single query. The particular requirements of the application will determine which option is best.
It’s also worth noting that in REST, the structure of the request object is defined on the server. In GraphQL, you define the object on the client.
When to use GraphQL
GraphQL is an excellent solution to a unique problem around building and consuming APIs. When used as designed, it can be an ideal tool for the use cases described below.
Using multiple data sources
In GraphQL, you can combine data from numerous backends or services into a single query, enabling clients to quickly retrieve data from multiple sources. This feature improves efficiency and flexibility when retrieving data from various repositories using a GraphQL API.
Alleviating bandwidth concerns
GraphQL gives you the tools to minimize over-fetching and wasteful data transmission and optimize data transfer by enabling clients to request only the precise data they need. This improves GraphQL API performance and solves issues with bandwidth efficiency.
Rapid prototyping
Quick and iterative development processes are enabled by the ability to request and receive precisely the needed data. This allows developers to efficiently experiment with and refine queries without the constraints of fixed endpoints, speeding up the prototyping phase in application development. This flexibility facilitates faster exploration and implementation of data structures during the early stages of building GraphQL APIs.
Flexible API that different clients will use to make many unique requests, in which clients create their own unique queries and get only the specific data they need quickly.
GraphQL in practice
Let's take the above REST example and turn it into GraphQL. Now, let's get the list of categories for our store. The query will look like this:
// Query for retrieving a list of categories
query GetCategories {
categories {
name
id
}
}
The response to the above query would be the following:
// An example response to a query requesting a list of categories.
{
"data": {
"categories": [
"name": "Electronics", "id": "1",
"name": "Clothing", "id": "2",
"name": "Books", "id": "3"
]
}
}
Now let's get our products for the category "Electronics." We will do the following query:
query GetProductsForCategory($categoryId: ID!) {
category(id: $categoryId) {
name
products {
name
price
}
}
}
The response will be the following:
{
"data": {
"category": {
"name": "Electronics",
"products": [
{
"name": "iPhone",
"price": "$999.99"
},
{
"name": "Macbook Pro",
"price": "$1,299.99"
},
{
"name": "Smartwatch",
"price": "$349.99"
}
]
}
}
}
This is similar to how REST works, except that we send queries to our GraphQL instead of raw HTTP requests.
REST can do much of what GraphQL does.
It’s essential to remember that GraphQL is an alternative to REST for developing APIs, not a replacement.
The main benefit of using GraphQL is the ability to send a query that specifies only the information you need and receive precisely that. However, you can achieve this same effect using REST by passing the name of the fields you want to use in the URL and then implementing the parsing and returning logic yourself:
GET/categories/1/products?fields=title,price
So, the most suitable case for GraphQL is when you need a proxy gateway to gather data from multiple places, like in microservice architecture or in cases where you have a lot of unnecessary data and you only want to take what you need, in which case GraphQL is the right tool for the job.
Introduction to WebSocket
As per the conventional definition, WebSocket is a duplex protocol used mainly in the client-server communication channel. It’s bidirectional in nature, which means communication happens to and fro between client and server.
The connection, developed using WebSocket, lasts as long as any participating party lays it off. Once one party breaks the connection, the second party won’t be able to communicate, as the connection fails automatically at its front.
WebSocket needs support from HTTP to initiate the connection. Speaking of WebSocket’s utility, it’s the spine for modern web application development when seamless streaming of data and assorted unsynchronized traffic is concerned.
When to use WebSocket
- Real-time applications
- Chat application
- Dashboards that need a live data feed into their charts
- Gamification
- Synchronization with external hardware for real-time data display in IoT apps
- Multiplayer games
Limitations of WebSocket
The biggest downside to using WebSocket is the weight of the protocol and the hardware requirements it brings with it. WebSocket requires a TCP implementation, which may or may not be a problem, but it also requires an HTTP implementation for the initial connection setup.
WebSocket in practice
Let's now take our store as an example and turn it into a web socket messaging exchange to display products. We will do the example in JavaScript, but the concept applies pretty much to every language.
const socket = new WebSocket('ws://localhost:8080'); // connecting to the socket server from client
// Connection opened
socket.addEventListener('open', (event) => {
socket.send(JSON.stringify({ type: 'getCategories' }));
socket.send(JSON.stringify({ type: 'getProducts', categoryId: 1 }));
});
// Listen for messages
socket.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
switch (data.type) {
case 'categories':
displayCategories(data.categories);
break;
case 'products':
displayProducts(data.products);
break;
}
});
function displayCategories(categories) {
console.log('Categories:', categories);
}
function displayProducts(products) {
console.log('Products:', products);
}
The following code will send the message "getCategories" to the server.
socket.send(JSON.stringify({ type: 'getCategories' }));
Therefore, the server will send back a message of type "categories," which will contain the data with a category list. Therefore, the "displayCategories" function will be invoked, which will print out the JSON of the categories to the console. The next line of code will send the message "getProducts" to the server with the parameter "categoryId" equal to 1.
socket.send(JSON.stringify({ type: 'getProducts', categoryId: 1 }));
Therefore, the server will send back a message of type "products," which will contain the data with a product list for category ID 1. Therefore, the function "displayProducts" will be invoked, resulting in the printing of a JSON of the product list in the console.
Introduction to gRPC
gRPC is a robust open-source RPC (Remote Procedure Call) framework used to build scalable and fast APIs. It allows the client and server applications to communicate transparently and develop connected systems. Many leading tech firms, such as Google, Netflix, Square, IBM, Cisco, and Dropbox, have adopted gRPC. This framework relies on HTTP/2, protocol buffers, and other modern technology stacks to ensure maximum API security, performance, and scalability.
When to use gRPC
So now you know that gRPC is a request-response protocol for streaming RPC that uses protocol buffers to define interfaces and messages. You might wonder what the difference between protocol buffers and WebSocket is. Protocol Buffers (Protobuf) and WebSocket serve distinct roles in communication. Protobuf is a data serialization format designed for efficiency and structure, often used in distributed systems. At the same time, WebSocket provides a bidirectional communication protocol, establishing persistent connections for real-time applications like chat or gaming. Each technology addresses different data communication aspects, with Protobuf focusing on serialization and WebSocket facilitating low-latency, persistent connections.
But where and why would you use gRPC, and what programming languages can be used with it?
The "where" is pretty straightforward: you can leverage gRPC almost anywhere you have two computers communicating over a network.
- Microservices: gRPC shines as a way to connect servers in service-oriented environments. One of the original problems its predecessor, Stubby, aimed to solve was wiring together microservices. It is well-suited for a wide variety of arenas, from medium and large enterprise systems all the way to "web-scale" eCommerce and SaaS offerings.
- Client-server applications: gRPC works just as well in client-server applications, where the client application runs on a desktop or mobile device. It uses HTTP/2, which improves on HTTP 1.1 in both latency and network utilization. This means faster response times and longer battery life.
- Integrations and APIs: gRPC is also a way to offer APIs over the Internet for integrating applications with services from third-party providers. For example, many of Google's Cloud APIs are exposed via gRPC. This is an alternative to REST+JSON, but it does not have to be mutually exclusive. There are tools for easily exposing gRPC services over REST and JSON, such as `gRPC-gateway`.
- Browser-based web applications: The last big area superficially seems like a poor fit. JavaScript code running in a browser cannot directly utilize gRPC because gRPC has a strict requirement for HTTP/2, but browser XHRs do not. However, as mentioned above, there are tools for exposing your gRPC APIs as REST+JSON, where browser clients can easily consume them.
gRPC in practice
Now that we know that using gRPC in a browser is a bad choice, we will imagine the following scenario:
Where does an API Gateway in C# communicate with a store microservice somewhere?
Let's first describe our protobuf schema.
// store.proto
syntax = "proto3";
service Store {
rpc GetCategories (google.protobuf.Empty) returns (Categories);
rpc GetProducts (CategoryId) returns (Products);
}
message Categories {
repeated Category categories = 1;
}
message Category {
int32 id = 1;
string name = 2;
}
message Products {
repeated Product products = 1;
}
message Product {
int32 id = 1;
string name = 2;
}
message CategoryId {
int32 id = 1;
}
Okay, now that we have our proto buf schema, let's write the code for our client application.
// StoreClient.cs using Grpc.Core; using System;
namespace StoreClient
{
class Program
{
static void Main(string[] args)
{
var channel = new Channel("localhost:50051", ChannelCredentials.Insecure);
var client = new Store.StoreClient(channel);
// Request the list of categories
var categories = client.GetCategories(new Google.Protobuf.WellKnownTypes.Empty());
Console.WriteLine("Categories:");
foreach (var category in categories.Categories)
{
Console.WriteLine($" ID: {category.Id} Name: {category.Name}");
}
// Request the list of products for the first category
var categoryId = categories.Categories[0]. Id;
var products = client.GetProducts(new CategoryId { Id = categoryId });
Console.WriteLine("Products:");
foreach (var product in products.Products)
{
Console.WriteLine($" ID: {product.Id} Name: {product.Name}");
}
channel.ShutdownAsync(). Wait();
Console. WriteLine("Press any key to exit...");
Console.ReadKey();
}
}
}
In this example, the store.proto file contains the gRPC service definition. The C# client uses the generated code from the store.proto file and the gRPC library to create a gRPC connection to the server and call the GetCategories and GetProducts methods. The client displays the returned list of categories and products.
As you might notice, gRPC works in the following way: you send a message, and you receive a message in the scenario:
rpc GetProducts (CategoryId) returns (Products);
We need to define the message for the category ID. We can't use a primitive type like Int32 to tell that we send category ID, which might lead to much verbosity in the code, but it will have a clearly defined schema of what comes in and what goes out.
Conclusion
At the end of the day, the best API technology for your specific project will depend on what you are trying to achieve. If you need a generic API that many clients will use, then REST might be your best option. If you need a flexible API that different clients will use to make many unique requests, then let the clients create their unique queries and get only the specific data they need quickly, and you can achieve this with GraphQL. If you want to create fast, seamless communication between internal services, then gRPC is your best option. WebSocket is the right choice if you want real-time bidirectional communication between many clients and a server.
As we've seen, all four have their trade-offs, and you can get optimal results by combining technologies.