|
| 1 | + |
| 2 | + |
| 3 | +GraphQL server library for Crystal. |
| 4 | + |
| 5 | +- **Boilerplate-free**: Schema generated at compile time |
| 6 | +- **Type-safe**: Crystal guarantees your code matches your schema |
| 7 | +- **High performance**: See [benchmarks](https://github.com/graphql-crystal/benchmarks) |
| 8 | + |
| 9 | +## Getting Started |
| 10 | + |
| 11 | +Install the shard by adding the following to our `shard.yml`: |
| 12 | + |
| 13 | +```yaml |
| 14 | +dependencies: |
| 15 | + graphql: |
| 16 | + github: graphql-crystal/graphql |
| 17 | +``` |
| 18 | +
|
| 19 | +Then run `shards install`. |
| 20 | + |
| 21 | +The first step is to define a query object. This is the root type for all |
| 22 | +queries and it looks like this: |
| 23 | + |
| 24 | +```crystal |
| 25 | +require "graphql" |
| 26 | +
|
| 27 | +@[GraphQL::Object] |
| 28 | +class Query < GraphQL::BaseQuery |
| 29 | + @[GraphQL::Field] |
| 30 | + def hello(name : String) : String |
| 31 | + "Hello, #{name}!" |
| 32 | + end |
| 33 | +end |
| 34 | +``` |
| 35 | + |
| 36 | +Now we can create a schema object: |
| 37 | + |
| 38 | +```crystal |
| 39 | +schema = GraphQL::Schema.new(Query.new) |
| 40 | +``` |
| 41 | + |
| 42 | +To verify we did everything correctly, we can print out the schema: |
| 43 | + |
| 44 | +```crystal |
| 45 | +puts schema.document.to_s |
| 46 | +``` |
| 47 | + |
| 48 | +Which, among several built-in types, prints our query type: |
| 49 | + |
| 50 | +```graphql |
| 51 | +type Query { |
| 52 | + hello(name: String!): String! |
| 53 | +} |
| 54 | +``` |
| 55 | + |
| 56 | +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: |
| 57 | + |
| 58 | +```crystal |
| 59 | +post "/graphql" do |env| |
| 60 | + env.response.content_type = "application/json" |
| 61 | +
|
| 62 | + query = env.params.json["query"].as(String) |
| 63 | + variables = env.params.json["variables"]?.as(Hash(String, JSON::Any)?) |
| 64 | + operation_name = env.params.json["operationName"]?.as(String?) |
| 65 | +
|
| 66 | + schema.execute(query, variables, operation_name) |
| 67 | +end |
| 68 | +``` |
| 69 | + |
| 70 | +Now we're ready to query our API: |
| 71 | + |
| 72 | +```bash |
| 73 | +curl \ |
| 74 | + -X POST \ |
| 75 | + -H "Content-Type: application/json" \ |
| 76 | + --data '{ "query": "{ hello(name: \"John Doe\") }" }' \ |
| 77 | + http://0.0.0.0:3000/graphql |
| 78 | +``` |
| 79 | + |
| 80 | +This should return: |
| 81 | + |
| 82 | +```json |
| 83 | +{ "data": { "hello": "Hello, John Doe!" } } |
| 84 | +``` |
| 85 | + |
| 86 | +For easier development, we recommend using [GraphiQL](https://github.com/graphql/graphiql). |
| 87 | +A starter template combining Kemal and GraphiQL is found at [examples/graphiql](examples/graphiql). |
| 88 | + |
| 89 | +## Context |
| 90 | + |
| 91 | +`context` is a optional argument that our fields can retrieve. It lets fields |
| 92 | +access global data, like database connections. |
| 93 | + |
| 94 | +```crystal |
| 95 | +# Define our own context type |
| 96 | +class MyContext < GraphQL::Context |
| 97 | + @pi : Float64 |
| 98 | + def initialize(@pi) |
| 99 | + end |
| 100 | +end |
| 101 | +
|
| 102 | +# Pass it to schema.execute |
| 103 | +context = MyContext.new(Math::PI) |
| 104 | +schema.execute(query, variables, operation_name, context) |
| 105 | +
|
| 106 | +# Access it in our fields |
| 107 | +@[GraphQL::Object] |
| 108 | +class MyMath < GraphQL::BaseObject |
| 109 | + @[GraphQL::Field] |
| 110 | + def pi(context : MyContext) : Float64 |
| 111 | + context.pi |
| 112 | + end |
| 113 | +end |
| 114 | +``` |
| 115 | + |
| 116 | +Context instances must not be reused for multiple executions. |
| 117 | + |
| 118 | +## Objects |
| 119 | + |
| 120 | +Objects are perhaps the most commonly used type in GraphQL. They are implemented |
| 121 | +as classes. To define a object, we need a `GraphQL::Object` annotation and to inherit |
| 122 | +`GraphQL::BaseObject`. Fields are methods with a `GraphQL::Field` annotation. |
| 123 | + |
| 124 | +```crystal |
| 125 | +@[GraphQL::Object] |
| 126 | +class Foo < GraphQL::BaseObject |
| 127 | + # type restrictions are mandatory on fields |
| 128 | + @[GraphQL::Field] |
| 129 | + def hello(first_name : String, last_name : String) : String |
| 130 | + "Hello #{first_name} #{last_name}" |
| 131 | + end |
| 132 | +
|
| 133 | + # besides basic types, we can also return other objects |
| 134 | + @[GraphQL::Field] |
| 135 | + def bar : Bar |
| 136 | + Bar.new |
| 137 | + end |
| 138 | +end |
| 139 | +
|
| 140 | +@[GraphQL::Object] |
| 141 | +class Bar < GraphQL::BaseObject |
| 142 | + @[GraphQL::Field] |
| 143 | + def baz : Float64 |
| 144 | + 42_f64 |
| 145 | + end |
| 146 | +end |
| 147 | +``` |
| 148 | + |
| 149 | +For simple objects, we can use instance variables: |
| 150 | + |
| 151 | +```crystal |
| 152 | +@[GraphQL::Object] |
| 153 | +class Foo < GraphQL::BaseObject |
| 154 | + @[GraphQL::Field] |
| 155 | + property bar : String |
| 156 | +
|
| 157 | + @[GraphQL::Field] |
| 158 | + getter baz : Float64 |
| 159 | +end |
| 160 | +``` |
| 161 | + |
| 162 | +## Query |
| 163 | + |
| 164 | +Query is the root type of all queries. |
| 165 | + |
| 166 | +```crystal |
| 167 | +@[GraphQL::Object] |
| 168 | +class Query < GraphQL::BaseQuery |
| 169 | + @[GraphQL::Field] |
| 170 | + def echo(str : String) : String |
| 171 | + str |
| 172 | + end |
| 173 | +end |
| 174 | +
|
| 175 | +schema = GraphQL::Schema.new(Query.new) |
| 176 | +``` |
| 177 | + |
| 178 | +## Mutation |
| 179 | + |
| 180 | +Mutation is the root type for all mutations. |
| 181 | + |
| 182 | +```crystal |
| 183 | +@[GraphQL::Object] |
| 184 | +class Mutation < GraphQL::BaseMutation |
| 185 | + @[GraphQL::Field] |
| 186 | + def echo(str : String) : String |
| 187 | + str |
| 188 | + end |
| 189 | +end |
| 190 | +
|
| 191 | +schema = GraphQL::Schema.new(Query.new, Mutation.new) |
| 192 | +``` |
| 193 | + |
| 194 | +## Input Objects |
| 195 | + |
| 196 | +Input objects are objects that are used as field arguments. To define an input |
| 197 | +object, use a `GraphQL::InputObject` annotation and inherit `GraphQL::BaseInputObject`. |
| 198 | +It must define a constructor with a `GraphQL::Field` annotation. |
| 199 | + |
| 200 | +```crystal |
| 201 | +@[GraphQL::InputObject] |
| 202 | +class User < GraphQL::BaseInputObject |
| 203 | + getter first_name : String? |
| 204 | + getter last_name : String? |
| 205 | +
|
| 206 | + @[GraphQL::Field] |
| 207 | + def initialize(@first_name : String?, @last_name : String?) |
| 208 | + end |
| 209 | +end |
| 210 | +``` |
| 211 | + |
| 212 | +## Enums |
| 213 | + |
| 214 | +Defining enums is straightforward. Just add a `GraphQL::Enum` annotation: |
| 215 | + |
| 216 | +```crystal |
| 217 | +@[GraphQL::Enum] |
| 218 | +enum IPAddressType |
| 219 | + IPv4 |
| 220 | + IPv6 |
| 221 | +end |
| 222 | +``` |
| 223 | + |
| 224 | +## Scalars |
| 225 | + |
| 226 | +The following scalar values are supported: |
| 227 | + |
| 228 | +- `Int32` <-> `Int` |
| 229 | +- `Float64` <-> `Float` |
| 230 | +- `String` <-> `String` |
| 231 | +- `Bool` <-> `Boolean` |
| 232 | +- `GraphQL::Scalars::ID` <-> `String` |
| 233 | + |
| 234 | +Built-in custom scalars: |
| 235 | + |
| 236 | +- `GraphQL::Scalars::BigInt` <-> `String` |
| 237 | + |
| 238 | +Custom scalars are created by implementing from_json/to_json: |
| 239 | + |
| 240 | +```crystal |
| 241 | +@[GraphQL::Scalar] |
| 242 | +class ReverseStringScalar < GraphQL::BaseScalar |
| 243 | + @value : String |
| 244 | +
|
| 245 | + def initialize(@value) |
| 246 | + end |
| 247 | +
|
| 248 | + def self.from_json(string_or_io) |
| 249 | + self.new(String.from_json(string_or_io).reverse) |
| 250 | + end |
| 251 | +
|
| 252 | + def to_json(builder : JSON::Builder) |
| 253 | + builder.scalar(@value.reverse) |
| 254 | + end |
| 255 | +end |
| 256 | +``` |
| 257 | + |
| 258 | +## Interfaces |
| 259 | + |
| 260 | +Interfaces are not supported. |
| 261 | + |
| 262 | +## Subscriptions |
| 263 | + |
| 264 | +Subscriptions are not supported. |
| 265 | + |
| 266 | +## Annotation Arguments |
| 267 | + |
| 268 | +### name |
| 269 | + |
| 270 | +Supported on: `Object`, `InputObject`, `Field`, `Enum`, `Scalar` |
| 271 | + |
| 272 | +We can use the `name` argument to customize the introspection type name of a |
| 273 | +type. This is not needed in most situations because type names are automatically |
| 274 | +converted to PascalCase or camelCase. However, `item_id` converts to |
| 275 | +`itemId`, but we might want to use `itemID`. For this, we can use the `name` |
| 276 | +argument. |
| 277 | + |
| 278 | +```crystal |
| 279 | +@[GraphQL::Object(name: "Sheep")] |
| 280 | +class Wolf |
| 281 | + @[GraphQL::Field(name: "baa")] |
| 282 | + def howl : String |
| 283 | + "baa" |
| 284 | + end |
| 285 | +end |
| 286 | +``` |
| 287 | + |
| 288 | +### description |
| 289 | + |
| 290 | +Supported on: `Object`, `InputObject`, `Field`, `Enum`, `Scalar` |
| 291 | + |
| 292 | +Describes the type. Descriptions are available through the introspection interface |
| 293 | +so it's always a good idea to set this argument. |
| 294 | + |
| 295 | +```crystal |
| 296 | +@[GraphQL::Object(description: "I'm a sheep, I promise!")] |
| 297 | +class Wolf |
| 298 | +end |
| 299 | +``` |
| 300 | + |
| 301 | +### deprecated |
| 302 | + |
| 303 | +Supported on: `Field` |
| 304 | + |
| 305 | +The deprecated argument marks a type as deprecated. |
| 306 | + |
| 307 | +```crystal |
| 308 | +class Sheep |
| 309 | + @[GraphQL::Field(deprecated: "This was a bad idea.")] |
| 310 | + def fight_wolf : String |
| 311 | + "Wolf ate sheep" |
| 312 | + end |
| 313 | +end |
| 314 | +``` |
| 315 | + |
| 316 | +### arguments |
| 317 | + |
| 318 | +Sets names and descriptions for field arguments. Note that |
| 319 | +arguments cannot be marked as deprecated. |
| 320 | + |
| 321 | +```crystal |
| 322 | +class Sheep |
| 323 | + @[GraphQL::Field(arguments: {weapon: {name: "weaponName", description: "The weapon the sheep should use."}})] |
| 324 | + def fight_wolf(weapon : String) : String |
| 325 | + if weapon == "Atomic Bomb" |
| 326 | + "Sheep killed wolf" |
| 327 | + else |
| 328 | + "Wolf ate sheep" |
| 329 | + end |
| 330 | + end |
| 331 | +end |
| 332 | +``` |
| 333 | + |
| 334 | +## Field Arguments |
| 335 | + |
| 336 | +Field arguments are automatically resolved. A type with a default value becomes |
| 337 | +optional. A nilable type is also considered a optional type. |
0 commit comments