DEV Community

Cover image for GraphQXL: the missing language extension
Gabriel
Gabriel

Posted on

GraphQXL: the missing language extension

https://github.com/gabotechs/graphqxl

I work at Propel, a platform for creating serverless analytical data products, and we use GraphQL extensively to enable our customers to communicate with us.

  • Customers can connect their data with Propel using our API.
  • They can execute real-time analytic queries using GraphQL.
  • They can manage their account's resources using GraphQL API requests.

The problem

As you may imagine, Propel's GraphQL API is pretty big, and we want to keep it clean, maintainable, and as well-documented as possible. Unfortunately, sometimes GraphQL does not make things easy for us:

  • Common fields: While defining types, we need to copy-paste all the common fields across a lot of resources, fields like id, createdAt, modifiedBy...
  • CRUD inputs: When defining inputs that are meant to create or modify specific resources, there is a lot of repeated code for handling the input that creates the resource vs the one that modifies it.
  • Cursor pagination: We follow the GraphQL Cursor Connections Specification, and for each paginated resource, we need to define its own Edge and Connection even though all look the same.
  • Documentation: We document every field, and if there is a common field across different types or inputs, we need to maintain the documentation individually for each one.

All these problems can be boiled down to one: the lack of tools the GraphQL SDL specification provides for reusing code.

Code-first approach?

Using a code-first approach and auto-generating the GraphQL schema is a very common way to overcome this problem, but it has some disadvantages:

  • For customer-facing schemas, it can get tricky to control and review exactly what is being served to the customers, as there is no direct control of the GraphQL schema.
  • Ensuring that the documentation is consistent across all the resources is less ergonomic, as the documentation pieces are spread through the code base.
  • Non-technical people find it more difficult to contribute to new API features, as they will need to deal with actual code (TypeScript, Python, GoLang...).

But what if there is another way?

GraphQXL

GraphQXL is a language built on top of the GraphQL SDL syntax that solves the reusability problems of the original language.

It is framework-agnostic, as it is just an independent language that compiles down to plain GraphQL, that way it can be used with any programming language that supports it.

Here are some of GraphQXL’s features:

Field inheritance

One common pattern is to have an interface that defines some common fields that are shared across many types, for example, imagine that we want to reuse the field’s from this interface across multiple types:

interface Resource {
  id: ID!
  createdAt: Date!
  modifiedAt: Date
  createdBy: String!
  modifiedBy: String
}
Enter fullscreen mode Exit fullscreen mode

In GraphQXL spread operators can be used to solve this:

Source GraphQXL

interface Resource {
  id: ID!
  createdAt: String!
  modifiedAt: String
  createdBy: String!
  modifiedBy: String
}

type User implements Resource {
  ...Resource
  username: String!
}

type Product implements Resource {
  ...Resource
  price: Float!
}
Enter fullscreen mode Exit fullscreen mode

Compiled GraphQL

interface Resource {
  id: ID!
  createdAt: String!
  modifiedAt: String
  createdBy: String!
  modifiedBy: String
}

type User implements Resource {
  id: ID!
  createdAt: String!
  modifiedAt: String
  createdBy: String!
  modifiedBy: String
  username: String!
}

type Product implements Resource {
  id: ID!
  createdAt: String!
  modifiedAt: String
  createdBy: String!
  modifiedBy: String
  price: Float!
}
Enter fullscreen mode Exit fullscreen mode

Try it in the GraphQXL explorer!

Generics

In GraphQL APIs there are usually some types or inputs that are very similar, and one clear example is the cursor-based pagination. Generics are really good at handling structures that look almost the same but have a small difference:

Source GraphQXL

type Product {
  price: Float!
}

type User {
  username: String!
}

"The ${{ variables.T }}'s Edge object for pagination"
type _Edge<T> {
  "The ${{ variables.T }}'s cursor that refers to the current node"
  cursor: String!
  "The ${{ variables.T }} itself"
  node: T!
}

"The Connection object for paginating across the ${{ variables.T }}s"
type _Connection<T> {
  "Metadata with the current page info"
  metadata: String!
  "List of ${{ variables.T }}s for the current page"
  edges: [T!]!
}

type ProductEdge = _Edge<Product>
type ProductConnection = _Connection<ProductEdge>
type UserEdge = _Edge<User>
type UserConnection = _Connection<UserEdge>
Enter fullscreen mode Exit fullscreen mode

Compiled GraphQL

type Product {
  price: Float!
}

type User {
  username: String!
}

"The Product's Edge object for pagination"
type ProductEdge {
  "The Product's cursor that refers to the current node"
  cursor: String!
  "The Product itself"
  node: Product!
}

"The Connection object for paginating across the ProductEdges"
type ProductConnection {
  "Metadata with the current page info"
  metadata: String!
  "List of ProductEdges for the current page"
  edges: [ProductEdge!]!
}

"The User's Edge object for pagination"
type UserEdge {
  "The User's cursor that refers to the current node"
  cursor: String!
  "The User itself"
  node: User!
}

"The Connection object for paginating across the UserEdges"
type UserConnection {
  "Metadata with the current page info"
  metadata: String!
  "List of UserEdges for the current page"
  edges: [UserEdge!]!
}
Enter fullscreen mode Exit fullscreen mode

Try it in the GraphQXL explorer!

💡 Note that the documentation for the Edges and the Connections is also autogenerated following the templating rules in the source GraphQXL file.

Import statements

There are different ways and tools developers can use to split a GraphQL schema into multiple files and merge them afterwards, but GraphQXL provides its own: import statements.

The behavior is pretty straightforward and predictable, given this file common-stuff.graphqxl:

type Identifier {
    id: ID!
}
Enter fullscreen mode Exit fullscreen mode

It can be imported and reused into other .graphqxl files:

Source GraphQXL

import "common-stuff"

type Product {
    id: Identifier
    price: Float!
}
Enter fullscreen mode Exit fullscreen mode

Compiled GraphQL

type Identifier {
    id: ID!
}

type Product {
    id: Identifier
    price: Float!
}
Enter fullscreen mode Exit fullscreen mode

A good usage of this feature could end up in a final .graphqxl that looks like this:

import "products"
import "users"
import "subscriptions"
# import other entities

type Query {
  ..._ProductsQuery
  ..._UsersQuery
  ..._SubscriptionsQuer
  # queries for the other entities
}

type Mutation {
  ..._ProductsMutation
  ..._UsersMutation
  ..._SubscriptionsMutation
  # mutations for the other entities
}

schema {
  query: Query
  mutation: Mutation
}
Enter fullscreen mode Exit fullscreen mode

Is there more?

There is a lot more! Feel free to

Top comments (0)