😲 Making GraphQL cacheable through a new, single-line query syntax!?

β€” 3 minute read

GraphQL is great, but it has a very big issue: It is not easy to cache in the server (while it is doable, it doesn't come out of the box and requires a good amount of extra engineering).

The problem is GraphQL's query, which generally spans multiple lines, and is sent to the server through the body of the request instead of through URL params. If the query could be passed through URL params instead, we could then use standard mechanisms to cache the page in the server based on its URL as a unique ID.

Sure, we could have a client-side library like Relay simply compress the query in a single line, and append it to the URL. However, the URL will be pretty much unreadable, and we won't be able to manually code it anymore, as we do with REST. So this is not a solution.

A better approach is to re-create the GraphQL syntax, attempting to support all the same elements (field arguments, variables, aliases, fragments, directives, etc), however designed to be easy to write, and easy to read and understand, in a single line, so it can be passed as a URL param.

This is what I did, and I think I might have succeeded!? The results are in this GitHub repo (check it out!), and I show several examples below... ta ta ta taaaannnnnn...

πŸ₯

Introducing my re-imagined GraphQL syntax permalink

In the repo's README is the description of how each query element is coded. Hoping that the syntax is self-evident, or at least understandable enough, here I just only show some examples:

Simple query:
/?query=posts.id|title|url

Nested query:
/?query=posts.comments.author.posts.id|title|url

Retrieving properties along the nested query:
/?query=posts.id|title|url|comments.id|content|date|author.id|name|url|posts.id|title|url

Field arguments:
/?query=posts(searchfor:template,limit:3).id|title

Variables:
/?query=posts(searchfor:$search,limit:$limit).id|title&limit=3&search=template

or:

/?query=posts(searchfor:$search,limit:$limit).id|title&variables[limit]=3&variables[search]=template

Aliases:
/?query=posts(searchfor:template,limit:3)@searchposts.id|title

Bookmarks: (to return to some query path, to keep adding data)
/?query=posts(searchfor:template,limit:3)[searchposts].id|title,[searchposts].author.id|name

Bookmark + Alias:
/?query=posts(searchfor:template,limit:3)[@searchposts].id|title,[searchposts].author.id|name

Fragments:
/?query=posts(limit:3).--postProps,posts(limit:4).author.posts.--postProps&postProps=id|title|url

Or:

/?query=posts(limit:3).--postProps,posts(limit:4).author.posts.--postProps&fragments[postProps]=id|title|url

Directives:
Include:
/?query=posts.id|title|url<include(if:$include)>&variables[include]=true
/?query=posts.id|title|url<include(if:$include)>&variables[include]=

Skip:
/?query=posts.id|title|url<skip(if:$skip)>&variables[skip]=true
/?query=posts.id|title|url<skip(if:$skip)>&variables[skip]=

Combining elements permalink

The different elements can be included within the other elements in a straightforward manner:

Concatenating fragments:
/?query=posts.--fr1.--fr2&fragments[fr1]=author.posts(limit:1)&fragments[fr2]=id|title

Fragments inside fragments:
/?query=posts.--fr1.--fr2&fragments[fr1]=author.posts(limit:1)&fragments[fr2]=id|title|--fr3&fragments[fr3]=author.id|url

Fragments with aliases:
/?query=posts.--fr1.--fr2&fragments[fr1]=author.posts(limit:1)@firstpost&fragments[fr2]=id|title

Fragments with variables:
/?query=posts.--fr1.--fr2&fragments[fr1]=author.posts(limit:$limit)&fragments[fr2]=id|title&variables[limit]=1

Fragments with directives:
/?query=posts.id|--props<include(if:hasComments())>&fragments[props]=title|url<include(if:not(hasComments()))>

Fragments with "Skip output if null":
/?query=posts.id|--props?&fragments[props]=title|url|featuredimage

Superpowers! permalink

Since we are creating a new syntax, why stop in what already exists? We are creating, we are dreaming, let's also build what doesn't exist yet! The following features below are not part of GraphQL, but sure they should be!

Operators:
/?query=not(true)
/?query=or([1, 0])
/?query=and([1, 0])
/?query=if(true,Show this text,Hide this text)
/?query=equals(first text, second text)
/?query=isNull(),isNull(something)
/?query=sprintf(API %s is %s, [PoP, cool]))

Helpers:
/?query=context
/?query=var(route),var(target)@target,var(datastructure)

Composable fields:
/?query=posts.hasComments|not(hasComments())
/?query=posts.hasComments|hasFeaturedImage|or([hasComments(),hasFeaturedImage()])
/?query=var(fetching-site),posts.hasFeaturedImage|and([hasFeaturedImage(), var(fetching-site)])
/?query=posts.if(hasComments(),sprintf(Post with title '%s' has %s comments,[title(), commentsCount()]),sprintf(Post with ID %s was created on %s, [id(),date(d/m/Y)]))@postDesc
/?query=users.name|equals(name(), leo)
/?query=posts.featuredimage|isNull(featuredimage())

Composable fields with directives:

/?query=posts.id|title|featuredimage<include(if:not(isNull(featuredimage())))>.id|src
/?query=posts.id|title|featuredimage<skip(if:isNull(featuredimage()))>.id|src

Skip output if null:

/?query=posts.id|title|featuredimage?.id|src

🦸🏻

Ta daaaa permalink

That seems promising, right!? What do you think? If you like it, check the repo for more info.

Thanks for reading!