GraphQL server library for Crystal.
- Boilerplate-free: Schema generated at compile time
- Type-safe: Crystal guarantees your code matches your schema
- High performance: See benchmarks
Install the shard by adding the following to our shard.yml:
dependencies:
  graphql:
    github: graphql-crystal/graphqlThen run shards install.
The first step is to define a query object. This is the root type for all queries and it looks like this:
require "graphql"
@[GraphQL::Object]
class Query < GraphQL::BaseQuery
  @[GraphQL::Field]
  def hello(name : String) : String
    "Hello, #{name}!"
  end
endNow we can create a schema object:
schema = GraphQL::Schema.new(Query.new)To verify we did everything correctly, we can print out the schema:
puts schema.document.to_sWhich, among several built-in types, prints our query type:
type Query {
  hello(name: String!): String!
}To serve our API over HTTP we call schema.execute with the request parameters and receive a JSON string. Here is an example for Kemal:
post "/graphql" do |env|
  env.response.content_type = "application/json"
  query = env.params.json["query"].as(String)
  variables = env.params.json["variables"]?.as(Hash(String, JSON::Any)?)
  operation_name = env.params.json["operationName"]?.as(String?)
  schema.execute(query, variables, operation_name)
endNow we're ready to query our API:
curl \
  -X POST \
  -H "Content-Type: application/json" \
  --data '{ "query": "{ hello(name: \"John Doe\") }" }' \
  http://0.0.0.0:3000/graphqlThis should return:
{ "data": { "hello": "Hello, John Doe!" } }For easier development, we recommend using GraphiQL. A starter template combining Kemal and GraphiQL is found at examples/graphiql.
context is a optional argument that our fields can retrieve. It lets fields
access global data, like database connections.
# Define our own context type
class MyContext < GraphQL::Context
  @pi : Float64
  def initialize(@pi)
  end
end
# Pass it to schema.execute
context = MyContext.new(Math::PI)
schema.execute(query, variables, operation_name, context)
# Access it in our fields
@[GraphQL::Object]
class MyMath < GraphQL::BaseObject
  @[GraphQL::Field]
  def pi(context : MyContext) : Float64
    context.pi
  end
endContext instances must not be reused for multiple executions.
Objects are perhaps the most commonly used type in GraphQL. They are implemented
as classes. To define a object, we need a GraphQL::Object annotation and to inherit
GraphQL::BaseObject. Fields are methods with a GraphQL::Field annotation.
@[GraphQL::Object]
class Foo < GraphQL::BaseObject
  # type restrictions are mandatory on fields
  @[GraphQL::Field]
  def hello(first_name : String, last_name : String) : String
    "Hello #{first_name} #{last_name}"
  end
  # besides basic types, we can also return other objects
  @[GraphQL::Field]
  def bar : Bar
    Bar.new
  end
end
@[GraphQL::Object]
class Bar < GraphQL::BaseObject
  @[GraphQL::Field]
  def baz : Float64
    42_f64
  end
endFor simple objects, we can use instance variables:
@[GraphQL::Object]
class Foo < GraphQL::BaseObject
  @[GraphQL::Field]
  property bar : String
  @[GraphQL::Field]
  getter baz : Float64
  def initialize(@bar, @baz)
  end
endQuery is the root type of all queries.
@[GraphQL::Object]
class Query < GraphQL::BaseQuery
  @[GraphQL::Field]
  def echo(str : String) : String
    str
  end
end
schema = GraphQL::Schema.new(Query.new)Mutation is the root type for all mutations.
@[GraphQL::Object]
class Mutation < GraphQL::BaseMutation
  @[GraphQL::Field]
  def echo(str : String) : String
    str
  end
end
schema = GraphQL::Schema.new(Query.new, Mutation.new)Input objects are objects that are used as field arguments. To define an input
object, use a GraphQL::InputObject annotation and inherit GraphQL::BaseInputObject.
It must define a constructor with a GraphQL::Field annotation.
@[GraphQL::InputObject]
class User < GraphQL::BaseInputObject
  getter first_name : String?
  getter last_name : String?
  @[GraphQL::Field]
  def initialize(@first_name : String?, @last_name : String?)
  end
endDefining enums is straightforward. Just add a GraphQL::Enum annotation:
@[GraphQL::Enum]
enum IPAddressType
  IPv4
  IPv6
endThe following scalar values are supported:
- Int32<->- Int
- Float64<->- Float
- String<->- String
- Bool<->- Boolean
- GraphQL::Scalars::ID<->- String
Built-in custom scalars:
- GraphQL::Scalars::BigInt<->- String
Custom scalars are created by implementing from_json/to_json:
@[GraphQL::Scalar]
class ReverseStringScalar < GraphQL::BaseScalar
  @value : String
  def initialize(@value)
  end
  def self.from_json(string_or_io)
    self.new(String.from_json(string_or_io).reverse)
  end
  def to_json(builder : JSON::Builder)
    builder.scalar(@value.reverse)
  end
endInterfaces are not supported.
Subscriptions are not supported.
Supported on: Object, InputObject, Field, Enum, Scalar
We can use the name argument to customize the introspection type name of a
type. This is not needed in most situations because type names are automatically
converted to PascalCase or camelCase. However, item_id converts to
itemId, but we might want to use itemID. For this, we can use the name
argument.
@[GraphQL::Object(name: "Sheep")]
class Wolf
  @[GraphQL::Field(name: "baa")]
  def howl : String
    "baa"
  end
endSupported on: Object, InputObject, Field, Enum, Scalar
Describes the type. Descriptions are available through the introspection interface so it's always a good idea to set this argument.
@[GraphQL::Object(description: "I'm a sheep, I promise!")]
class Wolf
endSupported on: Field
The deprecated argument marks a type as deprecated.
class Sheep
  @[GraphQL::Field(deprecated: "This was a bad idea.")]
  def fight_wolf : String
    "Wolf ate sheep"
  end
endSets names and descriptions for field arguments. Note that arguments cannot be marked as deprecated.
class Sheep
  @[GraphQL::Field(arguments: {weapon: {name: "weaponName", description: "The weapon the sheep should use."}})]
  def fight_wolf(weapon : String) : String
    if weapon == "Atomic Bomb"
      "Sheep killed wolf"
    else
      "Wolf ate sheep"
    end
  end
endField arguments are automatically resolved. A type with a default value becomes optional. A nilable type is also considered a optional type.