This article could also be called "Things your teacher told you not to do with GraphQL".

From reading blog posts and tutorials, we learn that there is a certain way to use GraphQL. But what if something is not mentioned? Is that because it is impossible to implement? Or maybe because it would be a "really bad idea"® to do it?

Let's have a bit of fun playing with GraphQL in an unorthodox way. I'm not suggesting you implement any of the ideas discussed here, certainly not in production (unless you know what you are doing), but, in this article, I'll demonstrate a few experiments I've done with GraphQL. Some of them are just cool tricks. Some others could actually be useful to you. I find all of them awesome.

GraphQL resource-based endpoints

Let's say we have a post field which, given the post's ID, retrieves the corresponding post:

type Root {
  post(id: ID!): Post
}

We can query this field like this:

query FetchPostContent($postId: ID!) {
  post(id: $postId) {
    title
    content
    date
  }
}

...for which we must pass the ID of the post as a variable:

{
  "postId": 1
}

Now, let's say we are given a new requirement: the GraphQL endpoint must, in addition, fetch a post also by its URL. How would we go about this?

One way is to add a new field postByURL to the schema:

type Root {
  postByURL(url: URL!): Post
}

We can then query this new field like this:

query FetchPostContent($postURL: URL!) {
  postByURL(url: $postURL) {
    title
    content
    date
  }
}

...passing the URL as a variable:

{
  "postURL": "https://newapi.getpop.org/uncategorized/hello-world/"
}

Alternatively, the field post could be modified, to receive either an ID or a URL, none of them mandatory:

type Root {
  post(id: ID, url: URL): Post
}

I don't particularly like any of these two solutions. In the first case, I try to avoid duplicating fields, keeping the schema DRY as much as possible; in the second case, the field's validation got weaker. In every case, the schema becomes a bit less elegant.

So I thought: why not have the post's URL become a GraphQL endpoint where the retrieved root element is already the post entity? Could that work?

In other words, could GraphQL emulate REST, and instead of having a single endpoint, every resource on the website could also have an endpoint to itself, satisfied via GraphQL?

Yes, it works. Every post be a GraphQL endpoint to itself on my WordPress blog, just by appending /api/graphql at the end of the post's URL. Instead of starting with Root, each an endpoint retrieves the queried post entity as its root, in this case Post.

This way, this is also a GraphQL endpoint: newapi.getpop.org/uncategorized/hello-world/api/graphql/. I have not enabled a GraphiQL client to query it, but we can specify what properties we want to query via the param ?query= using the PQL syntax (similar to GQL, but better suited for passing queries via the URL).

The following query (where the Post is the root of the query):

{
  title
  content
  date
}

...can be executed by loading this URL: ${ postURL }/api/graphql/?query=title|content|date.

This being GraphQL, we can go as deep down the rabbit hole as we wish to, and pass field arguments too. Loading this URL will execute this query:

{
  title
  content
  formattedDate: date(format: "d/m/Y")
  author {
    name
  }
  comments {
    date
    content
    author {
      name
    }
  }
  categories {
    name
  }
  tags {
    name
  }
}

Want to get all the posts rather than just an individual post? When executing the query on the posts/api/graphql endpoint (which is concerned with the list of all posts, not just a single one) then the root element becomes [Post].

Why do I find this cool? Because this strategy truly merges the best from both REST and GraphQL. Imagine browsing a website and reading a blog post, and then you'd like to fetch its data to cross-post it in your site. All you'd have to do is append /api/graphql to the blog post's URL, and provide the list of fields to query via ?query=, and voilà, you have the data.

Coding a gateway in the query

A gateway is a method for exposing external APIs within the GraphQL schema. It is what StepZen solves via its schema stitching and Apollo via its federation.

When do we need a gateway? When we need to access data from our company which is provided only via legacy APIs (possibly based on REST), or data from a 3rd party provider. Having this data exposed as part of our GraphQL service would make our lives easier, allowing us to use a single interface (GraphQL) to interact with any data required by our application.

I'll make a proposition: let's code a gateway directly in the query, to fetch data from any REST endpoint, to be provided via an input.

A word of caution: we should not expose a public service where users can freely provide the endpoint to fetch. They could point to a really big resource, weighting megabytes, which would take over the bandwitdh devoted to our servers and make us pay the bill for no good reason.

So this strategy makes sense only when users must be authenticated to access the endpoint, or when the GraphQL server provides persisted queries, so we can publish the query on the server-side and restrict users from creating their own queries on the client-side.

Let's do it! A gateway to connect to a REST endpoint is simply a field getJSON that retrieves the data from the provided endpoint, and makes its data available under a custom scalar type JSONObject:

type Root {
  getJSON(url: URL): JSONObject
}

The data in JSONObject might look like this (which comes from querying the REST endpoint for a WordPress blog post):

