Back to blog

Posted 8 months by Jakob Gillich

Open Sourcing Our GraphQL Library for Crystal

Everbase was initially built on top of Rust and Juniper, but we eventually switched over to the Crystal programming language - the why is a topic for another blog post. But what Crystal lacked was a great GraphQL library like Juniper - so we built one.

If you’re not interested in the backstory, head straight to our docs to learn more.

How GraphQL servers work

The three pillars of a GraphQL server implementation are:

  1. Query language parsing
  2. Field resolution
  3. Introspection

In any GraphQL implementation, the first two will look similar. Where they differ is in how they implement introspection.

But what is introspection, you ask? It’s what allows tools like GraphiQL to work. It’s an API that allows you to retrieve information about the types and structure of the API. Here is a query that works on any GraphQL-compliant API:

{
  __schema {
    types {
      name
      kind
    }
  }
}

The result is a list of types and their kind. Something like this:

{
  "data": {
    "__schema": {
      "types": [
        {
          "name": "Boolean",
          "kind": "SCALAR"
        },
        {
          "name": "City",
          "kind": "OBJECT"
        }
        // ...
      ]
    }
  }
}

A GraphQL API must be able to produce this type information at runtime. To achieve this, there are two common approaches.

Schema-First

Schema-first means you specify your schema separately from your implementation. In Graphql.js, you do this:

var schema = buildSchema(`
  type Query {
    hello: String
  }
`);

var root = {
  hello: () => {
    return 'Hello world!';
  },
};

First, we build a schema using a GraphQL definition, and then we provide an implementation. The schema is used to provide type information through the introspection API, and the implementation is used to resolve queries.

However, there is a big problem with this approach. What if my schema does not match the implementation? In the example above, what would happen if hello returns a number? Or what if we change the schema but forget to update the implementation?

It would fail at runtime. This is of course how dynamically typed languages work, and it becomes manageable through automated testing. But it’s not rare for errors to slip through unnoticed.

Statically typed languages offer a solution to this problem.

Code-First

With a code-first implementation, there is no schema. Your schema is your code. With our GraphQL library for Crystal, the example from above becomes:

class Query
  def hello : String
    "Hello world!"
  end
end

What if we return a number instead of a string? Compile error. What if we change the method return type but return something else? Compile error. What if we forget to set a return type? Sorry you can’t, also compile error.

The introspection information is generated through macros, so it’s guaranteed to match the implementation. Getting the schema is a single line of code, so we can have a test to ensure our schema doesn’t change in unexpected ways.

All of this is also standard Crystal code, so we can take full advantage of Crystal’s compiler and tooling to ensure our code is correct. This helps not only with development, where errors are caught quickly, it also increases the trust in your code. You will still have to write tests, but when you combine testing with Crystal’s static type checking, most errors are eliminated before they ever reach production.

Learn more

Here’s our documentation and here’s our Gitlab repo, please open an issue if you have questions.