Maybe you don't need GraphQL

September 03rd, 202612 min read

Ler em portugu锚s

GraphQL is a data query language created by Facebook, in 2012. The idea behind it is very interesting: condense many API calls and avoid sending unnecessary data.

Let's imagine a real case, a social network, like Instagram. To get the comments of a post, the frontend first makes a request to /p/{post_id}/comments, and with this response, for each user, makes a request to /u/{user_id}. This is to fetch the commenter's username and profile picture.

More comments means more users, causing the number of requests to grow rapidly.

With GraphQL, we can concentrate many queries in a single one, and at the same time, not pull irrelevant data, saving time, computer resources and network bandwidth. For the example above, the query could be written like this:

query GetPostComments($post_id: ID!) {
  comments(post_id: $post_id) {
    id
    text
    num_replies
    num_likes
    user {
      id
      username
      profile_img
    }
  }
}

Producing a response like below:

[
  {
    // fields from the Comment entity
    "comment_id": 610808,
    "comment_text": "To the victor, the potatoes.",
    "num_replies": 2,
    "num_likes": 23,
    "user_id": 34269523,
  
    // fields from the User entity
    "username": "machado_de_assis",
    "profile_img": "https://i.instacdn.com/user_34269523.jpg"
  },
  // ...
]

The query manages to fetch comments with each user's username and profile image. If other fields are required, all to do is specify them in the query.

Implementation challenges

Despite its benefits, GraphQL brings some hindrances.

The first one is coupling to the data layer. Most databases don't have native support to GraphQL queries, meaning that a layer of mappers and logic needs to be built on top of another data access layer. This is extra code and extra processing.

The second one is information security. Since each client can define which fields of information it wants, there must be sanitization and validation before executing a query, to block lateral access to unauthorized data.

Third one: GraphQL doesn't natively support aggregate functions (sums, counts, averages). On the scenario above, if num_likes and num_replies are counts over Likes and Comments tables, they will need to be calculated by code in the database query or in the API.

Appropriateness as a solution

GraphQL was designed to provide data for a plurality of clients, each with distinct needs. For example: the mobile app needs data in some specific structure, the web frontend in another, back office in another. In large companies, like Facebook, the adoption of GraphQL may make sense, also considering that there are many integrations with third-party services (B2B, analytics, advertising, etc.).

If this plurality of clients does not exist, or if a few endpoints can provide well most data needs, GraphQL loses attractiveness.

Alternatives

Condensation of queries

The condensation of multiple queries can be done by code on APIs or through joins, on both SQL and NoSQL databases. On the previous case of getting the comments of a post, it can be done through SQL JOINs.

SELECT
  cm.[Id],
  cm.[Text],
  COUNT(DISTINCT cm_rep.[Id]) AS [NumReplies],
  COUNT(DISTINCT lk.[Id]) AS [NumLikes],
  cm.[UserId],
  u.[Username],
  u.[ProfileImg]
FROM [dbo].[Comments] cm
LEFT JOIN [dbo].[Comments] cm_rep ON cm_rep.[ParentId] = cm.[Id]
LEFT JOIN [dbo].[Likes] lk ON lk.[CommentId] = cm.[Id]
INNER JOIN [dbo].[Users] u ON u.[Id] = cm.[UserId]
WHERE cm.[PostId] = @postId
GROUP BY cm.Id, cm.Text, cm.UserId, u.Username, u.ProfileImg

The query may look complex, but it's the most efficient solution in terms of performance.

In high performance systems, NumReplies and NumLikes would probably be fields inside the Comment table and their values would be incremented after each reply and like. The example above is to demonstrate aggregates.

Optional fields

APIs can receive flags from the client indicating which fields it wants to receive.

Imagine a logistics company with a webservice for listing shipments done by truckers, over the endpoint /driver/{driver_id}/shipments. The client can indicate the level of detail through a query parameter:

  • ?detailLevel=summary
[
  {
    "from": "Fazenda Capela do Bosque",
    "to": "CEAGESP S茫o Paulo",
    "departedAt": "2022-08-04T06:10:04",
    "arrivedAt": "2022-08-04T07:48:15",
    "status": "Finished",
  },
  // ...
]
  • ?detailLevel=full
[
  {
    "from": "Fazenda Capela do Bosque",
    "addressFrom": "Estrada da Gl贸ria, 1000, Porto Feliz - SP", 
    "to": "CEAGESP S茫o Paulo",
    "addressTo": "Av. Dr. Gast茫o Vidigal, 1946, S茫o Paulo - SP", 
    "departedAt": "2022-08-04T06:10:04",
    "arrivedAt": "2022-08-04T07:48:15",
    "status": "Finished",
    "loadContent": "Eggs", 
    "loadNetWeightKg": 14300, 
    // ...
  },
  // ...
]

These flags can also be granular (field-specific):

?includeAddresses=true&includeLoad=true

What isn't specified won't be queried and included on the response.

HTTP/2 and HTTP/3

HTTP/2 and HTTP/3 are able to seam many data flows on a single TCP or UDP packet. This is a network traffic optimization.

Let's take again the example above. If we want to obtain the driver's information, it may not make sense to include this data inside the delivery object JSON, therefore, we will need a separate API call. If HTTP/2 or HTTP/3 are used, the responses for /driver/{driver_id}/shipments and /driver/{driver_id} may arrive together in the same network packet.

Learn more about how HTTP/2 and HTTP/3 work in this article.

A

AlexandreHTRB

Campinas / SP,
Brasil