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 likeid
,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 theinput
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
andConnection
even though all look the same. -
Documentation: We document every field, and if there is a common field across different
types
orinputs
, 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
}
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!
}
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!
}
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>
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!]!
}
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!
}
It can be imported and reused into other .graphqxl
files:
Source GraphQXL
import "common-stuff"
type Product {
id: Identifier
price: Float!
}
Compiled 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:
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 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,
- or Read the GraphQXL book https://gabotechs.github.io/graphqxl.
Top comments (0)