GraphQXL: the missing language extension
Tags: graphql, api, typescript, javascript
Published: 2023-03-12 16:47:57

https://github.com/gabotechs/graphqxl

I work at [Propel](www.propeldata.com), 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](https://relay.dev/graphql/connections.htm), 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:

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

In GraphQXL spread operators can be used to solve this:

**Source GraphQXL**

```graphql
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!
}
```

**Compiled GraphQL**

```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!
}
```


[Try it in the GraphQXL explorer!](https://graphqxl-explorer.vercel.app/?code=aW50ZXJmYWNlIFJlc291cmNlIHsKICBpZDogSUQhCiAgY3JlYXRlZEF0OiBTdHJpbmchCiAgbW9kaWZpZWRBdDogU3RyaW5nCiAgY3JlYXRlZEJ5OiBTdHJpbmchCiAgbW9kaWZpZWRCeTogU3RyaW5nCn0KCnR5cGUgVXNlciBpbXBsZW1lbnRzIFJlc291cmNlIHsKICAuLi5SZXNvdXJjZQogIHVzZXJuYW1lOiBTdHJpbmchCn0KCnR5cGUgUHJvZHVjdCBpbXBsZW1lbnRzIFJlc291cmNlIHsKICAuLi5SZXNvdXJjZQogIHByaWNlOiBGbG9hdCEKfQ==)

### Generics

In GraphQL APIs there are usually some `types` or `inputs` that are very similar, and one clear example is the [cursor-based pagination](https://relay.dev/graphql/connections.htm). Generics are really good at handling structures that look almost the same but have a small difference:

**Source GraphQXL**

```graphql
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>
```

**Compiled GraphQL**

```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!]!
}
```

[Try it in the GraphQXL explorer!](https://graphqxl-explorer.vercel.app/?code=dHlwZSBQcm9kdWN0IHsKICBwcmljZTogRmxvYXQhCn0KCnR5cGUgVXNlciB7CiAgdXNlcm5hbWU6IFN0cmluZyEKfQoKIlRoZSAke3sgdmFyaWFibGVzLlQgfX0ncyBFZGdlIG9iamVjdCBmb3IgcGFnaW5hdGlvbiIKdHlwZSBfRWRnZTxUPiB7CiAgIlRoZSAke3sgdmFyaWFibGVzLlQgfX0ncyBjdXJzb3IgdGhhdCByZWZlcnMgdG8gdGhlIGN1cnJlbnQgbm9kZSIKICBjdXJzb3I6IFN0cmluZyEKICAiVGhlICR7eyB2YXJpYWJsZXMuVCB9fSBpdHNlbGYiCiAgbm9kZTogVCEKfQoKIlRoZSBDb25uZWN0aW9uIG9iamVjdCBmb3IgcGFnaW5hdGluZyBhY3Jvc3MgdGhlICR7eyB2YXJpYWJsZXMuVCB9fXMiCnR5cGUgX0Nvbm5lY3Rpb248VD4gewogICJNZXRhZGF0YSB3aXRoIHRoZSBjdXJyZW50IHBhZ2UgaW5mbyIKICBtZXRhZGF0YTogU3RyaW5nIQogICJMaXN0IG9mICR7eyB2YXJpYWJsZXMuVCB9fXMgZm9yIHRoZSBjdXJyZW50IHBhZ2UiCiAgZWRnZXM6IFtUIV0hCn0KCnR5cGUgUHJvZHVjdEVkZ2UgPSBfRWRnZTxQcm9kdWN0Pgp0eXBlIFByb2R1Y3RDb25uZWN0aW9uID0gX0Nvbm5lY3Rpb248UHJvZHVjdEVkZ2U%2BCnR5cGUgVXNlckVkZ2UgPSBfRWRnZTxVc2VyPgp0eXBlIFVzZXJDb25uZWN0aW9uID0gX0Nvbm5lY3Rpb248VXNlckVkZ2U%2B)

> 💡 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`:

```graphql
type Identifier {
    id: ID!
}
```

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

**Source GraphQXL**

```graphql
import "common-stuff"

type Product {
    id: Identifier
    price: Float!
}
```

**Compiled GraphQL**

```graphql
type Identifier {
    id: ID!
}

type Product {
    id: Identifier
    price: Float!
}
```

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

```graphql
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
}
```

## Is there more?

There is a lot more! Feel free to

- Check out [Propel](https://www.propeldata.com/) if you want to try what we have built in Propel,
- Stop by the GraphQXL’s source repository https://github.com/gabotechs/graphqxl,
- Play around in the GraphQXL explorer [https://graphqxl-explorer.vercel.app](https://graphqxl-explorer.vercel.app/),
- or Read the GraphQXL book [https://gabotechs.github.io/graphqxl](https://gabotechs.github.io/graphqxl/).