Let’s start by setting up some ground rules for this article.
For one, we’ll consider how to properly use each API, with all available features (for example, federations and data loaders for GraphQL, good endpoint design for REST, etc.). And two, we won’t look at one API requiring a mindset shift from another API as a disadvantage; this happens naturally when considering multiple technologies that vary in some way.
Now to the first question at hand. You may ask: Is the traditional REST API dying?
I’d argue that no, not really. It’s still a viable option. At STRV, we have experience with both APIs — and I will show you a comparison that can be helpful when picking one of these APIs. The compared criteria will include endpoints, legacy systems, microservices, codegen, file uploads, data fetching and requests.
Why these criteria? Mostly because they showcase the greatest differences between the two APIs. And after discussing with several engineers, they’re also most often listed as reasons to like or dislike either REST or GraphQL.
Endpoints
Why GraphQL?
There’s only one endpoint to care about. Frontend can be more independent as long as it’s able to query objects defined in the schema; everything else is handled within the request body — like what should be fetched, created or modified. We’ll get into more details about requests in the Requests section.
Why not GraphQL? There is one almighty endpoint.
Still, GraphQL is slightly better overall, as long as it’s done schema-first. Saves time typically spent on communication between backend and frontend by eliminating common conversations like, “This endpoint was added, you have to call it after these three endpoints, but with the POST method first…” But of course, frontend does need to be at least pinged about a new version of schema being available.
Why REST?
Each endpoint is responsible only for a small part of the functionality, and requirements for the request body per every endpoint are clearly defined.
Why not REST? Frontend becomes more coupled with backend, so the frontend requires more frequent updates about the endpoints, if they should be called in some specific order and with which methods.
Legacy Systems
For systems that already have a reasonable API, we’d recommend sticking to that one. With legacy systems, this will most likely be REST API — which is fine, since there’s nothing inherently bad about it and no reason to make it obsolete.
However, in rare cases when the old API is really bad (large costs to add new features, poor API design, unintuitive paths, etc.), it can make sense to migrate to a new API — which can be either a new REST API or GraphQL. We’ll go into more detail a bit later.
Microservices & Service-oriented Architecture
Why GraphQL?
In cases where multiple services need to be called from the frontend to fully fetch one object, it makes more sense to use GraphQL with federation; it’s a widely-used mechanism. Each service defines a subgraph of a supergraph which is in the router service. This approach hides the inner architecture from the outside world and allows the exposure of only one schema for a supergraph.
Why not GraphQL? GraphQL is mostly suited for client-to-service communication and doesn’t offer a simple way to hide some schemas (similarly to internal endpoints for REST). However, this can be solved by either:
- Creating a separate schema for service-to-service communication, or
- Using a different technology tailored to service-to-service communication, such as gRPC.
Why REST?
Internal endpoints for service-to-service communication can easily be added.
Why not REST? Even if the gateway is used, it’s possible to fetch only data from one service in one request. The gateway only redirects requests to a particular service.
Schema & Code Generation
Why GraphQL?
A lot of GraphQL code is usually meant to be generated from the schema, and generators tend to be better supported and maintained compared to REST.
Why not GraphQL? Since a lot of GraphQL code is generated, we have to rely on the generator and it’s often hard to modify the purely generated logic — which gets over-generated with schema changes anyway. Implementing GraphQL without any generators calls for much more effort compared to REST.
Why REST?
It’s simpler, plus it’s common practice to write REST code without generators. Engineers don’t have to necessarily rely on generators and can write the code themselves, with just a bit of added effort.
Why not REST? If you want to follow the schema-first approach together with code generation, the generators may not be as maintained as in GraphQL. We’ve seen that many tools adapt very slowly to new, swaggier versions. In 2022, we weren’t able to find a reasonable tool to simply compose OpenAPI 3 from several yaml files; we had to modify existing tools that worked only for OpenAPI 2 so they would support OpenAPI 3. That’s a bummer, given that OpenAPI 3 was released in 2017.
Requests & Responses
Why GraphQL?
You don’t need to think about which methods or status codes will be used. Every request has a POST method, every response has status code 200 and, in the body, you’ll find either the wanted response or an error message.
Why not GraphQL? We have to rely only on the body of requests and responses.
Here’s an example of GraphQL’s request and response (notice that we’re able to fetch only the fields we request):
Calling: POST localhost:8000/api/graphql
Request body:
{
user {
email
}
}
Response body:
{
"email": "jiri.siroky@strv.com"
}
Why REST?
Each request has its appropriate HTTP method, allowing you to better expect what the request will do. Each response is enriched with an appropriate HTTP response code, which lets you understand if the request succeeded or how it failed. All of this is known before diving into the request or response body.
Why not REST? There are no strict rules that enforce using exact methods or status codes. Engineers should follow best practices and conventions to ensure best use.
Here’s an example of REST’s request and response (notice that we’re unable to specify that we only want to fetch email — which we can do with GraphQL; here, we always fetch the whole user):
Calling: GET localhost:8000/api/v1/user/12b643df-5b1d-4015-b38f-5732b4c0cfd0
Response body:
{
"email": "jiri.siroky@strv.com",
"first_name": "Jiri",
"last_name": "Siroky",
"phone": "111222333",
"phone_extension": "+420",
"job_role": "Backend Engineer"
}
File Upload
Why REST?
For resource-driven systems with many uploads — where files are to be directly processed by the system — it’s better to use REST. There can be endpoints where each has a single responsibility for the upload and where the request body contains only the file.
Why not GraphQL? If uploading to some cloud storage is sufficient, then GraphQL is fine, too. In the response, we have to provide a URL for upload into that external storage (like the S3 bucket). Otherwise, file upload in GraphQL isn’t great — since the file is a part of a json just like that.
Data Fetching
Why REST?
Can be more suited for download for similar reasons as for uploading.
Why not REST? To fetch more complex data tied to several resources, you would usually need to make multiple requests and compose the final object yourself as a consumer. This also increases network traffic.
Why GraphQL?
In this scenario, it rocks. The consumer specifies what they want to fetch and if it’s fetchable and they have permissions, they fetch it. Everything in one request saves quite a lot of effort on the consumer side here. If we count on the fact that backend engineers understand and use data loaders, then GraphQL is a clear winner here. There is no reason to not go for GraphQL — other than personal preference.
API Migration
Sometimes, one may consider migrating the API — usually from an older technology to a newer one, although nothing forbids doing it the other way around.
Migration is especially applicable with legacy applications, where the current API design limits extendability while the application still has a future in terms of development. Yes, migration can take some time — but in the longer term, it can save a lot of time and money spent on new features.
The best time for migration isn’t set in stone, but we recommend considering it if the current API design is prolonging the time to market or if sticking to a reasonable time-to-market it requires more engineers.
Case Study
The Problem
We had a project with two APIs from a previous agency. These two APIs were developed in haste under pressure; however, both of these differently-structured REST APIs were developed for the same functionality, meaning having both was redundant. But the frontends (mobile FE and web FE) already had working implementations which were using each of these APIs.
The Possible Solutions
The use case of these APIs was mostly filling and reading large forms. There were several options:
- Keep both APIs and therefore the doubled effort for feature development on the backend.
- Completely remake one of the frontends and use only one of the APIs. However, neither one followed the best practices of REST development, so both had their flaws.
- Create a new, unified REST API according to best practices — but the frontends would need to adapt and undergo significant remakes.
- Create a new GraphQL API that would allow the frontends to undergo minimal changes in the parts where they are connected to the backend. This solution aligns well with the nature of GraphQL, which allows to fetch and fill only some parts of the forms in a convenient way.
The Solution
GraphQL was the most optimal solution to the above, especially because it meant not sacrificing best development practices. The engineers on the client’s side agreed that the GraphQL route was the most optimal solution. Although it hasn’t been fully resolved quite yet, we’re well on our way.