GraphQL is a query language that, by design, is much more restrictive than the others we have seen in this topic. It’s intended for OLTP queries; its purpose is to allow client software running on a user’s device (such as a mobile app or a JavaScript web app frontend) to request a JSON document with a particular structure, containing the fields necessary for rendering its UI.
GraphQL interfaces allow developers to rapidly change queries in client code without changing server-side APIs. That flexibility comes at a cost, however. Organizations that adopt GraphQL often need tooling to convert the queries into requests to internal services, which commonly use REST or gRPC. Authorization, rate limiting, and performance challenges are additional concerns.
The language is also intentionally limited, because GraphQL queries come from untrusted sources. It does not allow anything that could be expensive to execute, since otherwise users could (perhaps unintentionally) cause a denial-of-service condition on a server by running lots of expensive queries. In particular, GraphQL does not allow recursive queries (unlike Cypher, SPARQL, SQL, or Datalog), and it does not allow arbitrary search conditions (like our “find people who were born in the US and are now living in Europe”), unless the service owners specifically choose to offer such search functionality.
Nevertheless, GraphQL is useful. Example 3-13 shows how you might implement a group chat application like Discord or Slack using GraphQL. The query requests all the channels that the user has access to, including the channel name and the 50 most recent messages in each channel. For each message, the query requests the timestamp, the message content, and the name and profile picture URL of the sender. If a message is a reply to another message, the query also requests the name of the sender and the content of that message (which might be rendered in a smaller font above the reply, in order to provide some context).
Example 3-13. A GraphQL query for a group chat application
query ChatApp {
channels {
name
recentMessages(latest: 50) {
timestamp
content
sender {
fullName
imageUrl
}
replyTo {
content
sender {
fullName
}
}
}
}
}
Example 3-14 shows what a response to the query in Example 3-13 might look like. The response is a JSON document that mirrors the structure of the query: it contains exactly those attributes that were requested, no more and no less. This approach has the advantage that the server does not need to know which attributes the client requires in order to render the user interface; the client can simply request what it needs. For example, this query does not request a profile picture URL for the sender of the replyTo message, but if the UI were changed to include that profile picture, it would be easy for the client to add the required imageUrl attribute to the query with no changes on the server side.
Example 3-14. A possible response to the query in Example 3-13
{
"data": {
"channels": [
{
"name": "#general",
"recentMessages": [
{
"timestamp": 1693143014,
"content": "Hey! How are y'all doing?",
"sender": {"fullName": "Aaliyah", "imageUrl": "https://..."},
"replyTo": null
},
{
"timestamp": 1693143024,
"content": "Great! And you?",
"sender": {"fullName": "Caleb", "imageUrl": "https://..."},
"replyTo": {
"content": "Hey! How are y'all doing?",
"sender": {"fullName": "Aaliyah"}
}
},
...
In this example, the name and image URL of a the message’s sender are embedded directly in the message object. If the same user sends multiple messages, this information will be repeated in each message. In principle, it would be possible to reduce this duplication, but GraphQL makes the design choice to accept a larger response size in order to make it simpler to render the UI based on the requested data.
The replyTo field is similar: in Example 3-14, the second message is a reply to the first, and the content (“Hey…”) and sender name (Aaliyah) are duplicated under replyTo. It would be possible to instead return the ID of the message being replied to, but then the client would have to make an additional request to the server if that ID were not among the 50 most recent messages returned. Duplicating the content makes it much simpler to work with the data.
The server’s database can store the data in a more normalized form and perform the necessary joins to process a query. For example, the server might store a message along with the user ID of the sender and the ID of the message it is replying to; when it receives a query like the one in Example 3-13, the server would then resolve those IDs to find the records they refer to. However, only joins that have been explicitly declared in the GraphQL schema can be requested by the client.
Even though the response to a GraphQL query looks similar to a response from a document database, and even though it has “graph” in its name, GraphQL can be implemented on top of any type of database—relational, document, or graph.