1. Overview
For many years, REST has been a de-facto industry standard architectural style for designing web APIs. However, GraphQL and gRPC have recently emerged in order to address some of the limitations of REST. Each of these API approaches comes with substantial benefits and some trade-offs.
In this tutorial, we’ll first look at each API design approach. Then, we’ll build a simple service using the three different approaches in Spring Boot. Next, we’ll compare them by looking at several criteria that should be considered before deciding on one.
Finally, as there’s no one-size-fits-all approach, we’ll see how different approaches can be mixed on different application layers.
2. REST
Representational State Transfer (REST) is the most commonly used API architectural style worldwide. It was defined back in 2000 by Roy Fielding.
2.1. Architectural Style
REST is not a framework or a library but an architectural style describing an interface based on the URL structure and the HTTP protocol. It describes a stateless, cacheable, convention-based architecture for client-server interaction. It uses URLs to address the appropriate resources and HTTP methods to express the action to take:
- GET is for fetching an existing resource or multiple resources
- POST is used to create a new resource
- PUT is used to update a resource or create it if it does not exist
- DELETE is used to delete a resource
- PATCH is used to partially update an existing resource
REST can be implemented in various programming languages and supports multiple data formats like JSON and XML.
2.2. Example Service
We can build a REST service in Spring by defining a controller class using the @RestController annotation. Next, we define a function corresponding to the HTTP method, for example, GET, via the @GetMapping annotation. Finally, in the annotation argument, we provide a resource path on which the method should be triggered:
@GetMapping("/rest/books")
public List<Book> books() {
return booksService.getBooks();
}
MockMvc provides support for integration testing of REST services in Spring. It encapsulates all web application beans and makes them available for testing:
this.mockMvc.perform(get("/rest/books"))
.andDo(print())
.andExpect(status().isOk())
.andExpect(content().json(expectedJson));
As they are based on HTTP, REST services can be tested in the browser or using tools like Postman or CURL:
$ curl http://localhost:8082/rest/books
2.3. Pros and Cons
The biggest advantage of REST is that it’s the most mature API architectural style in the tech world. Due to its popularity, many developers are already familiar with REST and find it easy to work with. However, due to its flexibility, REST can be interpreted differently among developers.
Given that each resource is usually located behind a unique URL, it is easy to monitor and rate-limit APIs. REST also makes caching simple by taking advantage of HTTP. By caching HTTP responses, our client and server do not need to constantly interact with each other.
REST is prone to both under-fetching and over-fetching. For example, to fetch nested entities, we may need to make multiple requests. On the other hand, in REST APIs, it’s not usually possible to fetch only a specific piece of entity data. Clients always receive all the data that the requested endpoint is configured to return.
3. GraphQL
GraphQL is an open-source query language for APIs developed by Facebook.
3.1. Architectural Style
GraphQL provides a query language for developing APIs with a framework for fulfilling those queries. It doesn’t rely on the HTTP methods for manipulating data and mostly uses only POST. In contrast, GraphQL makes use of queries, mutations, and subscriptions:
- Queries are used to request data from the server
- Mutations are used to modify the data on the server
- Subscriptions are used to get live updates when data changes
GraphQL is client-driven, as it enables its clients to define exactly what data they need for a particular use case. The requested data is then retrieved from the server in a single roundtrip.
3.2. Example Service
In GraphQL, data is represented with schemas that define objects, their fields, and types. Therefore, we will start by defining a GraphQL schema for our example service:
type Author {
firstName: String!
lastName: String!
}
type Book {
title: String!
year: Int!
author: Author!
}
type Query {
books: [Book]
}
We can build GraphQL services in Spring similarly to REST services by using the @RestController class annotation. Next, we annotate our function with @QueryMapping to mark it as a GraphQL data-fetching component:
@QueryMapping
public List<Book> books() {
return booksService.getBooks();
}
HttpGraphQlTester provides support for integration testing of GraphQL services in Spring. It encapsulates all web application beans and makes them available for testing:
this.graphQlTester.document(document)
.execute()
.path("books")
.matchesJson(expectedJson);
GraphQL services can be tested with tools like Postman or CURL. However, they require a query to be specified in the POST body:
$ curl -X POST -H "Content-Type: application/json" -d "{\"query\":\"query{books{title}}\"}" http://localhost:8082/graphql
3.3. Pros and Cons
GraphQL is highly flexible towards its clients, as it allows fetching and delivering only the requested data. As no unnecessary data is sent over the network, GraphQL can lead to better performance.
It uses a more strict specification compared to the ambiguity of REST. In addition, GraphQL provides detailed error descriptions for debugging purposes and autogenerates documentation on API changes.
Since each query can be different, GraphQL breaks intermediate proxy caching, making caching implementations more difficult. Also, since a GraphQL query could potentially execute a large and complex server-side operation, queries are often limited by complexity to avoid overloading the server.
4. gRPC
RPC stands for remote procedural call, and gRPC is a high-performance, open-source RPC framework created by Google.
4.1. Architectural Style
The gRPC framework is based on a client-server model of remote procedure calls. A client application can directly call methods on a server application as if it was a local object. It is a contract-based strict approach where both the client and the server need access to the same schema definition.
In gRPC, a DSL called protocol buffer language defines requests and response types. The protocol buffer compiler then generates the server and client code artifacts. We can extend the generated server code with custom business logic and provide the response data.
The framework supports several types of client-server interactions:
- Traditional request-response interactions
- Server streaming, where one request from the client may yield multiple responses
- Client streaming, where multiple requests from the client result in a single response
Clients and servers communicate via HTTP/2 using a compact binary format which makes encoding and decoding of gRPC messages very efficient.
4.2. Example Service
Similar to GraphQL, we start by defining a schema that defines the service, requests, and responses, including their fields and types:
message BooksRequest {}
message AuthorProto {
string firstName = 1;
string lastName = 2;
}
message BookProto {
string title = 1;
AuthorProto author = 2;
int32 year = 3;
}
message BooksResponse {
repeated BookProto book = 1;
}
service BooksService {
rpc books(BooksRequest) returns (BooksResponse);
}
Then, we need to pass our protocol buffer file to the protocol buffer compiler in order to generate the required code. We can choose to perform this action manually using one of the precompiled binaries or make it a part of the build process using the protobuf-maven-plugin:
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>${protobuf-plugin.version}</version>
<configuration>
<protocArtifact>com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}</pluginArtifact>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
Now, we can extend the generated BooksServiceImplBase class, annotate it with @GrpcService annotation and override the books method:
@Override
public void books(BooksRequest request, StreamObserver<BooksResponse> responseObserver) {
List<Book> books = booksService.getBooks();
BooksResponse.Builder responseBuilder = BooksResponse.newBuilder();
books.forEach(book -> responseBuilder.addBook(GrpcBooksMapper.mapBookToProto(book)));
responseObserver.onNext(responseBuilder.build());
responseObserver.onCompleted();
}
Integration testing of gRPC service in Spring is possible but not yet as mature as REST and GraphQL:
BooksRequest request = BooksRequest.newBuilder().build();
BooksResponse response = booksServiceGrpc.books(request);
List<Book> books = response.getBookList().stream()
.map(GrpcBooksMapper::mapProtoToBook)
.collect(Collectors.toList());
JSONAssert.assertEquals(objectMapper.writeValueAsString(books), expectedJson, true);
To make this integration test work, we’ll need to annotate our test class with the following:
- @SpringBootTest to configure the client to connect to the so-called gRPC “in process” testing server
- @SpringJUnitConfig to prepare and provide the application beans
- @DirtiesContext to ensure that the server is properly shut down after each test
Postman has recently added support for testing gRPC services. Similar to CURL, a command-line tool called grpcurl enables us to interact with gRPC servers:
$ grpcurl --plaintext localhost:9090 com.baeldung.chooseapi.BooksService/books
This tool uses JSON encoding to make protocol buffers encoding more human-friendly for testing purposes.
4.3. Pros and Cons
The biggest advantage of gRPC is performance, which is enabled by a compact data format, fast message encoding and decoding, and the usage of HTTP/2. Also, its code generation feature supports multiple programming languages and helps us save some time writing boilerplate code.
By requiring HTTP 2 and TLS/SSL, gRPC provides better security defaults and built-in support for streaming. The language-agnostic definition of the interface contract enables the communication between services written in different programming languages.
However, at the moment, gRPC is much less popular than REST in the developer community. Its data format is unreadable for humans, so additional tools are required to analyze payloads and perform debugging. Also, HTTP/2 is only supported via TLS in the recent versions of modern browsers.
5. Which API to Choose
Now that we are familiar with all three API design approaches, let’s look at several criteria we should consider before deciding on one.
5.1. Data Format
REST is the most flexible approach in terms of request and response data formats. We can implement REST services to support one or multiple data formats like JSON and XML.
On the other hand, GraphQL defines its own query language that needs to be used when requesting data. GraphQL services respond in JSON format. Although it is possible to convert the response into another format, it’s uncommon and could affect performance.
The gRPC framework uses protocol buffers, a custom binary format. It is unreadable to humans, but it is also one of the main reasons which make gRPC so performant. Although supported in several programming languages, the format cannot be customized.
5.2. Data Fetch
GraphQL is the most efficient API approach for fetching data from the server. As it allows clients to select which data to fetch, there is usually no extra data sent over the network.
Both REST and gRPC do not support such advanced client querying. Thus, extra data might be returned by the server unless new endpoints or filters are developed and deployed on the server.
5.3. Browser Support
REST and GraphQL APIs are supported in all modern browsers. Typically, JavaScript client code is used to send over HTTP requests from the browser to the server APIs.
Browser support doesn’t come out of the box for gRPC APIs. However, an extension of gRPC for the web is available. It is based on HTTP 1.1., but doesn’t provide all gRPC features. Similar to a Java client, gRPC for the web requires the browser client code to generate a gRPC client from a protocol buffer schema.
5.4. Code Generation
GraphQL requires additional libraries to be added to a core framework like Spring. Those libraries add support for the processing of GraphQL schemas, annotation-based programming, and server handling of GraphQL requests. Code generation from a GraphQL schema is possible but not required. Any custom POJO matching a GraphQL type defined in the schema can be used.
The gRPC framework also requires additional libraries to be added to a core framework, as well as a mandatory code generation step. The protocol buffer compiler generates the server and client boilerplate code that we can then extend. In case we use custom POJOs, they will need to be mapped to the autogenerated protocol buffer types.
REST is an architectural style that can be implemented using any programming language and various HTTP libraries. It uses no predefined schema and does not require any code generation. That said, making use of Swagger or OpenAPI allows us to define a schema and generate code if we wish.
5.5. Response Time
Thanks to its optimized binary format, gRPC has significantly faster response times compared to REST and GraphQL. In addition, load balancing can be used in all three approaches to evenly distribute client requests across multiple servers.
However, in addition, gRPC makes use of HTTP 2.0 by default, which makes latency in gRPC lower than in REST and GraphQL APIs. With HTTP 2.0, several clients can send multiple requests simultaneously without establishing a new TCP connection. Most performance tests report that gRPC is roughly 5 to even 10 times faster than REST.
5.6. Caching
Caching requests and responses with REST is simple and mature as it allows for caching of data on the HTTP level. Each GET request exposes the application resources, which are easily cacheable by the browser, proxy servers, or CDNs.
Since GraphQL uses the POST method by default and each query can be different, it makes caching implementation more difficult. This is especially true when the client and the server are geographically distant from each other. A possible workaround for this issue is to make queries via GET, and use persisted queries that are pre-computed and stored on the server. Some GraphQL middleware services also offer caching.
At the moment, caching requests and responses are not supported in gRPC by default. However, it is possible to implement a custom middleware layer that will cache responses.
5.7. Intended Usage
REST, a good fit for domains, can be easily described as a set of resources as opposed to actions. Making use of HTTP methods enables standard CRUD operations on those resources. By relying on HTTP semantics, it’s intuitive to its callers, making it a good fit for public-facing interfaces. Good caching support for REST makes it suitable for APIs with stable usage patterns and geographically distributed users.
GraphQL is a good fit for public APIs where multiple clients require different data sets. Therefore, GraphQL clients can specify the exact data they want through a standardized query language. It is also a good choice for APIs that aggregate data from multiple sources and then serve it to multiple clients.
The gRPC framework is a good fit when developing internal APIs with frequent interaction between microservices. It is commonly used for collecting data from low-level agents like different IoT devices. However, its limited browser support makes it difficult to use in customer-facing web applications.
6. Mix and Match
Each of the three API architectural styles has its benefits. However, there’s no one-size-fits-all approach, and which approach we choose will depend on our use case.
We don’t have to make a single choice every time. We can also mix and match different styles in our solution architecture:
In the example architecture diagram above, we demonstrated how different API styles might be applied in different application layers.
7. Conclusion
In this article, we explored three popular architectural styles for designing web APIs: REST, GraphQL, and gRPC. We looked at each different style use cases and described its benefits and trade-offs.
We explored how to build a simple service using all three different approaches in Spring Boot. Also, we compared them by looking at several criteria that should be considered before deciding on an approach. Finally, as there’s no one-size-fits-all approach, we saw how we could mix and match different approaches in different application layers.
As always, the complete source code is available over on GitHub.