{
  "id": 1657,
  "date": "2020-12-21T08:24:18",
  "date_gmt": "2020-12-21T08:24:18",
  "guid": {
    "rendered": "https:\/\/newapi.getpop.org\/?p=1657"
  },
  "modified": "2021-01-13T17:12:34",
  "modified_gmt": "2021-01-13T17:12:34",
  "slug": "a-tale-of-two-cities-teaser",
  "status": "publish",
  "type": "post",
  "link": "https:\/\/newapi.getpop.org\/uncategorized\/a-tale-of-two-cities-teaser\/",
  "title": {
    "rendered": "A tale of two cities – teaser"
  },
  "content": {
    "rendered": "\n<p>It was the best of times, it was the worst of times, it was the age of wisdom, it was the age of foolishness, it was the epoch of belief, it was the epoch of incredulity, it was the season of Light, it was the season of Darkness, it was the spring of hope, it was the winter of despair, we had everything before us, we had nothing before us, we were all going direct to Heaven, we were all going direct the other way\u2014in short, the period was so far like the present period, that some of its noisiest authorities insisted on its being received, for good or for evil, in the superlative degree of comparison only.<\/p>\n",
    "protected": false
  },
  "excerpt": {
    "rendered": "<p>It was the best of times, it was the worst of times, it was the age of wisdom, it was the age of foolishness, it was the epoch of belief, it was the epoch of incredulity, it was the season of Light, it was the season of Darkness, it was the spring of hope, it&hellip; <a class=\"more-link\" href=\"https:\/\/newapi.getpop.org\/uncategorized\/a-tale-of-two-cities-teaser\/\">Continue reading <span class=\"screen-reader-text\">A tale of two cities &#8211; teaser<\/span><\/a><\/p>\n",
    "protected": false
  },
  "author": 1,
  "featured_media": 0,
  "comment_status": "closed",
  "ping_status": "closed",
  "sticky": false,
  "template": "",
  "format": "standard",
  "meta": [],
  "categories": [
    1
  ],
  "tags": [
    191
  ]
}

The following query executes the getJSON field:

query FetchPostData($endpoint: URL!) {
  postData: getJSON(url: $endpoint)
}

Once we have the data, we need to access the specific item we need. For instance, if we need the blog post's content, it is accessible under path content.rendered. To do that, we create another field, extract, which, given a JSONObject and a path, retrieves the element within:

type Root {
  extract(object: JSONObject, path: String): Mixed
}

Please notice this field returns type Mixed, which is a custom scalar type to represent all built-in scalar types (String, Int, Float, Boolean and ID). To deal with arrays, we could also create field extractList:

type Root {
  extractList(object: JSONObject, path: String): [Mixed]
}

Using the Mixed type is not ideal, but given that GraphQL presently does not support union of scalar types, I find this an acceptable hack. The alternative to the hack would be to create different fields for different return types (extractString, extractInt, extractFloat, extractBoolean and extractID), and then again for each to return the array type (extractStringList, extractIntList, extractFloatList, extractBooleanList and extractIDList), which I find way too verbose.

The consequence of using Mixed is that the GraphiQL client will show errors from providing a Mixed input where a String or Int is expected, but otherwise the GraphQL server can deal with it without any problem (since a custom scalar provides its own coercing strategy, the Mixed type would basically accept everything).

Let's continue. We can provide several queries, and tie them up together via an @export directive, which will make available the output of a field from some query as an input to the subsequent query.

In this query, the JSON data is exported under a dynamic variable $_postData, and then the specific item under path content.rendered is extracted from it:

query FetchPostData($endpoint: URL!) {
  postData: getJSON(url: $endpoint) @export(as: "_postData")
}

query ExtractPostContent($_postData: Object! = {}) {
  postContent: extract(object: $_postData, path: "content.rendered")
}

# This is a hack to make GraphiQL execute several queries in a single request.
# Select operation "__ALL" from the dropdown when pressing on the "Run" button
query __ALL { id }

...and we indicate the endpoint via a variable:

{
  "endpoint": "https://newapi.getpop.org/wp-json/wp/v2/posts/1657/"
}

Voilà! We can now access data for a REST endpoint, any REST endpoint, in our GraphQL queries.

Manipulating the content from any blog post

Let's find some application for the idea above (which may be useless, but still fun). We can now easily access the data for any REST endpoint, and GraphQL offers formidable data-manipulation capabilities via custom directives. Let's marry the two propositions to manipulate the data from any endpoint.

For instance, since I have coded a @translate directive which uses the Google Translate API, I can now translate the content for any site (in this case, from English to French):

query FetchPostData($endpoint: URL!) {
  postData: getJSON(url: $endpoint) @export(as: "_postData")
}

query ExtractPostContent($_postData: Object! = {}) {
  postContent: extract(object: $_postData, path: "content.rendered") @export(as: "_postContent")
}

query TranslatePostContent($_postContent: String! = "") {
  translatedPostContent: echoStr(value: $_postContent) @translate(from: "en", to: "fr")
}

query __ALL { id }

This other query translates all the descriptions for all my repos in GitHub from English to Spanish:

query FetchGitHubData($endpointURL: String!) {
  ghData: getJSON(url: $endpointURL) @export(as: "_ghData")
}

query TranslateRepoDescriptions($_ghData: Object! = {}) {
  repoDescriptions: extract(object: $_ghData, path: "description") @forEach @translate(from: "en", to: "es", nestedUnder: -1) @export(as: "_repoData")
}

query __ALL { id }

...passing this endpoint:

{
  "endpointURL": "https://api.github.com/users/leoloso/repos"
}

How cool is that!?

Conclusion

Your colleagues told you not to use GraphQL this way. Your boss told you not to use GraphQL that way. And the internet is telling you how you must use GraphQL and not stray from that.

But you can also play. Check out what wild things you can accomplish with GraphQL, not because it is the right way to do it, but because you can. I hope you've enjoyed my experiments.